org.tinymediamanager.core.entities.MediaFile.java Source code

Java tutorial

Introduction

Here is the source code for org.tinymediamanager.core.entities.MediaFile.java

Source

/*
 * Copyright 2012 - 2016 Manuel Laggner
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.tinymediamanager.core.entities;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinymediamanager.Globals;
import org.tinymediamanager.core.AbstractModelObject;
import org.tinymediamanager.core.MediaFileType;
import org.tinymediamanager.core.Utils;
import org.tinymediamanager.scraper.util.LanguageUtils;
import org.tinymediamanager.scraper.util.StrgUtils;
import org.tinymediamanager.thirdparty.MediaInfo;
import org.tinymediamanager.thirdparty.MediaInfo.StreamKind;
import org.tinymediamanager.thirdparty.MediaInfoXMLParser;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.stephenc.javaisotools.loopfs.iso9660.Iso9660FileEntry;
import com.github.stephenc.javaisotools.loopfs.iso9660.Iso9660FileSystem;

/**
 * The Class MediaFile.
 * 
 * @author Manuel Laggner
 */
public class MediaFile extends AbstractModelObject implements Comparable<MediaFile> {
    private static final Logger LOGGER = LoggerFactory.getLogger(MediaFile.class);

    private static final String PATH = "path";
    private static final String FILENAME = "filename";
    private static final String FILESIZE = "filesize";
    private static final String FILESIZE_IN_MB = "filesizeInMegabytes";
    private static final List<String> PLEX_EXTRA_FOLDERS = Arrays.asList("behind the scenes", "behindthescenes",
            "deleted scenes", "deletedscenes", "featurettes", "interviews", "scenes", "shorts", "trailers");

    private static Pattern moviesetPattern = Pattern.compile("(?i)(movieset-poster|movieset-fanart)\\..{2,4}");
    private static Pattern posterPattern = Pattern
            .compile("(?i)(.*-poster|poster|folder|movie|.*-cover|cover)\\..{2,4}");
    private static Pattern fanartPattern = Pattern.compile("(?i)(.*-fanart|.*\\.fanart|fanart)[0-9]{0,2}\\..{2,4}");
    private static Pattern bannerPattern = Pattern.compile("(?i)(.*-banner|banner)\\..{2,4}");
    private static Pattern thumbPattern = Pattern.compile("(?i)(.*-thumb|thumb)[0-9]{0,2}\\..{2,4}");
    private static Pattern seasonPattern = Pattern.compile("(?i)season([0-9]{0,2}|-specials)-poster\\..{2,4}");
    private static Pattern logoPattern = Pattern.compile("(?i)(.*-logo|logo)\\..{2,4}");
    private static Pattern clearlogoPattern = Pattern.compile("(?i)(.*-clearlogo|clearlogo)\\..{2,4}");
    // be careful: disc.avi would be valid!
    private static Pattern discartPattern = Pattern
            .compile("(?i)(.*-discart|discart|.*-disc|disc)\\.(jpg|jpeg|png|tbn)");
    private static Pattern clearartPattern = Pattern.compile("(?i)(.*-clearart|clearart)\\..{2,4}");

    public static final String VIDEO_FORMAT_480P = "480p";
    public static final String VIDEO_FORMAT_576P = "576p";
    public static final String VIDEO_FORMAT_540P = "540p";
    public static final String VIDEO_FORMAT_720P = "720p";
    public static final String VIDEO_FORMAT_1080P = "1080p";
    public static final String VIDEO_FORMAT_4K = "4k";
    public static final String VIDEO_FORMAT_8K = "8k";

    // meta formats
    public static final String VIDEO_FORMAT_SD = "SD";
    public static final String VIDEO_FORMAT_HD = "HD";

    // 3D / side-by-side / top-and-bottom / H=half - http://wiki.xbmc.org/index.php?title=3D#Video_filenames_flags
    public static final String VIDEO_3D = "3D";
    public static final String VIDEO_3D_SBS = "3D SBS";
    public static final String VIDEO_3D_TAB = "3D TAB";
    public static final String VIDEO_3D_HSBS = "3D HSBS";
    public static final String VIDEO_3D_HTAB = "3D HTAB";

    @JsonProperty
    private MediaFileType type = MediaFileType.UNKNOWN;
    @JsonProperty
    private String path = "";
    @JsonProperty
    private String filename = "";
    @JsonProperty
    private long filesize = 0;
    @JsonProperty
    private long filedate = 0;
    @JsonProperty
    private String videoCodec = "";
    @JsonProperty
    private String containerFormat = "";
    @JsonProperty
    private String exactVideoFormat = "";
    @JsonProperty
    private String video3DFormat = "";
    @JsonProperty
    private int videoWidth = 0;
    @JsonProperty
    private int videoHeight = 0;
    @JsonProperty
    private int overallBitRate = 0;
    @JsonProperty
    private int durationInSecs = 0;
    @JsonProperty
    private int stacking = 0;
    @JsonProperty
    private String stackingMarker = "";

    @JsonProperty
    private List<MediaFileAudioStream> audioStreams = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<MediaFileSubtitle> subtitles = new CopyOnWriteArrayList<>();

    private MediaInfo mediaInfo;
    private Map<StreamKind, List<Map<String, String>>> miSnapshot = null;
    private Path file = null;
    private boolean isISO = false;

    /**
     * "clones" a new media file.
     */
    public MediaFile(MediaFile clone) {
        this.path = new String(clone.path);
        this.filename = new String(clone.filename);
        this.filesize = clone.filesize;
        this.filedate = clone.filedate;
        this.videoCodec = new String(clone.videoCodec);
        this.containerFormat = new String(clone.containerFormat);
        this.exactVideoFormat = new String(clone.exactVideoFormat);
        this.video3DFormat = new String(clone.video3DFormat);
        this.videoHeight = clone.videoHeight;
        this.videoWidth = clone.videoWidth;
        this.overallBitRate = clone.overallBitRate;
        this.durationInSecs = clone.durationInSecs;
        this.stacking = clone.stacking;
        this.stackingMarker = clone.stackingMarker;
        this.type = clone.type;
        this.audioStreams.addAll(clone.audioStreams);
        this.subtitles.addAll(clone.subtitles);
    }

    /**
     * Instantiates a new media file.
     */
    public MediaFile() {
        this.path = "";
        this.filename = "";
    }

    /**
     * Instantiates a new media file.
     * 
     * @deprecated use Java 7 Path() instead of File()
     * @param f
     *          the file
     */
    @Deprecated
    public MediaFile(File f) {
        this(f.toPath(), null);
    }

    /**
     * Instantiates a new media file.
     * 
     * @param f
     *          the file
     */
    public MediaFile(Path f) {
        this(f, null);
    }

    /**
     * Instantiates a new media file.
     * 
     * @deprecated use Java 7 Path() instead of File()
     * @param f
     *          the file
     * @param type
     *          the MediaFileType
     */
    @Deprecated
    public MediaFile(File f, MediaFileType type) {
        this(f.toPath(), type);
    }

