com.moviejukebox.scanner.MediaInfoScanner.java Source code

Java tutorial

Introduction

Here is the source code for com.moviejukebox.scanner.MediaInfoScanner.java

Source

/*
 *      Copyright (c) 2004-2016 YAMJ Members
 *      https://github.com/orgs/YAMJ/people
 *
 *      This file is part of the Yet Another Movie Jukebox (YAMJ) project.
 *
 *      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-v2
 *
 */
package com.moviejukebox.scanner;

import com.moviejukebox.model.Codec;
import com.moviejukebox.model.Movie;
import com.moviejukebox.model.MovieFile;
import com.moviejukebox.model.enumerations.CodecSource;
import com.moviejukebox.model.enumerations.CodecType;
import com.moviejukebox.model.enumerations.OverrideFlag;
import com.moviejukebox.tools.*;
import com.mucommander.file.AbstractFile;
import com.mucommander.file.ArchiveEntry;
import com.mucommander.file.FileFactory;
import com.mucommander.file.impl.iso.IsoArchiveFile;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.xmm.moviemanager.fileproperties.FilePropertiesMovie;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Grael
 */
public class MediaInfoScanner {

    private static final Logger LOG = LoggerFactory.getLogger(MediaInfoScanner.class);
    private static final String MEDIAINFO_PLUGIN_ID = "mediainfo";
    private static final String SPLIT_GENRE = "(?<!-)/|,|\\|"; // Caters for the case where "-/" is not wanted as part of the split
    private static final Pattern PATTERN_CHANNELS = Pattern.compile(".*(\\d{1,2}).*");
    // mediaInfo repository
    private static final File MI_PATH = new File(PropertiesUtil.getProperty("mediainfo.home", "./mediaInfo/"));
    // mediaInfo command line, depend on OS
    private static final List<String> MI_EXE = new ArrayList<>();
    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";
    private static boolean isMediaInfoRar = Boolean.FALSE;
    public static final String OS_NAME = System.getProperty("os.name");
    public static final String OS_VERSION = System.getProperty("os.version");
    public static final String OS_ARCH = System.getProperty("os.arch");
    private static final boolean IS_ACTIVATED;
    private static final boolean ENABLE_METADATA = PropertiesUtil.getBooleanProperty("mediainfo.metadata.enable",
            Boolean.FALSE);
    private static final boolean ENABLE_UPDATE = PropertiesUtil.getBooleanProperty("mediainfo.update.enable",
            Boolean.FALSE);
    private static final boolean ENABLE_MULTIPART = PropertiesUtil.getBooleanProperty("mediainfo.multipart.enable",
            Boolean.TRUE);
    private static final boolean MI_OVERALL_BITRATE = PropertiesUtil.getBooleanProperty("mediainfo.overallbitrate",
            Boolean.FALSE);
    private static final boolean MI_READ_FROM_FILE = PropertiesUtil.getBooleanProperty("mediainfo.readfromfile",
            Boolean.FALSE);
    private final String randomDirName;
    private static final AspectRatioTools ASPECT_TOOLS = new AspectRatioTools();
    private static final String LANG_DELIM = PropertiesUtil.getProperty("mjb.language.delimiter",
            Movie.SPACE_SLASH_SPACE);
    private static final String AUDIO_LANG_UNKNOWN = PropertiesUtil.getProperty("mjb.language.audio.unknown");
    private static final List<String> MI_DISK_IMAGES = new ArrayList<>();
    // DVD rip infos Scanner
    private final DVDRipScanner localDVDRipScanner;

