org.yamj.core.service.mediainfo.MediaInfoService.java Source code

Java tutorial

Introduction

Here is the source code for org.yamj.core.service.mediainfo.MediaInfoService.java

Source

/*
 *      Copyright (c) 2004-2013 YAMJ Members
 *      https://github.com/organizations/YAMJ/teams
 *
 *      This file is part of the Yet Another Media Jukebox (YAMJ).
 *
 *      YAMJ is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      any later version.
 *
 *      YAMJ is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with YAMJ.  If not, see <http://www.gnu.org/licenses/>.
 *
 *      Web: https://github.com/YAMJ/yamj-v3
 *
 */
package org.yamj.core.service.mediainfo;

import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.yamj.common.tools.DateTimeTools;
import org.yamj.common.tools.PropertyTools;
import org.yamj.common.type.StatusType;
import org.yamj.core.database.model.AudioCodec;
import org.yamj.core.database.model.MediaFile;
import org.yamj.core.database.model.StageFile;
import org.yamj.core.database.model.Subtitle;
import org.yamj.core.database.model.dto.QueueDTO;
import org.yamj.core.database.service.MediaStorageService;
import org.yamj.core.tools.AspectRatioTools;
import org.yamj.core.tools.Constants;
import org.yamj.core.tools.LanguageTools;

@Service("mediaInfoService")
public class MediaInfoService implements InitializingBean {

    private static final Logger LOG = LoggerFactory.getLogger(LanguageTools.class);

    private static final Pattern PATTERN_CHANNELS = Pattern.compile(".*(\\d{1,2}).*");
    // mediaInfo command line, depend on OS
    private static final String MI_FILENAME_WINDOWS = "MediaInfo.exe";
    private static final String MI_RAR_FILENAME_WINDOWS = "MediaInfo-rar.exe";
    private static final String MI_FILENAME_LINUX = "mediainfo";
    private static final String MI_RAR_FILENAME_LINUX = "mediainfo-rar";
    // media info settings
    private static final File MEDIAINFO_PATH = new File(
            PropertyTools.getProperty("mediainfo.home", "./mediaInfo/"));
    private final List<String> execMediaInfo = new ArrayList<String>();
    private boolean isMediaInfoRar = Boolean.FALSE;
    private boolean isActivated = Boolean.TRUE;
    private static final List<String> RAR_DISK_IMAGES = new ArrayList<String>();

    @Autowired
    private MediaStorageService mediaStorageService;
    @Autowired
    private AspectRatioTools aspectRatioTools;
    @Autowired
    private LanguageTools languageTools;

    @Override
    public void afterPropertiesSet() {
        String OS_NAME = System.getProperty("os.name");
        LOG.debug("Operating System Name   : {}", OS_NAME);
        LOG.debug("Operating System Version: {}", System.getProperty("os.version"));
        LOG.debug("Operating System Type   : {}", System.getProperty("os.arch"));
        LOG.debug("Media Info Path         : {}", MEDIAINFO_PATH);

        File mediaInfoFile;
        if (OS_NAME.contains("Windows")) {
            mediaInfoFile = new File(MEDIAINFO_PATH.getAbsolutePath() + File.separator + MI_RAR_FILENAME_WINDOWS);
            if (!mediaInfoFile.exists()) {
                //  fall back to the normal filename
                mediaInfoFile = new File(MEDIAINFO_PATH.getAbsolutePath() + File.separator + MI_FILENAME_WINDOWS);
            } else {
                // enable the extra mediainfo-rar features
                isMediaInfoRar = Boolean.TRUE;
            }
        } else {
            mediaInfoFile = new File(MEDIAINFO_PATH.getAbsolutePath() + File.separator + MI_RAR_FILENAME_LINUX);
            if (!mediaInfoFile.exists()) {
                // Fall back to the normal filename
                mediaInfoFile = new File(MEDIAINFO_PATH.getAbsolutePath() + File.separator + MI_FILENAME_LINUX);
            } else {
                // enable the extra mediainfo-rar features
                isMediaInfoRar = Boolean.TRUE;
            }
        }

        if (!mediaInfoFile.canExecute()) {
            LOG.info("Couldn't find CLI mediaInfo executable tool: Media file data won't be extracted");
            isActivated = Boolean.FALSE;
        } else {
            if (OS_NAME.contains("Windows")) {
                execMediaInfo.add("cmd.exe");
                execMediaInfo.add("/E:1900");
                execMediaInfo.add("/C");
                execMediaInfo.add(mediaInfoFile.getName());
                execMediaInfo.add("-f");
            } else {
                execMediaInfo.add("./" + mediaInfoFile.getName());
                execMediaInfo.add("-f");
            }

            if (isMediaInfoRar) {
                LOG.info("MediaInfo-rar tool found, additional scanning functions enabled.");
            } else {
                LOG.info("MediaInfo tool will be used to extract video data. But not RAR and ISO formats");
            }
            isActivated = Boolean.TRUE;
        }

        // Add a list of supported extensions
        for (String ext : PropertyTools.getProperty("mediainfo.rar.diskExtensions", "iso,img,rar,001").split(",")) {
            RAR_DISK_IMAGES.add(ext.toLowerCase());
        }
    }