    /**
     * Instantiates a new media file.
     * 
     * @param f
     *          the file
     * @param type
     *          the MediaFileType
     */
    public MediaFile(Path f, MediaFileType type) {
        this.path = f.getParent().toString(); // just path w/o filename
        this.filename = f.getFileName().toString();
        this.file = f.toAbsolutePath();
        if (type == null) {
            this.type = parseType();
        } else {
            this.type = type;
        }

        // set containerformat for non MI files
        if (!isValidMediainfoFormat() && StringUtils.isBlank(getContainerFormat())) {
            setContainerFormat(getExtension());
        }
    }

    private void gatherSubtitleInformation() {
        MediaFileSubtitle sub = new MediaFileSubtitle();
        String shortname = getBasename().toLowerCase(Locale.ROOT);
        if (shortname.contains("forced")) {
            sub.setForced(true);
            shortname = shortname.replaceAll("\\p{Punct}*forced", "");
        }
        sub.setLanguage(parseLanguageFromString(shortname));

        if (sub.getLanguage().isEmpty() && this.filename.endsWith(".sub")) {
            // not found in name, try to parse from idx
            BufferedReader br;
            try {
                File idx = new File(this.path, this.filename.replaceFirst("sub$", "idx"));
                br = new BufferedReader(new FileReader(idx));
                String line;
                while ((line = br.readLine()) != null) {
                    String lang = "";
                    // System.out.println("line: " + line);
                    if (line.startsWith("id:")) {
                        lang = StrgUtils.substr(line, "id: (.*?),");
                    }
                    if (line.startsWith("# alt:")) {
                        lang = StrgUtils.substr(line, "^# alt: (.*?)$");
                    }
                    if (!lang.isEmpty()) {
                        sub.setLanguage(LanguageUtils.getIso3LanguageFromLocalizedString(lang));
                        break;
                    }
                }
                br.close();
            } catch (IOException e) {
                // ignore
            }
        }
        sub.setCodec(getExtension());
        subtitles.clear();
        subtitles.add(sub);
    }

    /**
     * tries to get the MediaFileType out of filename.
     * 
     * @return the MediaFileType
     */
    public MediaFileType parseType() {
        String ext = getExtension().toLowerCase(Locale.ROOT);
        String basename = FilenameUtils.getBaseName(getFilename());
        String foldername = FilenameUtils.getBaseName(getPath()).toLowerCase(Locale.ROOT);

        if (ext.equals("nfo")) {
            return MediaFileType.NFO;
        }

        if (ext.equals("jpg") || ext.equals("jpeg") || ext.equals("png") || ext.equals("tbn")) {
            return parseImageType();
        }

        if (Globals.settings.getAudioFileType().contains("." + ext)) {
            return MediaFileType.AUDIO;
        }

        if (Globals.settings.getSubtitleFileType().contains("." + ext)) {
            return MediaFileType.SUBTITLE;
        }

        if (Globals.settings.getVideoFileType().contains("." + ext)) {
            // has to fit TV & Movie naming...
            // String cleanName = ParserUtils.detectCleanMoviename(name); // tbc if useful...
            // old impl: https://github.com/brentosmith/xbmc-dvdextras
            // Official: http://wiki.xbmc.org/index.php?title=Add-on:VideoExtras#File_Naming_Convention
            if (getFilename().contains(".EXTRAS.") // scene file naming (need to check first! upper case!)
                    || basename.matches("(?i).*[_.-]+extra[s]?$") // end with "-extra[s]"
                    || basename.matches("(?i).*[-]+extra[s]?[-].*") // extra[s] just with surrounding dash (other delims problem)
                    || foldername.equalsIgnoreCase("extras") // preferred folder name
                    || foldername.equalsIgnoreCase("extra") // preferred folder name
                    || basename.matches("(?i).*[-](behindthescenes|deleted|featurette|interview|scene|short)$") // Plex (w/o trailer)
                    || PLEX_EXTRA_FOLDERS.contains(foldername)) // Plex Extra folders
            {
                return MediaFileType.VIDEO_EXTRA;
            }

            if (basename.matches("(?i).*[_.-]*trailer?$") || foldername.equalsIgnoreCase("trailer")) {
                return MediaFileType.TRAILER;
            }

            // we have some false positives too - make a more precise check
            if (basename.matches("(?i).*[_.-]*sample$") // end with sample
                    || foldername.equalsIgnoreCase("sample")) { // sample folder name
                return MediaFileType.SAMPLE;
            }

            return MediaFileType.VIDEO;
        }

        if (ext.equals("txt")) {
            return MediaFileType.TEXT;
        }

        return MediaFileType.UNKNOWN;
    }

    /**
     * Parses the image type.
     * 
     * @return the media file type
     */
    private MediaFileType parseImageType() {
        String name = getFilename();

        // movieset artwork
        Matcher matcher = moviesetPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.GRAPHIC;
        }