    static {
        File checkMediainfo = findMediaInfo();

        if (checkMediainfo.canExecute()) {
            IS_ACTIVATED = Boolean.TRUE;

            LOG.debug("Operating System Name   : {}", OS_NAME);
            LOG.debug("Operating System Version: {}", OS_VERSION);
            LOG.debug("Operating System Type   : {}", OS_ARCH);
            LOG.debug("MediaInfo file          : {}", checkMediainfo.getAbsolutePath());

            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");
            }

            if (MI_EXE.isEmpty()) {
                if (OS_NAME.contains("Windows")) {
                    MI_EXE.add("cmd.exe");
                    MI_EXE.add("/E:1900");
                    MI_EXE.add("/C");
                    MI_EXE.add(checkMediainfo.getName());
                    MI_EXE.add("-f");
                } else {
                    MI_EXE.add("./" + checkMediainfo.getName());
                    MI_EXE.add("-f");
                }
            }

            // Add a list of supported extensions
            for (String ext : PropertiesUtil.getProperty("mediainfo.rar.diskExtensions", "iso,img,rar,001")
                    .split(",")) {
                MI_DISK_IMAGES.add(ext.toLowerCase());
            }
        } else {
            LOG.info("Couldn't find CLI mediaInfo executable tool: Video file data won't be extracted");
            LOG.info("File: {}", checkMediainfo.getAbsolutePath());
            IS_ACTIVATED = Boolean.FALSE;
        }

    }

    public MediaInfoScanner() {
        localDVDRipScanner = new DVDRipScanner();
        randomDirName = PropertiesUtil.getProperty("mjb.jukeboxTempDir", "./temp") + "/isoTEMP/"
                + Thread.currentThread().getName();
    }

    /**
     * Check if filename has RAR extension
     *
     * @param filename
     * @return
     */
    public boolean extendedExtension(String filename) {
        if (isMediaInfoRar && (MI_DISK_IMAGES.contains(FilenameUtils.getExtension(filename).toLowerCase()))) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * Update movie details if there are new files
     *
     * @param currentMovie
     */
    public void update(Movie currentMovie) {
        if (!ENABLE_UPDATE) {
            // update not enabled
            return;
        }

        if (currentMovie.getFile().isDirectory()) {
            // no update needed if movie file is a directory (DVD structure)
            return;
        }

        // TODO add check if movie file is newer than generated movie XML
        //      in order to update possible changed media info values
        // no update if movie has no new files
        if (!currentMovie.hasNewMovieFiles()) {
            return;
        }

        // check if main file has changed
        boolean mainFileIsNew = Boolean.FALSE;

        try {
            // get the canonical path for movie file
            String movieFilePath = currentMovie.getFile().getCanonicalPath();
            String newFilePath;
            for (MovieFile movieFile : currentMovie.getMovieFiles()) {
                if (movieFile.isNewFile()) {
                    try {
                        // just compare paths to be sure that
                        // the main movie file is new
                        newFilePath = movieFile.getFile().getCanonicalPath();
                        if (movieFilePath.equalsIgnoreCase(newFilePath)) {
                            mainFileIsNew = Boolean.TRUE;
                            break;
                        }
                    } catch (IOException ignore) {
                        // nothing to do
                    }
                }
            }
        } catch (IOException ignore) {
            // nothing to do
        }

        if (mainFileIsNew) {
            LOG.debug("Main movie file has changed; rescan media info");
            this.scan(currentMovie);
        }
    }

    /**
     * Process the movie files for media information
     *
     * @param currentMovie
     */
    public void scan(Movie currentMovie) {
        if (currentMovie.getFile().isDirectory()) {
            // Scan IFO files
            FilePropertiesMovie mainMovieIFO = localDVDRipScanner.executeGetDVDInfo(currentMovie.getFile());
            if (mainMovieIFO != null) {
                if (IS_ACTIVATED) {
                    scan(currentMovie, mainMovieIFO.getLocation(), Boolean.FALSE);
                    // Issue 1176 - Prevent lost of NFO Data
                    if (StringTools.isNotValidString(currentMovie.getRuntime())) {
                        currentMovie.setRuntime(DateTimeTools.formatDuration(mainMovieIFO.getDuration()),
                                MEDIAINFO_PLUGIN_ID);
                    }
                } else if (OverrideTools.checkOverwriteRuntime(currentMovie, MEDIAINFO_PLUGIN_ID)) {
                    currentMovie.setRuntime(DateTimeTools.formatDuration(mainMovieIFO.getDuration()),
                            MEDIAINFO_PLUGIN_ID);
                }
            }
        } else if (!isMediaInfoRar
                && (MI_DISK_IMAGES.contains(FilenameUtils.getExtension(currentMovie.getFile().getName())))) {
            // extracting IFO files from ISO file
            AbstractFile abstractIsoFile;

            // Issue 979: Split the reading of the ISO file to catch any errors
            try {
                abstractIsoFile = FileFactory.getFile(currentMovie.getFile().getAbsolutePath());
            } catch (Exception error) {
                LOG.debug("Error reading disk Image. Please re-rip and try again");
                LOG.info(error.getMessage());
                return;
            }

            IsoArchiveFile scannedIsoFile = new IsoArchiveFile(abstractIsoFile);
            File tempRep = new File(randomDirName + "/VIDEO_TS");
            FileTools.makeDirs(tempRep);

            try {
                // Convert the returned vector to a List
                List<ArchiveEntry> allEntries = new ArrayList<>(scannedIsoFile.getEntries());
                Iterator<ArchiveEntry> parcoursEntries = allEntries.iterator();
                while (parcoursEntries.hasNext()) {
                    ArchiveEntry currentArchiveEntry = parcoursEntries.next();
                    if (currentArchiveEntry.getName().toLowerCase().endsWith(".ifo")) {
                        File currentIFO = new File(
                                randomDirName + "/VIDEO_TS" + File.separator + currentArchiveEntry.getName());
                        try (OutputStream fosCurrentIFO = FileTools.createFileOutputStream(currentIFO)) {
                            byte[] ifoFileContent = new byte[Integer
                                    .parseInt(Long.toString(currentArchiveEntry.getSize()))];
                            scannedIsoFile.getEntryInputStream(currentArchiveEntry).read(ifoFileContent);
                            fosCurrentIFO.write(ifoFileContent);
                        }
                    }
                }
            } catch (IOException | NumberFormatException error) {
                LOG.info(error.getMessage());
            }

            // Scan IFO files
            FilePropertiesMovie mainMovieIFO = localDVDRipScanner.executeGetDVDInfo(tempRep);
            if (mainMovieIFO != null) {
                if (IS_ACTIVATED) {
                    scan(currentMovie, mainMovieIFO.getLocation(), Boolean.FALSE);
                    // Issue 1176 - Prevent lost of NFO Data
                    if (StringTools.isNotValidString(currentMovie.getRuntime())) {
                        currentMovie.setRuntime(DateTimeTools.formatDuration(mainMovieIFO.getDuration()),
                                MEDIAINFO_PLUGIN_ID);
                    }
                } else if (OverrideTools.checkOverwriteRuntime(currentMovie, MEDIAINFO_PLUGIN_ID)) {
                    currentMovie.setRuntime(DateTimeTools.formatDuration(mainMovieIFO.getDuration()),
                            MEDIAINFO_PLUGIN_ID);
                }
            }

            // Clean up
            FileTools.deleteDir(randomDirName);
        } else if (IS_ACTIVATED) {
            if (isMediaInfoRar
                    && MI_DISK_IMAGES.contains(FilenameUtils.getExtension(currentMovie.getFile().getName()))) {
                LOG.debug("Using MediaInfo-rar to scan {}", currentMovie.getFile().getName());
            }
            scan(currentMovie, currentMovie.getFile().getAbsolutePath(), Boolean.TRUE);
        }

    }

    private void scan(Movie currentMovie, String movieFilePath, boolean processMultiPart) {
        // retrieve values usable for multipart values
        Map<String, String> infosMultiPart = new HashMap<>();
        if (isMultiPartsScannable(currentMovie, processMultiPart)) {
            for (MovieFile movieFile : currentMovie.getMovieFiles()) {
                if (movieFile.getFile() == null) {
                    continue;
                }
                String filePath = movieFile.getFile().getAbsolutePath();
                // avoid double scanning of files
                if (!movieFilePath.equalsIgnoreCase(filePath)) {
                    scanMultiParts(filePath, infosMultiPart);
                }
            }
        }

        try (MediaInfoStream stream = createStream(movieFilePath)) {

            Map<String, String> infosGeneral = new HashMap<>();
            List<Map<String, String>> infosVideo = new ArrayList<>();
            List<Map<String, String>> infosAudio = new ArrayList<>();
            List<Map<String, String>> infosText = new ArrayList<>();

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

            updateMovieInfo(currentMovie, infosGeneral, infosVideo, infosAudio, infosText, infosMultiPart);
        } catch (Exception ex) {
            LOG.warn("Failed reading mediainfo output for {}", movieFilePath);
            LOG.error(SystemTools.getStackTrace(ex));
        }
    }

    private static boolean isMultiPartsScannable(Movie movie, boolean processMultiPart) {
        if (!ENABLE_MULTIPART) {
            return Boolean.FALSE;
        } else if (!processMultiPart) {
            return Boolean.FALSE;
        } else if (movie.isTVShow()) {
            return Boolean.FALSE;
        } else if (movie.getMovieFiles().size() <= 1) {
            return Boolean.FALSE;
        } else if (!OverrideTools.checkOneOverwrite(movie, MEDIAINFO_PLUGIN_ID, OverrideFlag.RUNTIME)) {
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    private void scanMultiParts(String movieFilePath, Map<String, String> infosMultiPart) {
        try (MediaInfoStream stream = createStream(movieFilePath)) {

            Map<String, String> infosGeneral = new HashMap<>();
            List<Map<String, String>> infosVideo = new ArrayList<>();
            List<Map<String, String>> infosAudio = new ArrayList<>();
            List<Map<String, String>> infosText = new ArrayList<>();

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

            // resolve duration
            int duration = getDuration(infosGeneral, infosVideo);
            // add already stored multipart runtime
            duration = duration + getMultiPartDuration(infosMultiPart);
            if (duration > 0) {
                infosMultiPart.put("MultiPart_Duration", String.valueOf(duration));
            }
        } catch (Exception ex) {
            LOG.warn("Failed reading mediainfo output for {}", movieFilePath);
            LOG.error(SystemTools.getStackTrace(ex));
        }
    }

    @SuppressWarnings("resource")
    protected MediaInfoStream createStream(String movieFilePath) throws IOException {
        if (MI_READ_FROM_FILE) {
            // check file
            String filename = FilenameUtils.removeExtension(movieFilePath) + ".mediainfo";
            Collection<File> files = FileTools.fileCache.searchFilename(filename, Boolean.FALSE);
            if (files != null && !files.isEmpty()) {
                // create new input stream for reading
                LOG.debug("Reading from file {}", filename);
                return new MediaInfoStream(new FileInputStream(files.iterator().next()));
            }
        }

        // Create the command line
        List<String> commandMedia = new ArrayList<>(MI_EXE);
        commandMedia.add(movieFilePath);

        ProcessBuilder pb = new ProcessBuilder(commandMedia);
        // set up the working directory.
        pb.directory(MI_PATH);
        return new MediaInfoStream(pb.start());
    }

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

    protected void parseMediaInfo(MediaInfoStream stream, Map<String, String> infosGeneral,
            List<Map<String, String>> infosVideo, List<Map<String, String>> infosAudio,
            List<Map<String, String>> infosText) throws Exception {

        InputStreamReader isr = null;
        @SuppressWarnings("resource")
        BufferedReader bufReader = null;

        try {
            isr = new InputStreamReader(stream.getInputStream());
            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<>();

            // 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])); // Issue 1311 - Create a "link" between General and Gnral
            matches.put(generalKey[2], matches.get(generalKey[0])); // Issue 1311 - Create a "link" between General and * Gnral
            matches.put("Video", infosVideo);
            matches.put("Vido", matches.get("Video")); // Issue 1311 - Create a "link" between Vido and 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) {
                    Map<String, String> currentData = new HashMap<>();
                    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 singleKey : generalKey) {
                    List<Map<String, String>> arrayList = matches.get(singleKey);
                    if (!arrayList.isEmpty()) {
                        Map<String, String> datas = arrayList.get(0);
                        if (!datas.isEmpty()) {
                            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();
            }
        }
    }

    /**
     * Update the movie information from the media info
     *
     * @param movie
     * @param infosGeneral
     * @param infosVideo
     * @param infosAudio
     * @param infosText
     * @param infosMultiPart
     */
    public void updateMovieInfo(Movie movie, Map<String, String> infosGeneral, List<Map<String, String>> infosVideo,
            List<Map<String, String>> infosAudio, List<Map<String, String>> infosText,
            Map<String, String> infosMultiPart) {

        String infoValue;

        // update movie with meta tags if present and required
        if (ENABLE_METADATA) {
            processMetaData(movie, infosGeneral);
        } // enableMetaData

        // get Container from General Section
        if (OverrideTools.checkOverwriteContainer(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Format");
            movie.setContainer(infoValue, MEDIAINFO_PLUGIN_ID);
        }

        if (OverrideTools.checkOverwriteRuntime(movie, MEDIAINFO_PLUGIN_ID)) {
            int duration = getDuration(infosGeneral, infosVideo);
            duration = duration + getMultiPartDuration(infosMultiPart);

            if (duration > 0) {
                //                if (duration > 900000) {
                // 15 minutes in milliseconds
                duration = duration / 1000;
                /*              } else if (duration > 900) {
                 // 15 minutes in seconds
                 // No change required
                 } else {
                 // probably in minutes
                 duration = duration * 60;
                 }*/
                // Duration is returned in minutes, convert it to seconds
                movie.setRuntime(DateTimeTools.formatDuration(duration), MEDIAINFO_PLUGIN_ID);
            }
        }

        // get Info from first Video Stream
        // - can evolve to get info from longest Video Stream
        if (!infosVideo.isEmpty()) {
            // At this point there is only a codec pulled from the filename, so we can clear that now
            movie.getCodecs().clear();

            Map<String, String> infosMainVideo = infosVideo.get(0);

            // Add the video codec to the list
            Codec codecToAdd = getCodecInfo(CodecType.VIDEO, infosMainVideo);
            if (MI_OVERALL_BITRATE && StringTools.isNotValidString(codecToAdd.getCodecBitRate())) {
                infoValue = infosGeneral.get(Codec.MI_CODEC_OVERALL_BITRATE);
                if (StringUtils.isNotBlank(infoValue) && infoValue.length() > 3) {
                    infoValue = infoValue.substring(0, infoValue.length() - 3);
                    codecToAdd.setCodecBitRate(infoValue);
                }
            }
            movie.addCodec(codecToAdd);

            if (OverrideTools.checkOverwriteResolution(movie, MEDIAINFO_PLUGIN_ID)) {
                String width = infosMainVideo.get("Width");
                String height = infosMainVideo.get("Height");
                movie.setResolution(width, height, MEDIAINFO_PLUGIN_ID);
            }

            // Frames per second
            if (OverrideTools.checkOverwriteFPS(movie, MEDIAINFO_PLUGIN_ID)) {
                infoValue = infosMainVideo.get("Frame rate");
                if (infoValue == null) {
                    // use original frame rate
                    infoValue = infosMainVideo.get("Original frame rate");
                }

                if (infoValue != null) {
                    int inxDiv = infoValue.indexOf(Movie.SPACE_SLASH_SPACE);
                    if (inxDiv > -1) {
                        infoValue = infoValue.substring(0, inxDiv);
                    }

                    movie.setFps(NumberUtils.toFloat(infoValue, 0.0F), MEDIAINFO_PLUGIN_ID);
                }
            }

            // Save the aspect ratio for the video
            if (OverrideTools.checkOverwriteAspectRatio(movie, MEDIAINFO_PLUGIN_ID)) {
                infoValue = infosMainVideo.get("Display aspect ratio");
                if (infoValue != null) {
                    movie.setAspectRatio(ASPECT_TOOLS.cleanAspectRatio(infoValue), MEDIAINFO_PLUGIN_ID);
                }
            }

            if (OverrideTools.checkOverwriteVideoOutput(movie, MEDIAINFO_PLUGIN_ID)) {
                // Guessing Video Output (Issue 988)
                if (movie.isHD()) {
                    StringBuilder normeHD = new StringBuilder();
                    if (movie.isHD1080()) {
                        normeHD.append("1080");
                    } else {
                        normeHD.append("720");
                    }

                    infoValue = infosMainVideo.get("Scan type");
                    if (infoValue != null) {
                        if ("Progressive".equals(infoValue)) {
                            normeHD.append("p");
                        } else {
                            normeHD.append("i");
                        }
                    }
                    normeHD.append(" ").append(Math.round(movie.getFps())).append("Hz");
                    movie.setVideoOutput(normeHD.toString(), MEDIAINFO_PLUGIN_ID);
                } else {
                    StringBuilder videoOutput = new StringBuilder();
                    switch (Math.round(movie.getFps())) {
                    case 24:
                        videoOutput.append("24");
                        break;
                    case 25:
                        videoOutput.append("PAL 25");
                        break;
                    case 30:
                        videoOutput.append("NTSC 30");
                        break;
                    case 50:
                        videoOutput.append("PAL 50");
                        break;
                    case 60:
                        videoOutput.append("NTSC 60");
                        break;
                    default:
                        videoOutput.append("NTSC");
                        break;
                    }
                    infoValue = infosMainVideo.get("Scan type");
                    if (infoValue != null) {
                        if ("Progressive".equals(infoValue)) {
                            videoOutput.append("p");
                        } else {
                            videoOutput.append("i");
                        }
                    }
                    movie.setVideoOutput(videoOutput.toString(), MEDIAINFO_PLUGIN_ID);
                }
            }

            if (OverrideTools.checkOverwriteVideoSource(movie, MEDIAINFO_PLUGIN_ID)) {
                infoValue = infosMainVideo.get("MultiView_Count");
                if ("2".equals(infoValue)) {
                    movie.setVideoSource("3D", MEDIAINFO_PLUGIN_ID);
                }
            }
        }

        // Cycle through Audio Streams
        // boolean previousAudioCodec = !movie.getAudioCodec().equals(Movie.UNKNOWN); // Do we have AudioCodec already?
        // boolean previousAudioChannels = !movie.getAudioChannels().equals(Movie.UNKNOWN); // Do we have AudioChannels already?
        Set<String> foundLanguages = new HashSet<>();

        for (Map<String, String> infosCurAudio : infosAudio) {
            infoValue = infosCurAudio.get("Language");
            if (infoValue != null) {
                // Issue 1227 - Make some clean up in mediainfo datas.
                if (infoValue.contains("/")) {
                    infoValue = infoValue.substring(0, infoValue.indexOf('/')).trim(); // In this case, language are "doubled", just take the first one.
                }
                // Add determination of language.
                String determineLanguage = MovieFilenameScanner.determineLanguage(infoValue);
                foundLanguages.add(determineLanguage);
            }

            // Add the audio codec to the list
            Codec codecToAdd = getCodecInfo(CodecType.AUDIO, infosCurAudio);
            movie.addCodec(codecToAdd);
        }

        if (OverrideTools.checkOverwriteLanguage(movie, MEDIAINFO_PLUGIN_ID)) {
            StringBuilder movieLanguage = new StringBuilder();
            for (String language : foundLanguages) {
                if (movieLanguage.length() > 0) {
                    movieLanguage.append(LANG_DELIM);
                }
                movieLanguage.append(language);
            }

            if (StringTools.isValidString(movieLanguage.toString())) {
                movie.setLanguage(movieLanguage.toString(), MEDIAINFO_PLUGIN_ID);
            } else if (StringTools.isValidString(AUDIO_LANG_UNKNOWN)) {
                String determineLanguage = MovieFilenameScanner.determineLanguage(AUDIO_LANG_UNKNOWN);
                movie.setLanguage(determineLanguage, MEDIAINFO_PLUGIN_ID);
            }
        }

        // Cycle through Text Streams
        for (Map<String, String> infosCurText : infosText) {
            String infoLanguage = "";
            infoValue = infosCurText.get("Language");

            // Issue 1450 - If we are here, we have subtitles, but didn't have the language, setting an value of "UNDEFINED" to make it appear
            if (StringTools.isNotValidString(infoValue)) {
                infoValue = "UNDEFINED";
            }

            if (StringTools.isValidString(infoValue)) {
                // Issue 1227 - Make some clean up in mediainfo datas.
                if (infoValue.contains("/")) {
                    infoValue = infoValue.substring(0, infoValue.indexOf('/')).trim(); // In this case, languages are "doubled", just take the first one.
                }
                infoLanguage = MovieFilenameScanner.determineLanguage(infoValue);
            }

            String infoFormat = "";
            infoValue = infosCurText.get("Format");

            if (StringTools.isValidString(infoValue)) {
                infoFormat = infoValue;
            } else {
                // Issue 1450 - If we are here, we have subtitles, but didn't have the language
                // Take care of label for "Format" in mediaInfo 0.6.1.1
                infoValue = infosCurText.get("Codec");
                if (StringTools.isValidString(infoValue)) {
                    infoFormat = infoValue;
                }
            }

            // Make sure we have a codec & language before continuing
            if (StringTools.isValidString(infoFormat) && StringTools.isValidString(infoLanguage)) {
                if ("SRT".equalsIgnoreCase(infoFormat) || "UTF-8".equalsIgnoreCase(infoFormat)
                        || "RLE".equalsIgnoreCase(infoFormat) || "PGS".equalsIgnoreCase(infoFormat)
                        || "ASS".equalsIgnoreCase(infoFormat) || "VobSub".equalsIgnoreCase(infoFormat)) {
                    SubtitleTools.addMovieSubtitle(movie, infoLanguage);
                } else {
                    LOG.debug("Subtitle format skipped: {}", infoFormat);
                }
            }
        }
    }

    /**
     * Process the meta data from the media info results
     *
     * @param movie
     * @param infosGeneral
     */
    private static void processMetaData(Movie movie, Map<String, String> infosGeneral) {

        String infoValue;

        if (OverrideTools.checkOverwriteTitle(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Movie");
            if (infoValue == null) {
                infoValue = infosGeneral.get("Movie name");
            }
            movie.setTitle(infoValue, MEDIAINFO_PLUGIN_ID);
        }

        if (OverrideTools.checkOverwriteDirectors(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Director");
            movie.setDirector(infoValue, MEDIAINFO_PLUGIN_ID);
        }

        if (OverrideTools.checkOverwritePlot(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Summary");
            if (infoValue == null) {
                infoValue = infosGeneral.get("Comment");
            }
            movie.setPlot(infoValue, MEDIAINFO_PLUGIN_ID);
        }

        if (OverrideTools.checkOverwriteGenres(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Genre");
            if (infoValue != null) {
                List<String> newGenres = StringTools.splitList(infoValue, SPLIT_GENRE);
                movie.setGenres(newGenres, MEDIAINFO_PLUGIN_ID);
            }
        }

        if (OverrideTools.checkOverwriteActors(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Actor");
            if (infoValue == null) {
                infoValue = infosGeneral.get("Performer");
            }
            if (infoValue != null) {
                List<String> list = StringTools.splitList(infoValue, SPLIT_GENRE);
                movie.setCast(list, MEDIAINFO_PLUGIN_ID);
            }
        }

        if (OverrideTools.checkOverwriteCertification(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("LawRating");
            if (infoValue == null) {
                infoValue = infosGeneral.get("Law rating");
            }
            movie.setCertification(infoValue, MEDIAINFO_PLUGIN_ID);
        }

        infoValue = infosGeneral.get("Rating");
        if (infoValue != null) {
            try {
                float r = Float.parseFloat(infoValue);
                r = r * 20.0f;
                movie.addRating(MEDIAINFO_PLUGIN_ID, Math.round(r));
            } catch (NumberFormatException ignore) {
                // nothing to do
            }
        }

        if (OverrideTools.checkOverwriteCountry(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Country");
            if (infoValue == null) {
                infoValue = infosGeneral.get("Movie/Country");
            }
            if (infoValue == null) {
                infoValue = infosGeneral.get("Movie name/Country");
            }
            movie.setCountries(infoValue, MEDIAINFO_PLUGIN_ID);
        }

        if (OverrideTools.checkOverwriteReleaseDate(movie, MEDIAINFO_PLUGIN_ID)) {
            infoValue = infosGeneral.get("Released_Date");
            movie.setReleaseDate(infoValue, MEDIAINFO_PLUGIN_ID);
        }
    }

    private static int getDuration(Map<String, String> infosGeneral, List<Map<String, String>> infosVideo) {
        String runtimeValue;
        int runtime;

        runtimeValue = infosGeneral.get("PlayTime");
        runtime = DateTimeTools.processRuntime(runtimeValue);

        if (runtime <= 0 && (!infosVideo.isEmpty())) {
            // Get the main video information
            Map<String, String> infosMainVideo = infosVideo.get(0);
            runtimeValue = infosMainVideo.get("Duration");
            runtime = DateTimeTools.processRuntime(runtimeValue);
        }

        if (runtime <= 0) {
            runtimeValue = infosGeneral.get("Duration");
            runtime = DateTimeTools.processRuntime(runtimeValue);
        }

        LOG.trace("Found runtime: '{}'", runtime);
        return runtime;
    }

    private static int getMultiPartDuration(Map<String, String> infosMultiPart) {
        if (infosMultiPart.isEmpty()) {
            return 0;
        }

        String runtimeValue = infosMultiPart.get("MultiPart_Duration");
        return Math.max(0, DateTimeTools.processRuntime(runtimeValue));
    }

    /**
     * Create a Codec object with the information from the file
     *
     * @param codecType
     * @param codecInfos
     * @return
     */
    protected Codec getCodecInfo(CodecType codecType, Map<String, String> codecInfos) {
        Codec codec = new Codec(codecType);
        codec.setCodecSource(CodecSource.MEDIAINFO);

        codec.setCodec(codecInfos.get(Codec.MI_CODEC));
        codec.setCodecFormat(codecInfos.get(Codec.MI_CODEC_FORMAT));
        codec.setCodecFormatProfile(codecInfos.get(Codec.MI_CODEC_FORMAT_PROFILE));
        codec.setCodecFormatVersion(codecInfos.get(Codec.MI_CODEC_FORMAT_VERSION));
        codec.setCodecId(codecInfos.get(Codec.MI_CODEC_ID));
        codec.setCodecIdHint(codecInfos.get(Codec.MI_CODEC_ID_HINT));
        codec.setCodecLanguage(codecInfos.get(Codec.MI_CODEC_LANGUAGE));

        String[] keyBitrates = { Codec.MI_CODEC_BITRATE, Codec.MI_CODEC_NOMINAL_BITRATE };
        for (String key : Arrays.asList(keyBitrates)) {
            String infoValue = codecInfos.get(key);
            if (StringUtils.isNotBlank(infoValue)) {
                if (infoValue.contains(Movie.SPACE_SLASH_SPACE)) {
                    infoValue = infoValue.substring(0, infoValue.indexOf(Movie.SPACE_SLASH_SPACE));
                }

                // Check to see if we have a valid value
                if (StringTools.isValidString(infoValue.trim())) {
                    infoValue = infoValue.substring(0, infoValue.length() - 3);
                    codec.setCodecBitRate(infoValue);
                    break;
                }
            }
            if (codecType.equals(CodecType.AUDIO) && key.equals(Codec.MI_CODEC_BITRATE)) {
                break;
            }
        }

        // Cycle through the codec channel labels
        String codecChannels = "";
        for (String cc : Codec.MI_CODEC_CHANNELS) {
            codecChannels = codecInfos.get(cc);
            if (StringUtils.isNotBlank(codecChannels)) {
                // We have our channels
                break;
            }
        }

        if (StringUtils.isNotBlank(codecChannels)) {
            if (codecChannels.contains("/")) {
                codecChannels = codecChannels.substring(0, codecChannels.indexOf('/'));
            }
            Matcher codecMatch = PATTERN_CHANNELS.matcher(codecChannels);
            if (codecMatch.matches()) {
                codec.setCodecChannels(Integer.parseInt(codecMatch.group(1)));
            }
        }
        return codec;
    }

    public String archiveScan(String movieFilePath) {
        if (!IS_ACTIVATED) {
            return null;
        }

        LOG.debug("Mini-scan on {}", movieFilePath);

        try {
            // Create the command line
            List<String> commandMedia = new ArrayList<>(MI_EXE);
            // Technically, mediaInfoExe has "-f" in it from above, but "-s" will override it anyway.
            // "-s" will dump just "$size $path" inside RAR/ISO.
            commandMedia.add("-s");
            commandMedia.add(movieFilePath);

            ProcessBuilder pb = new ProcessBuilder(commandMedia);

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

            Process p = pb.start();

            String mediaArchive;
            try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
                String line;
                mediaArchive = null;
                while ((line = localInputReadLine(input)) != null) {
                    Pattern patternArchive = Pattern.compile("^\\s*\\d+\\s(.*)$");
                    Matcher m = patternArchive.matcher(line);
                    if (m.find() && (m.groupCount() == 1)) {
                        mediaArchive = m.group(1);
                    }
                }
            }

            LOG.debug("Returning with archivename {}", mediaArchive);

            return mediaArchive;

        } catch (IOException error) {
            LOG.error(SystemTools.getStackTrace(error));
        }

        return null;
    }

    /**
     * Look for the mediaInfo filename and return it.
     *
     * Will check first for the mediainfo-rar file and then mediainfo
     *
     * @return
     */
    private static File findMediaInfo() {
        File mediaInfoFile;

        if (OS_NAME.contains("Windows")) {
            mediaInfoFile = FileUtils.getFile(MI_PATH.getAbsolutePath(), MI_RAR_FILENAME_WINDOWS);
            if (!mediaInfoFile.exists()) {
                // Fall back to the normal filename
                mediaInfoFile = FileUtils.getFile(MI_PATH.getAbsolutePath(), MI_FILENAME_WINDOWS);
            } else {
                // Enable the extra mediainfo-rar features
                isMediaInfoRar = Boolean.TRUE;
            }
        } else {
            mediaInfoFile = FileUtils.getFile(MI_PATH.getAbsolutePath(), MI_RAR_FILENAME_LINUX);
            if (!mediaInfoFile.exists()) {
                // Fall back to the normal filename
                mediaInfoFile = FileUtils.getFile(MI_PATH.getAbsolutePath(), MI_FILENAME_LINUX);
            } else {
                // Enable the extra mediainfo-rar features
                isMediaInfoRar = Boolean.TRUE;
            }
        }

        return mediaInfoFile;
    }
}