    public boolean isMediaInfoActivated() {
        return isActivated;
    }

    public void processingError(QueueDTO queueElement) {
        if (queueElement == null) {
            // nothing to
            return;
        }

        mediaStorageService.errorMediaFile(queueElement.getId());
    }

    public void scanMediaInfo(Long id) {
        MediaFile mediaFile = mediaStorageService.getRequiredMediaFile(id);

        StageFile stageFile = mediaFile.getVideoFile();
        if (stageFile == null) {
            LOG.error("No valid video file found for media file: {}", mediaFile.getFileName());
            mediaFile.setStatus(StatusType.ERROR);
            mediaStorageService.update(mediaFile);
            return;
        }

        // check if stage file can be read by MediaInfo
        File file = new File(stageFile.getFullPath());
        boolean scanned = false;
        if (!file.exists()) {
            LOG.warn("Media file not found: {}", stageFile.getFullPath());
        } else if (!file.canRead()) {
            LOG.warn("Media file not readable: {}", stageFile.getFullPath());
        } else {
            LOG.debug("Scanning media file {}", stageFile.getFullPath());
            InputStream is = null;
            try {
                is = createInputStream(stageFile.getFullPath());
                Map<String, String> infosGeneral = new HashMap<String, String>();
                List<Map<String, String>> infosVideo = new ArrayList<Map<String, String>>();
                List<Map<String, String>> infosAudio = new ArrayList<Map<String, String>>();
                List<Map<String, String>> infosText = new ArrayList<Map<String, String>>();

                parseMediaInfo(is, infosGeneral, infosVideo, infosAudio, infosText);

                updateMediaFile(mediaFile, infosGeneral, infosVideo, infosAudio, infosText);

                scanned = true;
            } catch (IOException error) {
                LOG.error("Failed reading mediainfo output: {}", stageFile);
                LOG.warn("MediaInfo error", error);
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException ex) {
                        LOG.trace("Failed to close stream: {}", ex.getMessage(), ex);
                    }
                }
            }
        }

        if (scanned) {
            mediaFile.setStatus(StatusType.DONE);
        } else {
            mediaFile.setStatus(StatusType.ERROR);
        }
        mediaStorageService.updateMediaFile(mediaFile);
    }

    private void updateMediaFile(MediaFile mediaFile, Map<String, String> infosGeneral,
            List<Map<String, String>> infosVideo, List<Map<String, String>> infosAudio,
            List<Map<String, String>> infosText) {

        String infoValue;

        // get container format from general section
        infoValue = infosGeneral.get("Format");
        if (StringUtils.isNotBlank(infoValue)) {
            mediaFile.setContainer(infoValue);
        }

        // get overall bit rate from general section
        infoValue = infosGeneral.get("Overall bit rate");
        mediaFile.setOverallBitrate(getBitRate(infoValue));

        // get runtime either from video info or general section
        String runtime = getRuntime(infosGeneral, infosVideo);
        if (StringUtils.isNotBlank(runtime)) {
            mediaFile.setRuntime(DateTimeTools.processRuntime(runtime));
        } else {
            mediaFile.setRuntime(-1);
        }

        // get Info from first video stream only
        // TODO can evolve to get info from longest video stream
        if (infosVideo.size() > 0) {
            Map<String, String> infosMainVideo = infosVideo.get(0);

            // codec
            mediaFile.setCodec(infosMainVideo.get("Codec ID"));
            mediaFile.setCodecFormat(infosMainVideo.get("Format"));
            mediaFile.setCodecProfile(infosMainVideo.get("Format profile"));

            // width
            mediaFile.setWidth(-1);
            try {
                infoValue = infosMainVideo.get("Width");
                if (StringUtils.isNumeric(infoValue)) {
                    mediaFile.setWidth(Integer.parseInt(infoValue));
                }
            } catch (NumberFormatException error) {
                LOG.trace("Failed to parse width: {}", infoValue, error);
            }

            // height
            mediaFile.setHeight(-1);
            try {
                infoValue = infosMainVideo.get("Height");
                if (StringUtils.isNumeric(infoValue)) {
                    mediaFile.setHeight(Integer.parseInt(infoValue));
                }
            } catch (NumberFormatException ex) {
                LOG.trace("Failed to parse height: {}", infoValue, ex);
            }

            // frame rate
            infoValue = infosMainVideo.get("Frame rate");
            if (StringUtils.isBlank(infoValue)) {
                // use original frame rate
                infoValue = infosMainVideo.get("Original frame rate");
            }
            if (StringUtils.isNotBlank(infoValue)) {
                try {
                    int inxDiv = infoValue.indexOf(Constants.SPACE_SLASH_SPACE);
                    if (inxDiv > -1) {
                        infoValue = infoValue.substring(0, inxDiv);
                    }
                    mediaFile.setFps(Float.parseFloat(infoValue));
                } catch (NumberFormatException error) {
                    LOG.debug("Failed to parse frame rate: {}", infoValue, error);
                }
            }

            // aspect ratio
            infoValue = infosMainVideo.get("Display aspect ratio");
            mediaFile.setAspectRatio(aspectRatioTools.cleanAspectRatio(infoValue));

            // bit rate
            mediaFile.setBitrate(getBitRate(infosMainVideo));

            // check 3D video source,
            infoValue = infosMainVideo.get("MultiView_Count");
            if ("2".equals(infoValue)) {
                mediaFile.setVideoSource("3D");
            }
        }

        // cycle through audio streams
        Set<AudioCodec> processedAudioCodecs = new HashSet<AudioCodec>(0);
        for (int numAudio = 0; numAudio < infosAudio.size(); numAudio++) {
            Map<String, String> infosCurrentAudio = infosAudio.get(numAudio);
            AudioCodec codec = mediaFile.getAudioCodec(numAudio + 1);
            if (codec == null) {
                codec = new AudioCodec();
                codec.setCounter(numAudio + 1);
                codec.setMediaFile(mediaFile);
            }
            parseAudioCodec(codec, infosCurrentAudio);
            mediaFile.getAudioCodecs().add(codec);
            processedAudioCodecs.add(codec);
        }

        // remove unprocessed internal audio codecs
        Iterator<AudioCodec> iterAudio = mediaFile.getAudioCodecs().iterator();
        while (iterAudio.hasNext()) {
            if (!processedAudioCodecs.contains(iterAudio.next())) {
                iterAudio.remove();
            }
        }

        // cycle through subtitle streams
        Set<Subtitle> processedSubtitles = new HashSet<Subtitle>(0);
        for (int numText = 0; numText < infosText.size(); numText++) {
            Map<String, String> infosCurrentText = infosText.get(numText);
            Subtitle subtitle = mediaFile.getSubtitle(numText + 1);
            if (subtitle == null) {
                subtitle = new Subtitle();
                subtitle.setCounter(numText + 1);
                subtitle.setMediaFile(mediaFile);
            }

            boolean processed = parseSubtitle(subtitle, infosCurrentText);
            if (processed) {
                mediaFile.getSubtitles().add(subtitle);
                processedSubtitles.add(subtitle);
            }
        }

        // remove unprocessed internal subtitles
        Iterator<Subtitle> iterSubs = mediaFile.getSubtitles().iterator();
        while (iterSubs.hasNext()) {
            Subtitle subtitle = iterSubs.next();
            if (subtitle.getStageFile() == null && !processedSubtitles.contains(subtitle)) {
                iterSubs.remove();
            }
        }
    }

    private void parseAudioCodec(AudioCodec audioCodec, Map<String, String> infosAudio) {
        // codec
        String infoValue = infosAudio.get("Codec ID");
        if (StringUtils.isBlank(infoValue)) {
            audioCodec.setCodec(Constants.UNDEFINED);
        } else {
            audioCodec.setCodec(infoValue);
        }

        // codec format
        infoValue = infosAudio.get("Format");
        if (StringUtils.isBlank(infoValue)) {
            audioCodec.setCodecFormat(Constants.UNDEFINED);
        } else {
            audioCodec.setCodecFormat(infoValue);
        }

        // bit rate
        audioCodec.setBitRate(getBitRate(infosAudio));

        // number of channels
        audioCodec.setChannels(-1);
        infoValue = infosAudio.get("Channel(s)");
        if (StringUtils.isNotBlank(infoValue)) {
            if (infoValue.contains("/")) {
                infoValue = infoValue.substring(0, infoValue.indexOf('/'));
            }
            try {
                Matcher codecMatch = PATTERN_CHANNELS.matcher(infoValue);
                if (codecMatch.matches()) {
                    audioCodec.setChannels(Integer.parseInt(codecMatch.group(1)));
                }
            } catch (NumberFormatException ex) {
                LOG.trace("Failed to parse channels: {}", infoValue, ex);
            }
        }

        // language
        audioCodec.setLanguage(Constants.UNDEFINED);
        infoValue = infosAudio.get("Language");
        if (StringUtils.isNotBlank(infoValue)) {
            if (infoValue.contains("/")) {
                infoValue = infoValue.substring(0, infoValue.indexOf('/')).trim(); // In this case, language are "doubled", just take the first one.
            }
            // determine language
            if (StringUtils.isNotBlank(infoValue)) {
                String language = languageTools.determineLanguage(infoValue);
                if (StringUtils.isNotBlank(language)) {
                    audioCodec.setLanguage(language);
                }
            }
        }
    }

    private boolean parseSubtitle(Subtitle subtitle, Map<String, String> infosText) {
        // format
        String infoFormat = infosText.get("Format");
        if (StringUtils.isBlank(infoFormat)) {
            // use codec instead format
            infoFormat = infosText.get("Codec");
        }

        // language
        String infoLanguage = infosText.get("Language");
        if (StringUtils.isNotBlank(infoLanguage)) {
            if (infoLanguage.contains("/")) {
                infoLanguage = infoLanguage.substring(0, infoLanguage.indexOf('/')).trim(); // In this case, language are "doubled", just take the first one.
            }
            // determine language
            infoLanguage = languageTools.determineLanguage(infoLanguage);
        }

        // just use defined formats
        if ("SRT".equalsIgnoreCase(infoFormat) || "UTF-8".equalsIgnoreCase(infoFormat)
                || "RLE".equalsIgnoreCase(infoFormat) || "PGS".equalsIgnoreCase(infoFormat)
                || "ASS".equalsIgnoreCase(infoFormat) || "VobSub".equalsIgnoreCase(infoFormat)) {
            subtitle.setFormat(infoFormat);
            if (StringUtils.isBlank(infoLanguage)) {
                subtitle.setLanguage(Constants.UNDEFINED);
            } else {
                subtitle.setLanguage(infoLanguage);
            }
            return Boolean.TRUE;
        }

        LOG.debug("Subtitle format skipped: {}", infoFormat);
        return Boolean.FALSE;
    }

    public boolean isRarDiskImage(String filename) {
        if (isMediaInfoRar && (RAR_DISK_IMAGES.contains(FilenameUtils.getExtension(filename).toLowerCase()))) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    private String getRuntime(Map<String, String> infosGeneral, List<Map<String, String>> infosVideo) {
        String runtimeValue = null;
        if (runtimeValue == null) {
            runtimeValue = infosGeneral.get("PlayTime");
        }
        if ((runtimeValue == null) && (infosVideo.size() > 0)) {
            Map<String, String> infosMainVideo = infosVideo.get(0);
            runtimeValue = infosMainVideo.get("Duration");
        }
        if (runtimeValue == null) {
            runtimeValue = infosGeneral.get("Duration");
        }
        if (runtimeValue != null) {
            if (runtimeValue.indexOf('.') >= 0) {
                runtimeValue = runtimeValue.substring(0, runtimeValue.indexOf('.'));
            }
        }
        return runtimeValue;
    }

    public int getBitRate(Map<String, String> infos) {
        String bitRateValue = infos.get("Bit rate");
        if (StringUtils.isBlank(bitRateValue)) {
            bitRateValue = infos.get("Nominal bit rate");
        }
        return getBitRate(bitRateValue);
    }

    private int getBitRate(String bitRateValue) {
        if (StringUtils.isNotBlank(bitRateValue)) {
            if (bitRateValue.indexOf(Constants.SPACE_SLASH_SPACE) > -1) {
                bitRateValue = bitRateValue.substring(0, bitRateValue.indexOf(Constants.SPACE_SLASH_SPACE));
            }
            try {
                bitRateValue = bitRateValue.substring(0, bitRateValue.length() - 3);
                return Integer.parseInt(bitRateValue);
            } catch (NumberFormatException ex) {
                LOG.trace("Failed to parse bit rate: {}", bitRateValue, ex);
            }
        }
        return -1;
    }

    private InputStream createInputStream(String movieFilePath) throws IOException {
        // Create the command line
        List<String> commandMedia = new ArrayList<String>(execMediaInfo);
        commandMedia.add(movieFilePath);

        ProcessBuilder pb = new ProcessBuilder(commandMedia);

        // set up the working directory.
        pb.directory(MEDIAINFO_PATH);

        Process p = pb.start();
        return p.getInputStream();
    }

    /**
     * Read the input skipping any blank lines
     *
     * @param input
     * @return
     * @throws IOException
     */
    private String localInputReadLine(BufferedReader input) throws IOException {
        String line = input.readLine();
        while ((line != null) && (line.equals(""))) {
            line = input.readLine();
        }
        return line;
    }

    public void parseMediaInfo(InputStream in, Map<String, String> infosGeneral,
            List<Map<String, String>> infosVideo, List<Map<String, String>> infosAudio,
            List<Map<String, String>> infosText) throws IOException {

        InputStreamReader isr = null;
        BufferedReader bufReader = null;

        try {
            isr = new InputStreamReader(in);
            bufReader = new BufferedReader(isr);

            // Improvement, less code line, each cat have same code, so use the same for all.
            Map<String, List<Map<String, String>>> matches = new HashMap<String, List<Map<String, String>>>();

            // Create a fake one for General, we got only one, but to use the same algo we must create this one.
            String generalKey[] = { "General", "Gneral", "* Gnral" };
            matches.put(generalKey[0], new ArrayList<Map<String, String>>());
            matches.put(generalKey[1], matches.get(generalKey[0]));
            matches.put(generalKey[2], matches.get(generalKey[0]));
            matches.put("Video", infosVideo);
            matches.put("Vido", matches.get("Video"));
            matches.put("Audio", infosAudio);
            matches.put("Text", infosText);

            String line = localInputReadLine(bufReader);
            String label;

            while (line != null) {
                // In case of new format : Text #1, Audio #1
                if (line.indexOf('#') >= 0) {
                    line = line.substring(0, line.indexOf('#')).trim();
                }

                // Get cat ArrayList from cat name.
                List<Map<String, String>> currentCat = matches.get(line);

                if (currentCat != null) {
                    HashMap<String, String> currentData = new HashMap<String, String>();
                    int indexSeparator = -1;
                    while (((line = localInputReadLine(bufReader)) != null)
                            && ((indexSeparator = line.indexOf(" : ")) != -1)) {
                        label = line.substring(0, indexSeparator).trim();
                        if (currentData.get(label) == null) {
                            currentData.put(label, line.substring(indexSeparator + 3));
                        }
                    }
                    currentCat.add(currentData);
                } else {
                    line = localInputReadLine(bufReader);
                }
            }

            // Setting General Info - Beware of lose data if infosGeneral already have some ...
            try {
                for (String generalKey1 : generalKey) {
                    List<Map<String, String>> arrayList = matches.get(generalKey1);
                    if (arrayList.size() > 0) {
                        Map<String, String> datas = arrayList.get(0);
                        if (datas.size() > 0) {
                            infosGeneral.putAll(datas);
                            break;
                        }
                    }
                }
            } catch (Exception ignore) {
                // We don't care about this exception
            }
        } finally {
            if (isr != null) {
                isr.close();
            }

            if (bufReader != null) {
                bufReader.close();
            }
        }
    }
}