        // *season(XX|-specials)-poster.*
        matcher = seasonPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.SEASON_POSTER;
        }

        // *-poster.* or poster.* or folder.* or movie.*
        matcher = posterPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.POSTER;
        }

        // *-fanart.* or fanart.* or *-fanartXX.* or fanartXX.*
        matcher = fanartPattern.matcher(name);
        if (matcher.matches()) {
            // decide between fanart and extrafanart
            if (getPath().endsWith("extrafanart")) {
                return MediaFileType.EXTRAFANART;
            }
            return MediaFileType.FANART;
        }

        // *-banner.* or banner.*
        matcher = bannerPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.BANNER;
        }

        // *-thumb.* or thumb.* or *-thumbXX.* or thumbXX.*
        matcher = thumbPattern.matcher(name);
        if (matcher.matches()) {
            // decide between thumb and extrathumb
            if (getPath().endsWith("extrathumbs")) {
                return MediaFileType.EXTRATHUMB;
            }
            return MediaFileType.THUMB;
        }

        // clearart.*
        matcher = clearartPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.CLEARART;
        }

        // logo.*
        matcher = logoPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.LOGO;
        }

        // clearlogo.*
        matcher = clearlogoPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.CLEARLOGO;
        }

        // discart.*
        matcher = discartPattern.matcher(name);
        if (matcher.matches()) {
            return MediaFileType.DISCART;
        }

        return MediaFileType.GRAPHIC;
    }

    /**
     * is this a "packed" file? (zip, rar, whatsoever).
     * 
     * @return true/false
     */
    public boolean isPacked() {
        String ext = getExtension().toLowerCase(Locale.ROOT);
        return (ext.equals("zip") || ext.equals("rar") || ext.equals("7z") || ext.matches("r\\d+"));
    }

    /**
     * Is this a graphic file?.
     * 
     * @return true/false
     */
    public boolean isGraphic() {
        switch (type) {
        case GRAPHIC:
        case BANNER:
        case FANART:
        case POSTER:
        case THUMB:
        case LOGO:
        case CLEARLOGO:
        case CLEARART:
        case SEASON_POSTER:
        case EXTRAFANART:
        case EXTRATHUMB:
        case DISCART:
            return true;

        default:
            return false;
        }
    }

    /**
     * Is this a playable video file?.
     * 
     * @return true/false
     */
    public boolean isVideo() {
        return (type.equals(MediaFileType.VIDEO) || type.equals(MediaFileType.VIDEO_EXTRA)
                || type.equals(MediaFileType.TRAILER) || type.equals(MediaFileType.SAMPLE));
    }

    /**
     * is this a "disc file"? (video_ts, vts, bdmv, ...) for movierenamer
     * 
     * @return true/false
     */
    public boolean isDiscFile() {
        String name = getFilename().toLowerCase(Locale.ROOT);
        return (name.matches("(video_ts|vts_\\d\\d_\\d)\\.(vob|bup|ifo)") || // dvd
                name.matches("(index\\.bdmv|movieobject\\.bdmv|\\d{5}\\.m2ts)")); // bluray
    }

    @Deprecated
    public File getFile() {
        if (file == null) {
            file = getFileAsPath();
        }
        return file.toFile();
    }

    public Path getFileAsPath() {
        if (file == null) {
            Path f = Paths.get(this.path, this.filename);
            file = f.toAbsolutePath();
        }
        return file;
    }

    public void setFile(Path file) {
        setFilename(file.getFileName().toString());
        setPath(file.toAbsolutePath().getParent().toString());
        this.file = file.toAbsolutePath();
    }

    /**
     * if name/path changes, invalidate the file handle (if not null).
     */
    private void invalidateFileHandle() {
        if (file != null) {
            file = null;
        }
    }

    /**
     * Gets the path.
     * 
     * @return the path
     */
    public String getPath() {
        return path;
    }

    /**
     * Sets the path.
     * 
     * @param newValue
     *          the new path
     */
    public void setPath(String newValue) {
        String oldValue = this.path;
        this.path = newValue;
        invalidateFileHandle();
        firePropertyChange(PATH, oldValue, newValue);
    }

    /**
     * (re)sets the path (when renaming MediaEntity folder).<br>
     * Exchanges the beginning MF path from oldPath with newPath<br>
     * <br>
     * eg: <br>
     * Params: /movie/alien1/ & /movie/Alien 1/<br>
     * File: /movie/alien1/asdf/jklo/file.avi -> /movie/Alien 1/asdf/jklo/file.avi<br>
     * <br>
     * this id done by a simple string.replace()
     * 
     * @param oldPath
     *          the old path
     * @param newPath
     *          the new path
     */
    public void replacePathForRenamedFolder(Path oldPath, Path newPath) {
        String p = getPath();
        p = p.replace(oldPath.toAbsolutePath().toString(), newPath.toAbsolutePath().toString());
        setPath(p);
    }

    public String getFilename() {
        return filename;
    }

    /**
     * it might be, that there "seems" to be a stacking marker in filename,<br>
     * but the file is not stacked itself. Just return correct string.
     * 
     * @return the file name without the stacking info
     */
    public String getFilenameWithoutStacking() {
        if (stackingMarker.isEmpty()) {
            // no stacking, remove all occurrences
            // fname = Utils.cleanStackingMarkers(filename);
            // NOO, keep em!!! "mockingjay part1" might be unstacked, so keep the part1 !!!
            return filename;
        } else {
            // stacking, so just remove known marker
            return filename.replaceAll("[ _.-]*" + stackingMarker, ""); // optional delimiter
        }
    }

    public void setFilename(String newValue) {
        String oldValue = this.filename;
        this.filename = newValue;
        invalidateFileHandle();
        firePropertyChange(FILENAME, oldValue, newValue);
    }

    public String getExtension() {
        return FilenameUtils.getExtension(filename);
    }

    /**
     * Gets the "basename" (filename without extension).
     * 
     * @return the basename
     */
    public String getBasename() {
        return FilenameUtils.getBaseName(filename);
    }

    public long getFiledate() {
        return filedate;
    }

    public long getFilesize() {
        return filesize;
    }

    public void setFilesize(long newValue) {
        long oldValue = this.filesize;
        this.filesize = newValue;
        firePropertyChange(FILESIZE, oldValue, newValue);
        firePropertyChange(FILESIZE_IN_MB, oldValue, newValue);
    }

    public String getFilesizeInMegabytes() {
        DecimalFormat df = new DecimalFormat("#0.00");
        return df.format(filesize / (1024.0 * 1024.0)) + " M";
    }

    public MediaFileType getType() {
        return type;
    }

    public void setType(MediaFileType type) {
        this.type = type;
    }

    public int getStacking() {
        return stacking;
    }

    public void setStacking(int stacking) {
        this.stacking = stacking;
    }

    public String getStackingMarker() {
        return stackingMarker;
    }

    public void setStackingMarker(String stackingMarker) {
        this.stackingMarker = stackingMarker;
    }

    /**
     * this might be needed in case of "Harry Potter 7 - Part 1" - this is no stacking!
     */
    public void removeStackingInformation() {
        setStacking(0);
        setStackingMarker("");
    }

    /**
     * detect stacking information for this media file
     */
    public void detectStackingInformation() {
        this.stacking = Utils.getStackingNumber(this.filename);
        if (this.stacking == 0) {
            // try to parse from parent directory
            this.stacking = Utils.getStackingNumber(FilenameUtils.getBaseName(getPath()));
        }
        this.stackingMarker = Utils.getStackingMarker(this.filename);
        if (this.stackingMarker.isEmpty()) {
            // try to parse from parent directory
            this.stackingMarker = Utils.getFolderStackingMarker(FilenameUtils.getBaseName(getPath()));
        }
    }

    public List<MediaFileSubtitle> getSubtitles() {
        return subtitles;
    }

    public String getSubtitlesAsString() {
        StringBuilder sb = new StringBuilder();
        Set<MediaFileSubtitle> cleansub = new LinkedHashSet<>(subtitles);

        for (MediaFileSubtitle sub : cleansub) {
            if (sb.length() > 0) {
                sb.append(", ");
            }
            sb.append(sub.getLanguage());
        }

        return sb.toString();
    }

    public void setSubtitles(List<MediaFileSubtitle> subtitles) {
        this.subtitles = subtitles;
    }

    public void addSubtitle(MediaFileSubtitle subtitle) {
        if (!this.subtitles.contains(subtitle)) {
            this.subtitles.add(subtitle);
        }
    }

    /**
     * clears all subtitles
     */
    public void clearAllSubtitles() {
        this.subtitles.clear();
    }

    public boolean hasSubtitles() {
        return (subtitles != null && subtitles.size() > 0);
    }

    /**
     * <p>
     * Uses <code>ReflectionToStringBuilder</code> to generate a <code>toString</code> for the specified object.
     * </p>
     * 
     * @return the String result
     * @see ReflectionToStringBuilder#toString(Object)
     */
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }

    /**
     * instantiates and gets new mediainfo object.
     */
    private void getMediaInfoSnapshot() {
        if (mediaInfo == null) {
            mediaInfo = new MediaInfo();
        }

        if (miSnapshot == null) {
            try {
                if (!mediaInfo.open(this.getFileAsPath())) {
                    LOGGER.error("Mediainfo could not open file: " + getFileAsPath());

                    // clear references
                    closeMediaInfo();
                } else {
                    miSnapshot = mediaInfo.snapshot();
                }
            }
            // sometimes also an error is thrown
            catch (Exception | Error e) {
                LOGGER.error("Mediainfo could not open file: " + getFileAsPath() + "; " + e.getMessage());

                // clear references
                closeMediaInfo();
            }
        }
    }

    /**
     * Closes the connection to the mediainfo lib.
     */
    private void closeMediaInfo() {
        if (mediaInfo != null) {
            mediaInfo.close();
            mediaInfo = null;
        }
        miSnapshot = null;
    }

    /**
     * Gets the real mediainfo values.
     * 
     * @param streamKind
     *          MediaInfo.StreamKind.(General|Video|Audio|Text|Chapters|Image|Menu )
     * @param streamNumber
     *          the stream number (0 for first)
     * @param keys
     *          the information you want to fetch
     * @return the media information you asked<br>
     *         <b>OR AN EMPTY STRING IF MEDIAINFO COULD NOT BE LOADED</b> (never NULL)
     */
    private String getMediaInfo(StreamKind streamKind, int streamNumber, String... keys) {
        if (miSnapshot == null) {
            getMediaInfoSnapshot(); // load snapshot
        }
        if (miSnapshot != null) {
            for (String key : keys) {
                List<Map<String, String>> stream = miSnapshot.get(streamKind);
                if (stream != null) {
                    LinkedHashMap<String, String> info = (LinkedHashMap<String, String>) stream.get(streamNumber);
                    if (info != null) {
                        String value = info.get(key);
                        // System.out.println(" " + streamKind + " " + key + " = " + value);
                        if (value != null && value.length() > 0) {
                            return value;
                        }
                    }
                }
            }
        }

        return "";
    }

    /**
     * Checks if is empty value.
     * 
     * @param object
     *          the object
     * @return true, if is empty value
     */
    public static boolean isEmptyValue(Object object) {
        return object == null || object.toString().length() == 0;
    }

    /**
     * gets the first token of video codec<br>
     * (e.g. DivX 5 => DivX)
     * 
     * @return the video codec
     */
    public String getVideoCodec() {
        return videoCodec;
    }

    /**
     * Sets the video codec.
     * 
     * @param newValue
     *          the new video codec
     */
    public void setVideoCodec(String newValue) {
        // AVC = h264 = x264; display as h264
        if ("avc".equalsIgnoreCase(newValue) || "x264".equalsIgnoreCase(newValue)) {
            newValue = "h264";
        }

        String oldValue = this.videoCodec;
        this.videoCodec = newValue;
        firePropertyChange("videoCodec", oldValue, newValue);
    }

    /**
     * gets the audio codec<br>
     * (w/o punctuation; eg AC-3 => AC3).
     * 
     * @return the audio codec
     */
    public String getAudioCodec() {
        String codec = "";

        // get audio stream with highest channel count
        MediaFileAudioStream highestStream = getBestAudioStream();
        if (highestStream != null) {
            codec = highestStream.getCodec();
        }

        return codec;
    }

    private MediaFileAudioStream getBestAudioStream() {
        MediaFileAudioStream highestStream = null;
        for (MediaFileAudioStream stream : audioStreams) {
            if (highestStream == null) {
                highestStream = stream;
            } else if (highestStream.getChannelsAsInt() < stream.getChannelsAsInt()) {
                highestStream = stream;
            }
        }
        return highestStream;
    }

    public String getAudioLanguage() {
        String language = "";

        // get audio stream with highest channel count
        MediaFileAudioStream highestStream = getBestAudioStream();
        if (highestStream != null) {
            language = highestStream.getLanguage();
        }

        return language;
    }

    /**
     * returns the container format extensions (e.g. avi, mkv mka mks, OGG, etc.)
     * 
     * @return the container format
     */
    public String getContainerFormat() {
        return this.containerFormat;
    }

    /**
     * sets the container format (e.g. avi, mkv mka mks, OGG, etc.)<br>
     * <b>DOES A DIRECT CALL TO MEDIAINFO</b>
     */
    public void setContainerFormatDirect() {
        String extensions = getMediaInfo(StreamKind.General, 0, "Codec/Extensions", "Format");
        setContainerFormat(
                StringUtils.isEmpty(extensions) ? "" : new Scanner(extensions).next().toLowerCase(Locale.ROOT));
    }

    /**
     * Sets the container format.
     * 
     * @param newValue
     *          the new container format
     */
    public void setContainerFormat(String newValue) {
        String oldValue = this.containerFormat;
        this.containerFormat = newValue;
        firePropertyChange("containerFormat", oldValue, newValue);
    }

    /**
     * gets the "common" video format.
     * 
     * @return 1080p 720p 480p... or SD if too small
     */
    public String getVideoFormat() {
        int w = getVideoWidth();
        int h = getVideoHeight();

        // use XBMC implementation https://github.com/xbmc/xbmc/blob/master/xbmc/utils/StreamDetails.cpp#L514
        if (w == 0 || h == 0) {
            return "";
        } else if (w <= 720 && h <= 480) {
            return VIDEO_FORMAT_480P;
        }
        // else if (w <= 768 && h <= 576) {
        else if (w <= 776 && h <= 592) {
            // 720x576 (PAL) (handbrake sometimes encode it to a max of 776 x 592)
            return VIDEO_FORMAT_576P;
        } else if (w <= 960 && h <= 544) {
            // 960x540 (sometimes 544 which is multiple of 16)
            return VIDEO_FORMAT_540P;
        } else if (w <= 1280 && h <= 720) {
            return VIDEO_FORMAT_720P;
        } else if (w <= 1920 && h <= 1080) {
            return VIDEO_FORMAT_1080P;
        } else if (w <= 3840 && h <= 2160) {
            return VIDEO_FORMAT_4K;
        }

        return VIDEO_FORMAT_8K;
    }

    /**
     * gets the exact video format (height + scantype p/i).
     * 
     * @return the exact video format
     */
    public String getExactVideoFormat() {
        return this.exactVideoFormat;
    }

    /**
     * Sets the exact video format (height + scantype p/i).
     * 
     * @param newValue
     *          the new exact video format
     */
    public void setExactVideoFormat(String newValue) {
        String oldValue = this.exactVideoFormat;
        this.exactVideoFormat = newValue;
        firePropertyChange("exactVideoFormat", oldValue, newValue);
    }

    public String getAudioChannels() {
        String channels = "";

        // get audio stream with highest channel count
        MediaFileAudioStream highestStream = getBestAudioStream();
        if (highestStream != null) {
            channels = highestStream.getChannels();
        }

        return channels;
    }

    /**
     * returns the exact video resolution.
     * 
     * @return eg 1280x720
     */
    public String getVideoResolution() {
        if (this.videoWidth == 0 || this.videoHeight == 0) {
            return "";
        }
        return this.videoWidth + "x" + this.videoHeight;
    }

    public int getVideoWidth() {
        return videoWidth;
    }

    public int getVideoHeight() {
        return videoHeight;
    }

    public void setVideoWidth(int newValue) {
        int oldValue = this.videoWidth;
        this.videoWidth = newValue;
        firePropertyChange("videoWidth", oldValue, newValue);
        firePropertyChange("videoFormat", oldValue, newValue);
    }

    public void setVideoHeight(int newValue) {
        int oldValue = this.videoHeight;
        this.videoHeight = newValue;
        firePropertyChange("videoHeight", oldValue, newValue);
        firePropertyChange("videoFormat", oldValue, newValue);
    }

    /**
     * if width-to-height aspect ratio greater than 1.37:1
     * 
     * @return true/false if widescreen
     */
    public Boolean isWidescreen() {
        if (this.videoWidth == 0 || this.videoHeight == 0) {
            return false;
        }
        return ((float) this.videoWidth) / ((float) this.videoHeight) > 1.37f ? true : false;
    }

    public Float getAspectRatio() {
        Float ret = 0F;
        if (this.videoWidth == 0 || this.videoHeight == 0) {
            return ret;
        }
        Float ar = (float) this.videoWidth / (float) this.videoHeight;

        // https://github.com/xbmc/xbmc/blob/master/xbmc/utils/StreamDetails.cpp#L538
        // Given that we're never going to be able to handle every single possibility in
        // aspect ratios, particularly when cropping prior to video encoding is taken into account
        // the best we can do is take the "common" aspect ratios, and return the closest one available.
        // The cutoffs are the geometric mean of the two aspect ratios either side.
        if (ar < 1.3499f) { // sqrt(1.33*1.37)
            ret = 1.33F;
        } else if (ar < 1.5080f) { // sqrt(1.37*1.66)
            ret = 1.37F;
        } else if (ar < 1.7190f) { // sqrt(1.66*1.78)
            ret = 1.66F;
        } else if (ar < 1.8147f) { // sqrt(1.78*1.85)
            ret = 1.78F;
        } else if (ar < 2.0174f) { // sqrt(1.85*2.20)
            ret = 1.85F;
        } else if (ar < 2.2738f) { // sqrt(2.20*2.35)
            ret = 2.20F;
        } else if (ar < 2.3749f) { // sqrt(2.35*2.40)
            ret = 2.35F;
        } else if (ar < 2.4739f) { // sqrt(2.40*2.55)
            ret = 2.40F;
        } else if (ar < 2.6529f) { // sqrt(2.55*2.76)
            ret = 2.55F;
        } else {
            ret = 2.76F;
        }
        return ret;
    }

    /**
     * SD (less than 720 lines) or HD (more than 720 lines).
     * 
     * @return SD or HD
     */
    public String getVideoDefinitionCategory() {
        if (this.videoWidth == 0 || this.videoHeight == 0) {
            return "";
        }
        return this.videoWidth >= 1280 || this.videoHeight >= 720 ? "HD" : "SD";
    }

    /**
     * returns the overall bit rate for this file.
     * 
     * @return bitrate in kbps
     */
    public int getOverallBitRate() {
        return overallBitRate;
    }

    /**
     * sets the overall bit rate for this file (in kbps).
     * 
     * @param newValue
     *          the new overall bit rate
     */
    public void setOverallBitRate(int newValue) {
        int oldValue = this.overallBitRate;
        this.overallBitRate = newValue;
        firePropertyChange("overallBitRate", oldValue, newValue);
        firePropertyChange("bitRateInKbps", oldValue, newValue);
    }

    /**
     * Gets the bite rate in kbps.
     * 
     * @return the bite rate in kbps
     */
    public String getBiteRateInKbps() {
        return this.overallBitRate + " kbps";
    }

    /**
     * returns the duration / runtime in seconds.
     * 
     * @return the duration
     */
    public int getDuration() {
        return durationInSecs;
    }

    public int getDurationInMinutes() {
        return durationInSecs / 60;
    }

    /**
     * returns the duration / runtime formatted<br>
     * eg 1h 35m.
     * 
     * @return the duration
     */
    public String getDurationHM() {
        if (this.durationInSecs == 0) {
            return "";
        }
        long h = TimeUnit.SECONDS.toHours(this.durationInSecs);
        long m = TimeUnit.SECONDS.toMinutes(this.durationInSecs - TimeUnit.HOURS.toSeconds(h));
        long s = TimeUnit.SECONDS
                .toSeconds(this.durationInSecs - TimeUnit.HOURS.toSeconds(h) - TimeUnit.MINUTES.toSeconds(m));
        if (s > 30) {
            m += 1; // round seconds
        }
        return h + "h " + String.format("%02d", m) + "m";
    }

    /**
     * returns the duration / runtime formatted<br>
     * eg 01:35:00.
     * 
     * @return the duration
     */
    public String getDurationHHMMSS() {
        if (this.durationInSecs == 0) {
            return "";
        }
        long h = TimeUnit.SECONDS.toHours(this.durationInSecs);
        long m = TimeUnit.SECONDS.toMinutes(this.durationInSecs - TimeUnit.HOURS.toSeconds(h));
        long s = TimeUnit.SECONDS
                .toSeconds(this.durationInSecs - TimeUnit.HOURS.toSeconds(h) - TimeUnit.MINUTES.toSeconds(m));
        return String.format("%02d:%02d:%02d", h, m, s);
    }

    /**
     * sets the duration / runtime in seconds.
     * 
     * @param newValue
     *          the new duration
     */
    public void setDuration(int newValue) {
        int oldValue = this.durationInSecs;
        this.durationInSecs = newValue;
        firePropertyChange("duration", oldValue, newValue);
        firePropertyChange("durationHM", oldValue, newValue);
        firePropertyChange("durationHHMMSS", oldValue, newValue);
    }

    public List<MediaFileAudioStream> getAudioStreams() {
        return audioStreams;
    }

    public void setAudioStreams(List<MediaFileAudioStream> audioStreams) {
        this.audioStreams = audioStreams;
    }

    public String getCombinedCodecs() {
        StringBuilder sb = new StringBuilder(videoCodec);

        for (MediaFileAudioStream audioStream : audioStreams) {
            if (sb.length() > 0) {
                sb.append(" / ");
            }
            sb.append(audioStream.getCodec());
        }

        return sb.toString();
    }

    /**
     * gets the 3D string from former mediainfo<br>
     * can be 3D / 3D SBS / 3D TAB
     * 
     */
    public String getVideo3DFormat() {
        return this.video3DFormat;
    }

    /**
     * explicit set the 3D format
     * 
     * @param video3DFormat
     *          the 3D format
     */
    public void setVideo3DFormat(String video3DFormat) {
        this.video3DFormat = video3DFormat;
    }

    private long getMediaInfoSnapshotFromISO() {
        // check if we have a snapshot xml
        Path xmlFile = Paths.get(this.path, this.filename.replaceFirst("\\.iso$", "-mediainfo.xml"));
        if (Files.exists(xmlFile)) {
            try {
                LOGGER.info("ISO: try to parse " + xmlFile);

                JAXBContext context = JAXBContext.newInstance(MediaInfoXMLParser.class);
                Unmarshaller um = context.createUnmarshaller();
                MediaInfoXMLParser xml = new MediaInfoXMLParser();

                Reader in = Files.newBufferedReader(xmlFile, StandardCharsets.UTF_8);
                xml = (MediaInfoXMLParser) um.unmarshal(in);
                in.close();
                xml.snapshot();

                // get snapshot from biggest file
                setMiSnapshot(xml.getBiggestFile().snapshot);
                setDuration(xml.getDuration()); // accumulated duration
                return xml.getFilesize();
            } catch (Exception e) {
                LOGGER.warn("ISO: Unable to parse " + xmlFile, e);
            }
        }

        if (miSnapshot == null) {
            int BUFFER_SIZE = 64 * 1024;
            Iso9660FileSystem image = null;
            try {
                LOGGER.trace("ISO: Open");
                image = new Iso9660FileSystem(getFileAsPath().toFile(), true);
                int dur = 0;
                long siz = 0L; // accumulated filesize
                long biggest = 0L;

                for (Iso9660FileEntry entry : image) {
                    LOGGER.trace("ISO: got entry " + entry.getName() + " size:" + entry.getSize());
                    siz += entry.getSize();

                    if (entry.getSize() <= 5000) { // small files and "." entries
                        continue;
                    }

                    MediaFile mf = new MediaFile(Paths.get(getFileAsPath().toString(), entry.getPath())); // set ISO as MF path
                    // mf.setMediaInfo(fileMI); // we need set the inner MI
                    if (mf.getType() == MediaFileType.VIDEO && mf.isDiscFile()) { // would not count video_ts.bup for ex (and not .dat files or other types)
                        mf.setFilesize(entry.getSize());

                        MediaInfo fileMI = new MediaInfo();
                        try {
                            // mediaInfo.option("File_IsSeekable", "0");
                            byte[] From_Buffer = new byte[BUFFER_SIZE];
                            int From_Buffer_Size; // The size of the read file buffer

                            // Preparing to fill MediaInfo with a buffer
                            fileMI.openBufferInit(entry.getSize(), 0);

                            long pos = 0L;
                            // The parsing loop
                            do {
                                // Reading data somewhere, do what you want for this.
                                From_Buffer_Size = image.readBytes(entry, pos, From_Buffer, 0, BUFFER_SIZE);
                                pos += From_Buffer_Size; // add bytes read to file position

                                // Sending the buffer to MediaInfo
                                int Result = fileMI.openBufferContinue(From_Buffer, From_Buffer_Size);
                                if ((Result & 8) == 8) { // Status.Finalized
                                    break;
                                }

                                // Testing if MediaInfo request to go elsewhere
                                if (fileMI.openBufferContinueGoToGet() != -1) {
                                    pos = fileMI.openBufferContinueGoToGet();
                                    LOGGER.trace("ISO: Seek to " + pos);
                                    // From_Buffer_Size = image.readBytes(entry, newPos, From_Buffer, 0, BUFFER_SIZE);
                                    // pos = newPos + From_Buffer_Size; // add bytes read to file position
                                    fileMI.openBufferInit(entry.getSize(), pos); // Informing MediaInfo we have seek
                                }

                            } while (From_Buffer_Size > 0);

                            LOGGER.trace("ISO: finalize");
                            // Finalizing
                            fileMI.openBufferFinalize(); // This is the end of the stream, MediaInfo must finish some work
                            Map<StreamKind, List<Map<String, String>>> tempSnapshot = fileMI.snapshot();
                            fileMI.close();

                            mf.setMiSnapshot(tempSnapshot); // set ours to MI for standard gathering
                            mf.gatherMediaInformation(); // normal gather from snapshots

                            // set ISO snapshot ONCE from biggest video file, so we copy all the resolutions & co
                            if (entry.getSize() > biggest) {
                                biggest = entry.getSize();
                                miSnapshot = tempSnapshot;
                            }

                            // accumulate durations from every MF
                            dur += mf.getDuration();
                            LOGGER.trace("ISO: file duration:" + mf.getDurationHHMMSS() + "  accumulated min:"
                                    + dur / 60);
                        }
                        // sometimes also an error is thrown
                        catch (Exception | Error e) {
                            LOGGER.error("Mediainfo could not open file STREAM", e);
                            fileMI.close();
                        }
                    } // end VIDEO
                } // end entry
                setDuration(dur); // set it here, and ignore duration parsing for ISO in gatherMI method...
                LOGGER.trace("ISO: final duration:" + getDurationHHMMSS());
                image.close();
                return siz;
            } catch (Exception e) {
                LOGGER.error("Mediainfo could not open STREAM - trying fallback", e);
                try {
                    if (image != null) {
                        image.close();
                        image = null;
                    }
                } catch (IOException e1) {
                    LOGGER.warn("Uh-oh. Cannot close disc image :(", e);
                }
                closeMediaInfo();
                getMediaInfoSnapshot();
            }
        }
        return 0;
    }

    /**
     * DO NOT USE - only for ISO!!!
     */
    @Deprecated
    public void setMediaInfo(MediaInfo mediaInfo) {
        this.mediaInfo = mediaInfo;
    }

    /**
     * DO NOT USE - only for ISO!!!
     */
    @Deprecated
    public void setMiSnapshot(Map<StreamKind, List<Map<String, String>>> miSnapshot) {
        this.miSnapshot = miSnapshot;
    }

    /**
     * Gathers the media information via the native mediainfo lib.<br>
     * If mediafile has already be scanned, it will be skipped.<br>
     * Use gatherMediaInformation(boolean force) to force the execution.
     */
    public void gatherMediaInformation() {
        gatherMediaInformation(false);
    }

    /**
     * Gathers the media information via the native mediainfo lib.
     * 
     * @param force
     *          forces the execution, will not stop on already imported files
     */
    public void gatherMediaInformation(boolean force) {
        // check for supported filetype
        if (!isValidMediainfoFormat()) {
            // okay, we have no valid MI file, be sure it will not be triggered any more
            if (StringUtils.isBlank(getContainerFormat())) {
                setContainerFormat(getExtension());
            }
            return;
        }

        // mediainfo already gathered
        if (!force && !getContainerFormat().isEmpty()) {
            return;
        }

        // gather subtitle infos independent of MI
        if (getType() == MediaFileType.SUBTITLE) {
            gatherSubtitleInformation();
        }

        // file size and last modified
        try {
            BasicFileAttributes attrs = Files.readAttributes(getFileAsPath(), BasicFileAttributes.class);
            filedate = attrs.lastModifiedTime().toMillis();
            setFilesize(attrs.size());
        } catch (IOException e) {
            if (miSnapshot == null) { // maybe we set it already (from ISO) so only display message when empty
                LOGGER.warn("could not get file information (size/date): " + e.getMessage());
            }
            // do not set/return here - we might have set it already... and the next check does check for a 0-byte file
            // setContainerFormat(getExtension());
            // return;
        }

        // do not work further on 0 byte files
        if (getFilesize() == 0) {
            LOGGER.warn("0 Byte file detected: " + this.filename);
            // set container format to do not trigger it again
            setContainerFormat(getExtension());
            return;
        }

        // do not work further on subtitles/NFO files
        if (type == MediaFileType.SUBTITLE || type == MediaFileType.NFO) {
            // set container format to do not trigger it again
            setContainerFormat(getExtension());
            return;
        }

        // get media info
        LOGGER.debug("start MediaInfo for " + this.getFileAsPath());
        long discFilesSizes = 0L;
        if (isISO) {
            discFilesSizes = getMediaInfoSnapshotFromISO();
        } else {
            getMediaInfoSnapshot();
        }

        if (miSnapshot == null) {
            // MI could not be opened
            LOGGER.error("error getting MediaInfo for " + this.filename);
            // set container format to do not trigger it again
            setContainerFormat(getExtension());
            closeMediaInfo();
            return;
        }
        LOGGER.trace("got MI");

        String height = "";
        String scanType = "";
        String width = "";
        String videoCodec = "";

        switch (type) {
        case VIDEO:
        case VIDEO_EXTRA:
        case SAMPLE:
        case TRAILER:
            height = getMediaInfo(StreamKind.Video, 0, "Height");
            scanType = getMediaInfo(StreamKind.Video, 0, "ScanType");
            width = getMediaInfo(StreamKind.Video, 0, "Width");
            videoCodec = getMediaInfo(StreamKind.Video, 0, "CodecID/Hint", "Format");

            // fix for Microsoft VC-1
            if (StringUtils.containsIgnoreCase(videoCodec, "Microsoft")) {
                videoCodec = getMediaInfo(StreamKind.Video, 0, "Format");
            }

            // *****************
            // get audio streams
            // *****************
            // int streams = getMediaInfo().streamCount(StreamKind.Audio);
            int streams = 0;
            if (streams == 0) {
                // fallback 1
                String cnt = getMediaInfo(StreamKind.General, 0, "AudioCount");
                try {
                    streams = Integer.parseInt(cnt);
                } catch (Exception e) {
                    streams = 0;
                }
            }
            if (streams == 0) {
                // fallback 2
                String cnt = getMediaInfo(StreamKind.Audio, 0, "StreamCount");
                try {
                    streams = Integer.parseInt(cnt);
                } catch (Exception e) {
                    streams = 0;
                }
            }
            audioStreams.clear();
            for (int i = 0; i < streams; i++) {
                MediaFileAudioStream stream = new MediaFileAudioStream();
                String audioCodec = getMediaInfo(StreamKind.Audio, i, "CodecID/Hint", "Format");
                audioCodec = audioCodec.replaceAll("\\p{Punct}", "");

                String audioAddition = getMediaInfo(StreamKind.Audio, i, "Format_Profile");
                if ("dts".equalsIgnoreCase(audioCodec) && StringUtils.isNotBlank(audioAddition)) {
                    if (audioAddition.contains("ES")) {
                        audioCodec = "DTSHD-ES";
                    }
                    if (audioAddition.contains("HRA")) {
                        audioCodec = "DTSHD-HRA";
                    }
                    if (audioAddition.contains("MA")) {
                        audioCodec = "DTSHD-MA";
                    }
                }
                stream.setCodec(audioCodec);

                // AAC sometimes codes channels into Channel(s)_Original
                String channels = getMediaInfo(StreamKind.Audio, i, "Channel(s)_Original", "Channel(s)");
                stream.setChannels(StringUtils.isEmpty(channels) ? "" : channels + "ch");
                try {
                    String br = getMediaInfo(StreamKind.Audio, i, "BitRate");
                    stream.setBitrate(Integer.parseInt(br) / 1024);
                } catch (Exception ignored) {
                }
                String language = getMediaInfo(StreamKind.Audio, i, "Language/String", "Language");
                if (language.isEmpty()) {
                    if (!isDiscFile()) { // video_ts parsed 'ts' as Tsonga
                        // try to parse from filename
                        String shortname = getBasename().toLowerCase(Locale.ROOT);
                        stream.setLanguage(parseLanguageFromString(shortname));
                    }
                } else {
                    stream.setLanguage(parseLanguageFromString(language));
                }
                audioStreams.add(stream);
            }

            // ********************
            // get subtitle streams
            // ********************
            // streams = getMediaInfo().streamCount(StreamKind.Text);
            streams = 0;
            if (streams == 0) {
                // fallback 1
                String cnt = getMediaInfo(StreamKind.General, 0, "TextCount");
                try {
                    streams = Integer.parseInt(cnt);
                } catch (Exception e) {
                    streams = 0;
                }
            }
            if (streams == 0) {
                // fallback 2
                String cnt = getMediaInfo(StreamKind.Text, 0, "StreamCount");
                try {
                    streams = Integer.parseInt(cnt);
                } catch (Exception e) {
                    streams = 0;
                }
            }

            subtitles.clear();
            for (int i = 0; i < streams; i++) {
                MediaFileSubtitle stream = new MediaFileSubtitle();

                String codec = getMediaInfo(StreamKind.Text, i, "CodecID/Hint", "Format");
                stream.setCodec(codec.replaceAll("\\p{Punct}", ""));
                String lang = getMediaInfo(StreamKind.Text, i, "Language/String", "Language");
                stream.setLanguage(parseLanguageFromString(lang));

                String forced = getMediaInfo(StreamKind.Text, i, "Forced");
                boolean b = forced.equalsIgnoreCase("true") || forced.equalsIgnoreCase("yes");
                stream.setForced(b);

                subtitles.add(stream);
            }

            // detect 3D video (mainly from MKV files)
            // sample Mediainfo output:
            // MultiView_Count : 2
            // MultiView_Layout : Top-Bottom (left eye first)
            // MultiView_Layout : Side by Side (left eye first)
            String mvc = getMediaInfo(StreamKind.Video, 0, "MultiView_Count");
            if (!StringUtils.isEmpty(mvc) && mvc.equals("2")) {
                video3DFormat = VIDEO_3D;
                String mvl = getMediaInfo(StreamKind.Video, 0, "MultiView_Layout").toLowerCase(Locale.ROOT);
                LOGGER.debug("3D detected :) " + mvl);
                if (!StringUtils.isEmpty(mvl) && mvl.contains("top") && mvl.contains("bottom")) {
                    video3DFormat = VIDEO_3D_TAB;
                }
                if (!StringUtils.isEmpty(mvl) && mvl.contains("side")) {
                    video3DFormat = VIDEO_3D_SBS;
                }
            }

            break;

        case AUDIO:
            MediaFileAudioStream stream = new MediaFileAudioStream();
            String audioCodec = getMediaInfo(StreamKind.Audio, 0, "CodecID/Hint", "Format");
            stream.setCodec(audioCodec.replaceAll("\\p{Punct}", ""));
            String channels = getMediaInfo(StreamKind.Audio, 0, "Channel(s)");
            stream.setChannels(StringUtils.isEmpty(channels) ? "" : channels + "ch");
            try {
                String br = getMediaInfo(StreamKind.Audio, 0, "BitRate");
                stream.setBitrate(Integer.parseInt(br) / 1024);
            } catch (Exception e) {
            }
            String language = getMediaInfo(StreamKind.Audio, 0, "Language/String", "Language");
            if (language.isEmpty()) {
                // try to parse from filename
                String shortname = getBasename().toLowerCase(Locale.ROOT);
                stream.setLanguage(parseLanguageFromString(shortname));
            } else {
                stream.setLanguage(parseLanguageFromString(language));
            }
            audioStreams.clear();
            audioStreams.add(stream);
            break;

        case POSTER:
        case BANNER:
        case FANART:
        case THUMB:
        case EXTRAFANART:
        case GRAPHIC:
        case SEASON_POSTER:
        case LOGO:
        case CLEARLOGO:
        case CLEARART:
        case DISCART:
        case EXTRATHUMB:
            height = getMediaInfo(StreamKind.Image, 0, "Height");
            // scanType = getMediaInfo(StreamKind.Image, 0, "ScanType"); // no scantype on graphics
            width = getMediaInfo(StreamKind.Image, 0, "Width");
            videoCodec = getMediaInfo(StreamKind.Image, 0, "CodecID/Hint", "Format");
            // System.out.println(height + "-" + width + "-" + videoCodec);
            break;

        case NFO: // do nothing here, but do not display default warning (since we got the filedate)
            break;

        default:
            LOGGER.warn("no mediainformation handling for MediaFile type " + getType() + " yet.");
            break;
        }

        // video codec
        // e.g. XviD, x264, DivX 5, MPEG-4 Visual, AVC, etc.
        // get first token (e.g. DivX 5 => DivX)
        setVideoCodec(StringUtils.isEmpty(videoCodec) ? "" : new Scanner(videoCodec).next());

        // container format for all except subtitles (subtitle container format is handled another way)
        if (type == MediaFileType.SUBTITLE) {
            setContainerFormat(getExtension());
        } else {
            String extensions = getMediaInfo(StreamKind.General, 0, "Codec/Extensions", "Format");
            // get first extension
            setContainerFormat(
                    StringUtils.isBlank(extensions) ? "" : new Scanner(extensions).next().toLowerCase(Locale.ROOT));

            // if container format is still empty -> insert the extension
            if (StringUtils.isBlank(containerFormat)) {
                setContainerFormat(getExtension());
            }
        }

        if (height.isEmpty() || scanType.isEmpty()) {
            setExactVideoFormat("");
        } else {
            setExactVideoFormat(height + Character.toLowerCase(scanType.charAt(0)));
        }

        // video dimension
        if (!width.isEmpty()) {
            try {
                setVideoWidth(Integer.parseInt(width));
            } catch (NumberFormatException e) {
                setVideoWidth(0);
            }
        }
        if (!height.isEmpty()) {
            try {
                setVideoHeight(Integer.parseInt(height));
            } catch (NumberFormatException e) {
                setVideoHeight(0);
            }
        }

        switch (type) {
        case VIDEO:
        case VIDEO_EXTRA:
        case SAMPLE:
        case TRAILER:
        case AUDIO:
            // overall bitrate (OverallBitRate/String)
            String br = getMediaInfo(StreamKind.General, 0, "OverallBitRate");
            if (!br.isEmpty()) {
                try {
                    setOverallBitRate(Integer.parseInt(br) / 1024); // in kbps
                } catch (NumberFormatException e) {
                    setOverallBitRate(0);
                }
            }

            // Duration;Play time of the stream in ms
            // Duration/String;Play time in format : XXx YYy only, YYy omited if zero
            // Duration/String1;Play time in format : HHh MMmn SSs MMMms, XX om.if.z.
            // Duration/String2;Play time in format : XXx YYy only, YYy omited if zero
            // Duration/String3;Play time in format : HH:MM:SS.MMM
            if (!isISO) {
                // ISO files get duration accumulated with snapshot
                String dur = getMediaInfo(StreamKind.General, 0, "Duration");
                if (!dur.isEmpty()) {
                    try {
                        Double d = Double.parseDouble(dur);
                        setDuration(d.intValue() / 1000);
                    } catch (NumberFormatException e) {
                        setDuration(0);
                    }
                }
            } else {
                // do some sanity check, to see, if we have an invalid DVD structure
                // eg when the sum(filesize) way higher than ISO size
                LOGGER.trace("ISO size:" + filesize + "  dataSize:" + discFilesSizes + "  = diff:"
                        + Math.abs(discFilesSizes - filesize));
                if (discFilesSizes > 0 && filesize > 0) {
                    long gig = 1024 * 1024 * 1024;
                    if (Math.abs(discFilesSizes - filesize) > gig) {
                        LOGGER.error("ISO file seems to have an invalid structure - ignore duration");
                        // we set the ISO duration to zero,
                        // so the standard getDuration() will always get the scraped duration
                        setDuration(0);
                    }
                }
            }
        default:
            break;
        }

        LOGGER.trace("extracted MI");
        // close mediainfo lib
        closeMediaInfo();
        LOGGER.trace("closed MI");
    }

    private String parseLanguageFromString(String shortname) {
        if (StringUtils.isBlank(shortname)) {
            return "";
        }
        Set<String> langArray = LanguageUtils.KEY_TO_LOCALE_MAP.keySet();
        shortname = shortname.replaceAll("(?i)Part [Ii]+", ""); // hardcoded; remove Part II which is no stacking marker; b/c II is a valid iso code :p
        shortname = StringUtils.split(shortname, '/')[0].trim(); // possibly "de / de" - just take first
        for (String s : langArray) {
            try {
                if (shortname.equalsIgnoreCase(s) || shortname.matches("(?i).*[ _.-]+" + s + "$")) {// ends with lang + delimiter prefix
                    LOGGER.debug("found language '" + s + "' in '" + this.getFilename());
                    return LanguageUtils.getIso3LanguageFromLocalizedString(s);
                }
            } catch (Exception e) {
                LOGGER.warn("Error parsing subtitle language from locale keyset: " + s, e);
            }
        }
        return "";
    }

    /**
     * Checks if is valid mediainfo format.
     * 
     * @return true, if is valid mediainfo format
     */
    private boolean isValidMediainfoFormat() {
        String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);

        // check unsupported extensions
        if ("bin".equals(extension) || "dat".equals(extension) || "img".equals(extension) || "nrg".equals(extension)
                || "disc".equals(extension)) {
            return false;
        } else if ("iso".equals(extension)) {
            isISO = true;
            return true;
        }

        // parse audio, video and graphic files (NFO only for getting the filedate)
        if (type.equals(MediaFileType.VIDEO) || type.equals(MediaFileType.VIDEO_EXTRA)
                || type.equals(MediaFileType.TRAILER) || type.equals(MediaFileType.SAMPLE)
                || type.equals(MediaFileType.SUBTITLE) || type.equals(MediaFileType.AUDIO)
                || type.equals(MediaFileType.NFO) || isGraphic()) {
            return true;
        }

        return false;
    }

    /**
     * does MediaFile exists?
     * 
     * @return true/false
     */
    public boolean exists() {
        return Files.exists(getFileAsPath());
    }

    /**
     * <b>PHYSICALLY</b> deletes a MF by moving it to datasource backup folder<br>
     * DS\.backup\&lt;filename&gt;<br>
     * maintaining its orginating directory
     * 
     * @param datasource
     *          the data source (for the location of the backup folder)
     * @return true/false if successful
     */
    public boolean deleteSafely(String datasource) {
        return Utils.deleteFileWithBackup(getFileAsPath(), datasource);
    }

    @Override
    public boolean equals(Object mf2) {
        if ((mf2 != null) && (mf2 instanceof MediaFile)) {
            return compareTo((MediaFile) mf2) == 0;
        }
        return false;
    }

    @Override
    public int compareTo(MediaFile mf2) {
        return this.getFileAsPath().compareTo(mf2.getFileAsPath());
    }

    @Override
    public int hashCode() {
        return this.getFileAsPath().hashCode();
    }
}