com.moviejukebox.model.Movie.java Source code

Java tutorial

Introduction

Here is the source code for com.moviejukebox.model.Movie.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.model;

import com.moviejukebox.model.enumerations.*;
import com.moviejukebox.plugin.MovieDatabasePlugin;
import com.moviejukebox.tools.*;
import java.io.File;
import java.util.*;
import java.util.Map.Entry;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.math.NumberUtils;
import org.pojava.datetime.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Movie bean
 *
 * @author jjulien
 * @author artem.gratchev
 */
@XmlType
public class Movie implements Comparable<Movie>, Identifiable, IMovieBasicInformation {

    /*
     * Static & Final variables that are used for control and don't relate
     * specifically to the Movie object
     */
    private static final Logger LOG = LoggerFactory.getLogger(Movie.class);
    public static final String UNKNOWN = "UNKNOWN";
    public static final String SOURCE_FILENAME = "filename";
    public static final String NOTRATED = "Not Rated";
    public static final String REMOVE = "Remove"; // All Movie objects with this type will be removed from library before index generation
    public static final String TYPE_MOVIE = "MOVIE";
    public static final String TYPE_TVSHOW = "TVSHOW";
    public static final String TYPE_UNKNOWN = UNKNOWN;
    public static final String TYPE_VIDEO_UNKNOWN = UNKNOWN;
    public static final String TYPE_VIDEO_HD = "HD";
    public static final String TYPE_BLURAY = "BLURAY"; // Used to indicate what physical format the video is
    public static final String TYPE_DVD = "DVD"; // Used to indicate what physical format the video is
    public static final String TYPE_FILE = "FILE"; // Used to indicate what physical format the video is
    public static final String TYPE_PERSON = "PERSON";
    public static final String SPACE_SLASH_SPACE = " / ";
    private String mjbVersion = UNKNOWN;
    private String mjbGitSHA = UNKNOWN;
    private DateTime mjbGenerationDate = null;
    /*
     * --------------------------------------------------------------------------------
     * Properties that control the object
     */
    private static final List<String> SORT_IGNORE_PREFIXES = new ArrayList<>();
    private final int highdef720 = PropertiesUtil.getIntProperty("highdef.720.width", 1280); // Get the minimum width for a high-definition movies
    private final int highdef1080 = PropertiesUtil.getIntProperty("highdef.1080.width", 1920); // Get the minimum width for a high-definition movies
    private final String[] ratingSource = PropertiesUtil.getProperty("mjb.rating.source", "average").split(",");
    private final String tmpRatingIgnore = PropertiesUtil.getProperty("mjb.rating.ignore", "");
    private final List<String> ratingIgnore = StringTools.isValidString(tmpRatingIgnore)
            ? Arrays.asList(tmpRatingIgnore.split(","))
            : new ArrayList<String>();
    private static final Set<String> GENRE_SKIP_LIST = new HashSet<>(); // List of genres to ignore
    private static final TitleSortType TITLE_SORT_TYPE = TitleSortType
            .fromString(PropertiesUtil.getProperty("mjb.sortTitle", "title"));
    // TODO: This will be removed in the future, once hashing has been completed
    private static final Boolean DIR_HASH = PropertiesUtil.getBooleanProperty("mjb.dirHash", Boolean.FALSE);
    // checks
    private static final int MAX_COUNT_DIRECTOR = PropertiesUtil.getReplacedIntProperty("movie.director.maxCount",
            "plugin.people.maxCount.director", 2);
    private static final int MAX_COUNT_WRITER = PropertiesUtil.getReplacedIntProperty("movie.writer.maxCount",
            "plugin.people.maxCount.writer", 3);
    private static final int MAX_COUNT_ACTOR = PropertiesUtil.getReplacedIntProperty("movie.actor.maxCount",
            "plugin.people.maxCount.actor", 10);
    private static final int MAX_LENGTH_PLOT = PropertiesUtil.getReplacedIntProperty("movie.plot.maxLength",
            "plugin.plot.maxlength", 500);
    private static final int MAX_LENGTH_OUTLINE = PropertiesUtil.getReplacedIntProperty("movie.outline.maxLength",
            "plugin.outline.maxlength", 300);
    /*
     * --------------------------------------------------------------------------------
     * Properties related to the Movie object itself
     */
    private String baseName; // Safe name for generated files
    private String baseFilename; // Base name for finding posters, nfos, banners, etc.
    private Map<String, String> idMap = new HashMap<>(2);
    private String title = UNKNOWN;
    private String titleSort = UNKNOWN;
    private String strippedTitleSort = UNKNOWN; // Not saved, used to speedup the sort
    private String originalTitle = UNKNOWN;
    private String year = UNKNOWN;
    private String releaseDate = UNKNOWN;
    private Map<String, Integer> ratings = new HashMap<>();
    private String plot = UNKNOWN;
    private String outline = UNKNOWN;
    private String quote = UNKNOWN;
    private String tagline = UNKNOWN;
    private String company = UNKNOWN;
    private String runtime = UNKNOWN;
    private String language = UNKNOWN;
    private String videoType = UNKNOWN;
    private String subtitles = UNKNOWN;
    private Set<String> countries = new LinkedHashSet<>();
    private Set<String> directors = new LinkedHashSet<>();
    private Map<String, Integer> sets = new HashMap<>();
    private Collection<String> genres = new TreeSet<>();
    private Set<String> cast = new LinkedHashSet<>();
    private Set<String> writers = new LinkedHashSet<>();
    private String container = UNKNOWN; // AVI, MKV, TS, etc.
    private Set<Codec> codecs = new LinkedHashSet<>();
    private String resolution = UNKNOWN; // 1280x528
    private String aspect = UNKNOWN;
    private String videoSource = UNKNOWN;
    private String videoOutput = UNKNOWN;
    private float fps = 60;
    private String certification = UNKNOWN;
    private String showStatus = UNKNOWN; // For TV shows a status such as "Continuing" or "Ended"
    private boolean scrapeLibrary;
    private boolean extra = Boolean.FALSE; // TODO Move extra flag to movie file
    private boolean trailerExchange = Boolean.FALSE; // Trailers
    private long trailerLastScan = 0; // Trailers
    private Collection<AwardEvent> awards = new ArrayList<>(); // Issue 1901: Awards
    private Collection<Filmography> people = new ArrayList<>(); // Issue 1897: Cast enhancement
    private String budget = UNKNOWN; // Issue 2012: Financial information about movie
    private Map<String, String> openweek = new HashMap<>();
    private Map<String, String> gross = new HashMap<>();
    private Map<OverrideFlag, String> overrideSources = new EnumMap<>(OverrideFlag.class);
    private List<String> didYouKnow = new ArrayList<>(); // Issue 2013: Add trivia
    private String libraryPath = UNKNOWN;
    private String movieType = TYPE_MOVIE;
    private String formatType = TYPE_FILE;
    private int top250 = -1;
    private String libraryDescription = UNKNOWN;
    private long prebuf = -1;
    // Graphics URLs & files
    private String posterURL = UNKNOWN; // The original, unaltered, poster
    private String posterFilename = UNKNOWN; // The poster filename
    private String detailPosterFilename = UNKNOWN; // The resized poster for skins
    private String thumbnailFilename = UNKNOWN; // The thumbnail version of the poster for skins
    private List<String> footerFilename = new ArrayList<>(); // The footer image for skins
    private String fanartURL = UNKNOWN; // The fanart URL
    private String fanartFilename = UNKNOWN; // The resized fanart file
    private String bannerURL = UNKNOWN; // The TV Show banner URL
    private String bannerFilename = UNKNOWN; // The resized banner file
    private String wideBannerFilename = UNKNOWN; // The original banner file
    private String clearArtURL = UNKNOWN;
    private String clearArtFilename = UNKNOWN;
    private String clearLogoURL = UNKNOWN;
    private String clearLogoFilename = UNKNOWN;
    private String seasonThumbURL = UNKNOWN;
    private String seasonThumbFilename = UNKNOWN;
    private String tvThumbURL = UNKNOWN;
    private String tvThumbFilename = UNKNOWN;
    private String movieDiscURL = UNKNOWN;
    private String movieDiscFilename = UNKNOWN;
    // File information
    private Date fileDate = null;
    private long fileSize = 0;
    // Watched informations
    private boolean watchedNFO = Boolean.FALSE;
    private boolean watchedFile = Boolean.FALSE;

    // Navigation data
    private String first = UNKNOWN;
    private String previous = UNKNOWN;
    private String next = UNKNOWN;
    private String last = UNKNOWN;
    private Map<String, String> indexes = new HashMap<>();
    // Media file properties
    private Collection<MovieFile> movieFiles = new TreeSet<>();
    private Collection<ExtraFile> extraFiles = new TreeSet<>();
    private Set<DirtyFlag> dirtyFlags = EnumSet.noneOf(DirtyFlag.class); // List of the dirty flags associated with the Movie
    private File file;
    private File containerFile;
    // Set information
    private boolean setMaster = Boolean.FALSE; // True if movie actually is only a entry point to movies set.
    private int setSize = 0; // Amount of movies in set
    private MovieDatabasePlugin movieScanner = null;
    private boolean skipped = Boolean.FALSE;

    /*
     * --------------------------------------------------------------------------------
     * End of properties
     * --------------------------------------------------------------------------------
     */
    static {
        if (GENRE_SKIP_LIST.isEmpty()) {
            StringTokenizer st = new StringTokenizer(PropertiesUtil.getProperty("mjb.genre.skip", ""), ",;|");
            while (st.hasMoreTokens()) {
                GENRE_SKIP_LIST.add(st.nextToken().toLowerCase());
            }
        }
    }

    public void setSkipped(Boolean skipped) {
        this.skipped = skipped;
    }

    public boolean isSkipped() {
        return skipped;
    }

    public void setMjbVersion(String mjbVersion) {
        if (StringTools.isNotValidString(mjbVersion)) {
            this.mjbVersion = GitRepositoryState.getVersion();
        } else {
            this.mjbVersion = mjbVersion;
        }
    }

    @XmlElement
    public String getMjbVersion() {
        return mjbVersion;
    }

    @XmlElement
    public String getMjbGitSHA() {
        return mjbGitSHA;
    }

    public void setMjbGitSHA(String mjbGitSHA) {
        this.mjbGitSHA = mjbGitSHA;
    }

    public void setMjbGenerationDateString(String mjbGenerationDate) {
        try {
            this.mjbGenerationDate = DateTime.parse(mjbGenerationDate);
        } catch (Exception error) {
            this.mjbGenerationDate = new DateTime();
        }
    }

    public void setMjbGenerationDate(DateTime mjbGenerationDate) {
        if (mjbGenerationDate == null) {
            this.mjbGenerationDate = new DateTime();
        } else {
            this.mjbGenerationDate = mjbGenerationDate;
        }
    }

    @XmlElement
    public String getMjbGenerationDateString() {
        return getMjbGenerationDate().toString(DateTimeTools.getDateFormatLongString());
    }

    public DateTime getMjbGenerationDate() {
        if (this.mjbGenerationDate == null) {
            this.mjbGenerationDate = new DateTime();
        }
        return this.mjbGenerationDate;
    }

    public void addSet(String set) {
        addSet(set, null);
    }

    public void addSet(final String set, final Integer order) {
        String newSet = StringUtils.trimToNull(set);

        if (StringTools.isValidString(newSet)) {
            setDirty(DirtyFlag.INFO);
            LOG.debug("Set added: {}, {}", newSet, order == null ? ", unordered" : ", order: " + order);
            sets.put(newSet, order);
        }
    }

    public void addMovieFile(MovieFile movieFile) {
        if (movieFile != null) {
            setDirty(DirtyFlag.INFO);
            // Always replace MovieFile
            for (MovieFile mf : this.movieFiles) {
                if (mf.compareTo(movieFile) == 0) {
                    movieFile.setFile(mf.getFile());
                    movieFile.setFilename(mf.getFilename());
                    movieFile.setInfo(mf.getInfo());
                }
            }
            this.movieFiles.remove(movieFile);
            this.movieFiles.add(movieFile);
        }
    }

    public void addAward(AwardEvent award) {
        if (award != null) {
            setDirty(DirtyFlag.INFO);
            this.awards.add(award);
        }
    }

    public boolean addPerson(Filmography person) {
        boolean added = Boolean.FALSE;
        if (person != null) {
            boolean duplicate = Boolean.FALSE;
            String name = person.getName();
            String job = person.getJob();
            for (Filmography p : people) {
                if (p.getName().equals(name) && p.getJob().equals(job)) {
                    duplicate = Boolean.TRUE;
                    break;
                }
            }
            if (!duplicate) {
                setDirty(DirtyFlag.INFO);
                added = people.add(person);
            }
        }
        return added;
    }

    public void removePerson(Filmography person) {
        if (person != null) {
            people.remove(person);
        }
    }

    public boolean addPerson(String key, String name, String url, String job, String source) {
        return addPerson(key, name, url, job, Movie.UNKNOWN, Movie.UNKNOWN, source);
    }

    public boolean addPerson(String key, String name, String url, String job, String character, String doublage,
            String source) {
        boolean added = Boolean.FALSE;

        if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(key) && StringUtils.isNotBlank(url)
                && StringUtils.isNotBlank(job)) {

            Filmography person = new Filmography();

            if (key.indexOf(':') > -1) {
                String[] keys = key.split(":");
                person.setId(keys[0], keys[1]);
            } else {
                person.setId(key);
            }

            if (name.indexOf(':') > -1) {
                String[] names = name.split(":");
                if (StringTools.isValidString(names[0])) {
                    person.setName(names[0]);
                    person.setTitle(names[1]);
                } else if (StringTools.isValidString(names[1])) {
                    person.setName(names[1]);
                } else {
                    person.setName(name);
                }
            } else {
                person.setName(name);
            }

            person.setUrl(url);
            person.setCharacter(character);
            person.setDoublage(doublage);
            person.setJob(job);
            person.setDepartment();

            int countActor = 0;
            if (person.getDepartment().equalsIgnoreCase(Filmography.DEPT_ACTORS)) {
                for (Filmography member : people) {
                    if (member.getDepartment().equalsIgnoreCase(Filmography.DEPT_ACTORS)) {
                        countActor++;
                    }
                }
            }

            person.setOrder(countActor);
            person.setCastId(people.size());
            person.setScrapeLibrary(scrapeLibrary);
            if (StringTools.isNotValidString(source)) {
                person.setSource(UNKNOWN);
            } else {
                person.setSource(source.toUpperCase());
            }
            added = addPerson(person);
        }
        return added;
    }

    public void removeMovieFile(MovieFile movieFile) {
        if (movieFile != null) {
            setDirty(DirtyFlag.INFO);
            for (MovieFile mf : this.movieFiles) {
                if (mf.compareTo(movieFile) == 0) {
                    this.movieFiles.remove(mf);
                    break;
                }
            }

        }
    }

    public boolean hasNewMovieFiles() {
        for (MovieFile movieFile : movieFiles) {
            if (movieFile.isNewFile()) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    /**
     * Add a new extra file to the movie
     *
     * @param extraFile
     */
    public void addExtraFile(ExtraFile extraFile) {
        addExtraFile(extraFile, Boolean.TRUE);
    }

    /**
     * Add a new extra file to the movie without marking the movie as dirty.
     *
     * @param extraFile
     * @param isNewFile Use carefully as this will not cause the movie to be marked as dirty and may not be written out
     */
    public void addExtraFile(ExtraFile extraFile, boolean isNewFile) {
        // Only add extraFile if it doesn't already exists
        if (extraFile != null && !this.extraFiles.contains(extraFile)) {
            if (isNewFile) {
                setDirty(DirtyFlag.INFO);
            }
            this.extraFiles.add(extraFile);
        }
    }

    public boolean hasNewExtraFiles() {
        for (MovieFile movieFile : extraFiles) {
            if (movieFile.isNewFile()) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    public String getStrippedTitleSort() {
        // If we have a strippedTitleSort, return that
        if (StringTools.isValidString(strippedTitleSort)) {
            return strippedTitleSort;
        }

        StringBuilder text = new StringBuilder(getStrippedTitle(getTitleSort()));
        int season = getSeason();

        // Added season to handle properly sorting the season numbers
        if (season >= 0) {
            if (season < 10) {
                text.append(" 0");
            } else {
                text.append(" ");
            }
            text.append(season);
        }
        // Added Year to handle movies like Ocean's Eleven (1960) and Ocean's Eleven (2001)
        text.append(" (").append(this.getYear()).append(") ");
        strippedTitleSort = text.toString().toLowerCase();
        return strippedTitleSort;
    }

    /**
     * Remove the sorting strip prefix from the title
     *
     * @param title
     * @return
     */
    private static String getStrippedTitle(String title) {
        String lowerTitle = title.toLowerCase();

        for (String prefix : SORT_IGNORE_PREFIXES) {
            if (lowerTitle.startsWith(prefix.toLowerCase())) {
                return title.substring(prefix.length());
            }
        }

        return title;
    }

    @Override
    public int compareTo(Movie anotherMovie) {
        return this.getStrippedTitleSort().compareToIgnoreCase(anotherMovie.getStrippedTitleSort());
    }

    @Deprecated
    public String getAudioCodec() {
        StringBuilder sb = new StringBuilder();
        boolean firstCodec = Boolean.TRUE;
        for (Codec audioCodec : codecs) {
            if (audioCodec.getCodecType() == CodecType.AUDIO) {
                if (firstCodec) {
                    firstCodec = Boolean.FALSE;
                } else {
                    sb.append(SPACE_SLASH_SPACE);
                }

                if (StringTools.isValidString(audioCodec.getCodecIdHint())) {
                    sb.append(audioCodec.getCodecIdHint());
                } else if (StringTools.isValidString(audioCodec.getCodec())) {
                    sb.append(audioCodec.getCodec());
                } else if (StringTools.isValidString(audioCodec.getCodecFormat())) {
                    sb.append(audioCodec.getCodecFormat());
                } else if (StringTools.isValidString(audioCodec.getCodecId())) {
                    sb.append(audioCodec.getCodecId());
                } else {
                    sb.append(Movie.UNKNOWN);
                }

                if (StringTools.isValidString(audioCodec.getCodecLanguage())) {
                    sb.append(" (");
                    sb.append(audioCodec.getCodecLanguage());
                    sb.append(")");
                }
            }
        }

        if (sb.length() > 0) {
            return sb.toString();
        }
        return UNKNOWN;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#getBaseName()
     */
    @Override
    public String getBaseName() {
        return baseName;
    }

    public String getBaseFilename() {
        return baseFilename;
    }

    @XmlElementWrapper(name = "cast")
    @XmlElement(name = "actor")
    public Collection<String> getCast() {
        return cast;
    }

    @XmlElementWrapper(name = "writers")
    @XmlElement(name = "writer")
    public Collection<String> getWriters() {
        return writers;
    }

    public Collection<String> getDidYouKnow() {
        return didYouKnow;
    }

    public String getCompany() {
        return company;
    }

    public String getContainer() {
        return container;
    }

    public Collection<String> getCountries() {
        return countries;
    }

    public String getCountriesAsString() {
        if (countries.isEmpty()) {
            return UNKNOWN;
        }
        return StringUtils.join(this.countries.iterator(), SPACE_SLASH_SPACE);
    }

    public Collection<MovieFile> getFiles() {
        return movieFiles;
    }

    @XmlElementWrapper(name = "extras")
    @XmlElement(name = "extra")
    public Collection<ExtraFile> getExtraFiles() {
        return extraFiles;
    }

    @XmlElementWrapper(name = "awards")
    @XmlElement(name = "award")
    public Collection<AwardEvent> getAwards() {
        return awards;
    }

    @XmlElementWrapper(name = "people")
    @XmlElement(name = "person")
    public Collection<Filmography> getPeople() {
        return people;
    }

    public Collection<String> getPerson(String department) {
        Collection<String> pList = new ArrayList<>();
        for (Filmography p : people) {
            if (p.getDepartment().equals(department)) {
                pList.add(p.getTitle());
            }
        }
        return pList;
    }

    public String getCertification() {
        return certification;
    }

    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getFirst() {
        return first;
    }

    /**
     * Get the first logical file of the set of videos
     *
     * @return
     */
    public MovieFile getFirstFile() {

        for (MovieFile part : movieFiles) {
            if (part.getFirstPart() == 1) {
                return part;
            }
        }

        Iterator<MovieFile> i = movieFiles.iterator();
        if (i.hasNext()) {
            return i.next();
        }
        return null;
    }

    public float getFps() {
        return fps;
    }

    @XmlElementWrapper(name = "genres")
    @XmlElement(name = "genre")
    public Collection<String> getGenres() {
        return genres;
    }

    public Collection<String> getSetsKeys() {
        return sets.keySet();
    }

    public Integer getSetOrder(String set) {
        return sets.get(set);
    }

    @Override
    public String getId(String key) {
        String result = idMap.get(key);
        return (result == null ? UNKNOWN : result);
    }

    public Map<String, String> getIdMap() {
        return idMap;
    }

    public String getGross(String country) {
        String result = gross.get(country);
        return (result == null ? UNKNOWN : result);
    }

    public Map<String, String> getGross() {
        return gross;
    }

    public String getOpenWeek(String country) {
        String result = openweek.get(country);
        return (result == null ? UNKNOWN : result);
    }

    public Map<String, String> getOpenWeek() {
        return openweek;
    }

    /**
     * Get the source of the override item
     *
     * @param overrideFlag
     * @return
     */
    public String getOverrideSource(OverrideFlag overrideFlag) {
        String source = overrideSources.get(overrideFlag);
        return StringUtils.isBlank(source) ? Movie.UNKNOWN : source;
    }

    public static class MovieId {

        @XmlAttribute
        public String movieDatabase;
        @XmlValue
        public String value;
    }

    @XmlElement(name = "id")
    public List<MovieId> getMovieIds() {
        List<MovieId> list = new ArrayList<>();
        for (Entry<String, String> e : idMap.entrySet()) {
            MovieId id = new MovieId();
            id.movieDatabase = e.getKey();
            id.value = e.getValue();
            list.add(id);
        }
        return list;
    }

    public void setMovieIds(List<MovieId> list) {
        idMap.clear();
        for (MovieId id : list) {
            idMap.put(id.movieDatabase, id.value);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#getLanguage()
     */
    @Override
    public String getLanguage() {
        return language;
    }

    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getLast() {
        return last;
    }

    @XmlElementWrapper(name = "files")
    @XmlElement(name = "file")
    public Collection<MovieFile> getMovieFiles() {
        return movieFiles;
    }

    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getNext() {
        return next;
    }

    public String getPlot() {
        return plot;
    }

    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getPrevious() {
        return previous;
    }

    public String getReleaseDate() {
        return releaseDate;
    }

    public String getResolution() {
        return resolution;
    }

    // Return the width of the movie
    public int getWidth() {
        int width = 0;
        if (StringTools.isValidString(getResolution()) && getResolution().contains("x")) {
            width = NumberUtils.toInt(getResolution().substring(0, getResolution().indexOf("x")), 0);
        }
        return width;
    }

    public String getRuntime() {
        return runtime;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#getSeason()
     */
    @Override
    public int getSeason() {
        /*
         * Return the first season as the whole season
         *
         * This could be changed later to allow multi season movie objects. Do
         * not return a value for the set master.
         */
        if (!movieFiles.isEmpty() && !setMaster) {
            return getFirstFile().getSeason();
        }
        // Strictly speaking this isn't "-1" its a non-existent season.
        return -1;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#getTitle()
     */
    @Override
    @XmlElement
    public String getTitle() {
        return title;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#getTitleSort()
     */
    /**
     * Return the correct sort title based on the mjb.sortTitle parameter
     *
     * @return
     */
    @Override
    @XmlElement
    public String getTitleSort() {
        // If we have a titleSort, return that
        if (StringTools.isValidString(titleSort)) {
            return titleSort;
        }

        // There are three choices for the sort title: title, original, filename
        if (TITLE_SORT_TYPE == TitleSortType.TITLE) {
            // Set the title sort (so this is only done once)
            setTitleSort(title);
        } else if (TITLE_SORT_TYPE == TitleSortType.FILENAME) {
            // Set the title sort (so this is only done once)
            setTitleSort(baseName);
        } else if ((TITLE_SORT_TYPE == TitleSortType.ORIGINAL || TITLE_SORT_TYPE == TitleSortType.ADOPT_ORIGINAL)
                && StringTools.isValidString(originalTitle)) {
            // Set the title sort (so this is only done once)
            setTitleSort(originalTitle);
        }

        return titleSort;
    }

    @Override
    @XmlElement
    public String getOriginalTitle() {
        return originalTitle;
    }

    public String getVideoOutput() {
        return videoOutput;
    }

    public String getVideoSource() {
        return videoSource;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#getYear()
     */
    @Override
    public String getYear() {
        return year;
    }

    public String getBudget() {
        return budget;
    }

    public String getSubtitles() {
        return subtitles;
    }

    public void setDirty(DirtyFlag dirtyType, boolean dirty) {
        if (dirty) {
            dirtyFlags.add(dirtyType);
        } else {
            dirtyFlags.remove(dirtyType);
        }
    }

    public void setDirty(DirtyFlag dirtyType) {
        dirtyFlags.add(dirtyType);
    }

    /**
     * Clear ALL the dirty flags, and just set DirtyFlag.INFO to the passed value
     *
     * @param dirty
     */
    public void setDirty(boolean dirty) {
        clearDirty();
        setDirty(DirtyFlag.INFO, dirty);
    }

    /**
     * Returns true if ANY of the dirty flags are set. Use with caution, it's better to test individual flags as you need them,
     * rather than this generic flag
     *
     * @return
     */
    @XmlTransient
    public boolean isDirty() {
        return !dirtyFlags.isEmpty();
    }

    public String showDirty() {
        if (dirtyFlags.isEmpty()) {
            return "NOT DIRTY";
        }
        return dirtyFlags.toString();
    }

    public Set<DirtyFlag> getDirty() {
        return dirtyFlags;
    }

    public void clearDirty() {
        dirtyFlags.clear();
    }

    public boolean isDirty(DirtyFlag dirtyType) {
        return dirtyFlags.contains(dirtyType);
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#isTVShow()
     */
    @XmlAttribute(name = "isTV")
    @Override
    public boolean isTVShow() {
        return (this.movieType.equals(TYPE_TVSHOW) || getSeason() != -1);
    }

    @XmlTransient
    public boolean isBluray() {
        return this.formatType.equals(TYPE_BLURAY);
    }

    @XmlTransient
    public boolean isDVD() {
        return this.formatType.equals(TYPE_DVD);
    }

    @XmlTransient
    public boolean isFile() {
        return this.formatType.equals(TYPE_FILE);
    }

    @XmlTransient
    public boolean isHD() {
        // Depreciated this check in favour of the width check
        // return this.videoType.equals(TYPE_VIDEO_HD) || videoOutput.indexOf("720") != -1 || videoOutput.indexOf("1080") != -1;
        return (getWidth() >= highdef720);
    }

    @XmlTransient
    public boolean isHD1080() {
        return (getWidth() >= highdef1080);
    }

    @XmlTransient
    public boolean is3D() {
        return (getVideoSource().contains("3D"));
    }

    public void setBaseName(String baseName) {
        this.baseName = validateString(baseName, this.baseName);
    }

    public void setBaseFilename(String baseFilename) {
        this.baseFilename = validateString(baseFilename, this.baseFilename);
    }

    public void addDidYouKnow(String fact) {
        if (fact != null && !didYouKnow.contains(fact)) {
            setDirty(DirtyFlag.INFO);
            didYouKnow.add(fact);
        }
    }

    public void setDidYouKnow(List<String> facts) {
        if (facts != null && !facts.isEmpty()) {
            didYouKnow = facts;
            setDirty(DirtyFlag.INFO);
        }
    }

    public void clearDidYouKnow() {
        didYouKnow.clear();
        setDirty(DirtyFlag.INFO);
    }

    public void clearAwards() {
        awards.clear();
        setDirty(DirtyFlag.INFO);
    }

    public boolean addActor(String actor, String source) {
        Boolean added = Boolean.FALSE;
        if (StringTools.isValidString(actor) && (cast.size() < MAX_COUNT_ACTOR)) {
            added = cast.add(actor.trim());
            if (added) {
                setDirty(DirtyFlag.INFO);
                setOverrideSource(OverrideFlag.ACTORS, source);
            }
        }
        return added;
    }

    public boolean addActor(String actorKey, String actorName, String character, String actorUrl, String doublage,
            String source) {
        boolean added = Boolean.FALSE;

        if (actorName != null) {
            String name = actorName;
            if (actorName.indexOf(':') > -1) {
                String[] names = actorName.split(":");
                if (StringTools.isValidString(names[1])) {
                    name = names[1];
                } else if (StringTools.isValidString(names[0])) {
                    name = names[0];
                }
            }

            name = name.trim();
            boolean found = Boolean.FALSE;
            for (Filmography p : people) {
                if (p.getName().equalsIgnoreCase(name) && p.getDepartment().equals(Filmography.DEPT_ACTORS)) {
                    found = Boolean.TRUE;
                    break;
                }
            }

            if (!found) {
                added = addPerson(actorKey, actorName, actorUrl, StringUtils.capitalize(Filmography.JOB_ACTOR),
                        character, doublage, source);
                if (added) {
                    setOverrideSource(OverrideFlag.PEOPLE_ACTORS, source);
                }
            }
        }
        return added;
    }

    public void setCast(Collection<String> cast, String source) {
        if (cast != null && !cast.isEmpty()) {
            clearCast();
            for (String actor : cast) {
                addActor(actor, source);
            }
        }
    }

    public void setPeopleCast(Collection<String> cast, String source) {
        if ((MAX_COUNT_ACTOR > 0) && (cast != null) && !cast.isEmpty()) {
            clearPeopleCast();
            int count = 0;
            for (String actor : cast) {
                if (addActor(Movie.UNKNOWN, actor, Movie.UNKNOWN, Movie.UNKNOWN, Movie.UNKNOWN, source)) {
                    count++;
                    if (count == MAX_COUNT_ACTOR) {
                        break;
                    }
                }
            }
        }
    }

    public void clearCast() {
        if (!cast.isEmpty()) {
            setDirty(DirtyFlag.INFO);
            cast.clear();
        }
    }

    public void clearPeopleCast() {
        if (!people.isEmpty()) {
            Collection<Filmography> pList = new ArrayList<>();
            for (Filmography p : people) {
                if (p.getDepartment().equals(Filmography.DEPT_ACTORS)) {
                    pList.add(p);
                }
            }
            for (Filmography p : pList) {
                removePerson(p);
            }
        }
    }

    public boolean addWriter(String writer, String source) {
        boolean added = Boolean.FALSE;
        if (StringTools.isValidString(writer) && (writers.size() < MAX_COUNT_WRITER)) {
            added = writers.add(writer.trim());
            if (added) {
                setDirty(DirtyFlag.INFO);
                setOverrideSource(OverrideFlag.WRITERS, source);
            }
        }
        return added;
    }

    public boolean addWriter(String writerKey, String name, String writerUrl, String source) {
        boolean added = Boolean.FALSE;

        if (name != null) {
            String writerName = name;
            if (name.indexOf(':') > -1) {
                String[] names = name.split(":");
                if (StringTools.isValidString(names[1])) {
                    writerName = names[1];
                } else if (StringTools.isValidString(names[0])) {
                    writerName = names[0];
                }
            }
            writerName = writerName.trim();

            boolean found = Boolean.FALSE;
            for (Filmography p : people) {
                if (p.getName().equalsIgnoreCase(writerName)
                        && p.getDepartment().equals(Filmography.DEPT_WRITING)) {
                    found = Boolean.TRUE;
                    break;
                }
            }

            if (!found) {
                added = addPerson(writerKey, name, writerUrl, "Writer", source);
                if (added) {
                    setOverrideSource(OverrideFlag.PEOPLE_WRITERS, source);
                }
            }
        }
        return added;
    }

    public void setWriters(Collection<String> writers, String source) {
        if (writers != null && !writers.isEmpty()) {
            clearWriters();
            for (String writer : writers) {
                addWriter(writer, source);
            }
        }
    }

    public void setPeopleWriters(Collection<String> writers, String source) {
        if ((MAX_COUNT_WRITER > 0) && (writers != null) && !writers.isEmpty()) {
            clearPeopleWriters();
            int count = 0;
            for (String writer : writers) {
                if (addWriter(Movie.UNKNOWN, writer, Movie.UNKNOWN, source)) {
                    count++;
                    if (count == MAX_COUNT_WRITER) {
                        break;
                    }
                }
            }
        }
    }

    public void clearWriters() {
        if (!writers.isEmpty()) {
            setDirty(DirtyFlag.INFO);
            writers.clear();
        }
    }

    public void clearPeopleWriters() {
        if (!people.isEmpty()) {
            Collection<Filmography> pList = new ArrayList<>();
            for (Filmography p : people) {
                if (p.getDepartment().equals(Filmography.DEPT_WRITING)) {
                    pList.add(p);
                }
            }
            for (Filmography p : pList) {
                removePerson(p);
            }
        }
    }

    public void setCompany(String company, String source) {
        if (StringTools.isValidString(company)) {
            if (!company.equalsIgnoreCase(this.company)) {
                setDirty(DirtyFlag.INFO);
                this.company = company;
            }
            setOverrideSource(OverrideFlag.COMPANY, source);
        }
    }

    public void setContainer(String container, String source) {
        if (StringTools.isValidString(container)) {
            if (!container.equalsIgnoreCase(this.container)) {
                setDirty(DirtyFlag.INFO);
                this.container = container;
            }
            setOverrideSource(OverrideFlag.CONTAINER, source);
        }
    }

    public void addCountry(final String country, String source) {
        if (StringTools.isValidString(country)) {
            String tmpCountry;
            // Shorten country to USA
            if ("United States of America".equalsIgnoreCase(country)) {
                tmpCountry = "USA";
            } else {
                tmpCountry = country;
            }

            if (this.countries.add(tmpCountry)) {
                // country added
                setDirty(DirtyFlag.INFO);
            }
            setOverrideSource(OverrideFlag.COUNTRY, source);
        }
    }

    public void setCountries(final Collection<String> countries, String source) {
        if (countries != null && !countries.isEmpty()) {
            this.countries.clear();
            for (String country : countries) {
                addCountry(country, source);
            }
        }
    }

    public void setCountries(final String countries, String source) {
        if (StringTools.isValidString(countries)) {
            this.countries.clear();
            for (String country : countries.split("/")) {
                addCountry(country.trim(), source);
            }
        }
    }

    /**
     * Get just one director from the collection
     *
     * @return
     */
    public String getDirector() {
        if (directors != null && !directors.isEmpty()) {
            return directors.iterator().next();
        }
        return UNKNOWN;
    }

    public Collection<String> getDirectors() {
        return directors;
    }

    public void setDirector(String director, String source) {
        if (StringTools.isValidString(director)) {
            clearDirectors();
            addDirector(director, source);
        }
    }

    public void setDirectors(Collection<String> directors, String source) {
        if (directors != null && !directors.isEmpty()) {
            clearDirectors();
            for (String director : directors) {
                addDirector(director, source);
            }
        }
    }

    public void setPeopleDirectors(Collection<String> directors, String source) {
        if ((MAX_COUNT_DIRECTOR > 0) && (directors != null) && !directors.isEmpty()) {
            clearPeopleDirectors();
            int count = 0;
            for (String director : directors) {
                if (addDirector(Movie.UNKNOWN, director, Movie.UNKNOWN, source)) {
                    count++;
                    if (count == MAX_COUNT_DIRECTOR) {
                        break;
                    }
                }
            }
        }
    }

    public void clearDirectors() {
        if (!directors.isEmpty()) {
            setDirty(DirtyFlag.INFO);
            directors.clear();
        }
    }

    public void clearPeopleDirectors() {
        if (!people.isEmpty()) {
            Collection<Filmography> pList = new ArrayList<>();
            for (Filmography p : people) {
                if (p.getDepartment().equals(Filmography.DEPT_DIRECTING)) {
                    pList.add(p);
                }
            }
            for (Filmography p : pList) {
                removePerson(p);
            }
        }
    }

    public boolean addDirector(String director, String source) {
        boolean added = Boolean.FALSE;
        if (StringTools.isValidString(director) && (directors.size() < MAX_COUNT_DIRECTOR)) {
            added = directors.add(director.trim());
            if (added) {
                setDirty(DirtyFlag.INFO);
                setOverrideSource(OverrideFlag.DIRECTORS, source);
            }
        }
        return added;
    }

    public boolean addDirector(String key, String name, String URL, String source) {
        boolean added = Boolean.FALSE;

        if (name != null) {
            String directorName = name;
            if (name.indexOf(':') > -1) {
                String[] names = name.split(":");
                if (StringTools.isValidString(names[1])) {
                    directorName = names[1];
                } else if (StringTools.isValidString(names[0])) {
                    directorName = names[0];
                }
            }
            directorName = directorName.trim();

            boolean found = Boolean.FALSE;
            for (Filmography p : people) {
                if (p.getName().equalsIgnoreCase(directorName)
                        && p.getDepartment().equals(Filmography.DEPT_DIRECTING)) {
                    found = Boolean.TRUE;
                    break;
                }
            }

            if (!found) {
                added = addPerson(key, name, URL, "Director", source);
                if (added) {
                    setOverrideSource(OverrideFlag.PEOPLE_DIRECTORS, source);
                }
            }
        }

        return added;
    }

    public void setFirst(String first) {
        this.first = validateString(first, this.first);
    }

    public void setFps(float fps, String source) {
        // Prevent wrong result caused by floating point rounding by allowing difference of 0.1 fpsS
        if (Math.abs(fps - this.fps) > 0.1) {
            setDirty(DirtyFlag.INFO);
            this.fps = fps;
        }
        setOverrideSource(OverrideFlag.FPS, source);
    }

    public void setGenres(Collection<String> genres, String source) {
        if (!extra && (genres != null) && (!genres.isEmpty())) {
            this.genres.clear();
            for (String genre : genres) {
                if (StringTools.isValidString(genre) && !GENRE_SKIP_LIST.contains(genre.toLowerCase())) {
                    this.genres.add(genre);
                }
            }
            setDirty(DirtyFlag.INFO);
            this.setOverrideSource(OverrideFlag.GENRES, source);
        }
    }

    public void setSets(Map<String, Integer> sets) {
        setDirty(DirtyFlag.INFO);
        this.sets = sets;
    }

    public Map<String, Integer> getSets() {
        return sets;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.Identifiable#setId(java.lang.String,
     * java.lang.String)
     */
    @Override
    public void setId(final String key, final String id) {
        String tmpId = StringUtils.trim(id); // Clean the ID
        if (StringTools.isValidString(key) && StringTools.isValidString(tmpId)
                && !tmpId.equalsIgnoreCase(this.getId(key))) {
            setDirty(DirtyFlag.INFO);
            this.idMap.put(key, tmpId);
        }
    }

    public void setId(String key, int id) {
        setId(key, Integer.toString(id));
    }

    public void setGross(String country, String value) {
        if (StringTools.isValidString(country) && StringTools.isValidString(value)
                && !value.equalsIgnoreCase(this.getGross(country))) {
            setDirty(DirtyFlag.INFO);
            this.gross.put(country, value);
        }
    }

    public void setGross(Map<String, String> gross) {
        if (gross != null) {
            this.gross = gross;
        }
    }

    public void setOpenWeek(String country, String value) {
        if (StringTools.isValidString(country) && StringTools.isValidString(value)
                && !value.equalsIgnoreCase(this.getOpenWeek(country))) {
            setDirty(DirtyFlag.INFO);
            this.openweek.put(country, value);
        }
    }

    public void setOpenWeek(Map<String, String> openweek) {
        if (openweek != null) {
            this.openweek = openweek;
        }
    }

    public void setOverrideSource(OverrideFlag flag, String source) {
        if (StringUtils.isBlank(source)) {
            this.overrideSources.put(flag, UNKNOWN);
        } else {
            this.overrideSources.put(flag, source.toUpperCase());
        }
    }

    public void setLanguage(String language, String source) {
        if (StringTools.isValidString(language)) {
            if (!language.equalsIgnoreCase(this.language)) {
                setDirty(DirtyFlag.INFO);
                this.language = language;
            }
            setOverrideSource(OverrideFlag.LANGUAGE, source);
        }
    }

    public void setCertification(String certification, String source) {
        if (StringTools.isValidString(certification)) {
            if (!certification.equalsIgnoreCase(this.certification)) {
                setDirty(DirtyFlag.INFO);
                this.certification = certification;
            }
            setOverrideSource(OverrideFlag.CERTIFICATION, source);
        }
    }

    public void setLast(String last) {
        this.last = validateString(last, this.last);
    }

    public void setMovieFiles(Collection<MovieFile> movieFiles) {
        setDirty(DirtyFlag.INFO);
        this.movieFiles = movieFiles;
    }

    public void setExtraFiles(Collection<ExtraFile> extraFiles) {
        setDirty(DirtyFlag.INFO);
        this.extraFiles = extraFiles;
    }

    public void setAwards(Collection<AwardEvent> awards) {
        setDirty(DirtyFlag.INFO);
        this.awards = awards;
    }

    public void setPeople(Collection<Filmography> people) {
        setDirty(DirtyFlag.INFO);
        this.people = people;
    }

    public void setNext(String next) {
        this.next = validateString(next, this.next);
    }

    /**
     * Set the movie plot.
     *
     * Will replace non-standard quotes and "&amp;" as needed
     *
     * @param plot
     * @param source
     */
    public void setPlot(String plot, String source) {
        this.setPlot(plot, source, Boolean.TRUE);
    }

    /**
     * Set the movie plot.
     *
     * Will replace non-standard quotes and "&amp;" as needed
     *
     * @param plot
     * @param source
     * @param trimToLength
     */
    public void setPlot(String plot, String source, boolean trimToLength) {
        if (StringTools.isValidString(plot)) {
            String tmpPlot = StringUtils.replace(StringTools.replaceQuotes(plot), "&amp;", "&");
            if (trimToLength) {
                tmpPlot = StringTools.trimToLength(tmpPlot, MAX_LENGTH_PLOT);
            }

            if (!tmpPlot.equalsIgnoreCase(this.plot)) {
                setDirty(DirtyFlag.INFO);
                this.plot = tmpPlot;
            }
            setOverrideSource(OverrideFlag.PLOT, source);
        }
    }

    public String getOutline() {
        return outline;
    }

    /**
     * Set the movie outline.
     *
     * Will replace non-standard quotes and "&amp;" as needed
     *
     * @param outline
     * @param source
     */
    public void setOutline(String outline, String source) {
        this.setOutline(outline, source, Boolean.TRUE);
    }

    /**
     * Set the movie outline.
     *
     * Will replace non-standard quotes and "&amp;" as needed
     *
     * @param outline
     * @param source
     * @param trimToLength
     */
    public void setOutline(String outline, String source, boolean trimToLength) {
        if (StringTools.isValidString(outline)) {
            String tmpOutline = StringUtils.replace(StringTools.replaceQuotes(outline), "&amp;", "&");
            if (trimToLength) {
                tmpOutline = StringTools.trimToLength(tmpOutline, MAX_LENGTH_OUTLINE);
            }

            if (!tmpOutline.equalsIgnoreCase(this.outline)) {
                setDirty(DirtyFlag.INFO);
                this.outline = tmpOutline;
            }
            setOverrideSource(OverrideFlag.OUTLINE, source);
        }
    }

    public void setPrevious(String previous) {
        this.previous = validateString(previous, this.previous);
    }

    public int getRating() {
        if (ratings == null || ratings.isEmpty()) {
            return -1;
        }

        for (String site : ratingSource) {
            if ("average".equalsIgnoreCase(site)) {
                // Return the average of the ratings
                int rating = 0;
                int count = 0;

                for (String ratingSite : ratings.keySet()) {
                    if (!ratingIgnore.isEmpty()) {
                        if (ratingIgnore.contains(ratingSite)) {
                            continue;
                        }

                        boolean found = Boolean.FALSE;
                        for (String ignoreName : ratingIgnore) {
                            if (ratingSite.indexOf(ignoreName) == 0) {
                                found = Boolean.TRUE;
                                break;
                            }
                        }
                        if (found) {
                            continue;
                        }
                    }
                    rating += ratings.get(ratingSite);
                    count++;
                }

                return (count > 0 ? (rating / count) : 0);
            }

            if (ratings.containsKey(site)) {
                return ratings.get(site);
            }
        }

        // No ratings found, so return -1
        return -1;
    }

    public int getRating(String site) {
        if (ratings.containsKey(site)) {
            return ratings.get(site);
        }
        return -1;
    }

    public Map<String, Integer> getRatings() {
        return ratings;
    }

    public void addRating(String site, int rating) {
        if (StringTools.isValidString(site)) {
            if (ratings.containsKey(site)) {
                if (ratings.get(site) != rating) {
                    ratings.remove(site);
                    ratings.put(site, rating);
                    setDirty(DirtyFlag.INFO);
                }
            } else {
                ratings.put(site, rating);
            }
        }
    }

    public void setRatings(Map<String, Integer> ratings) {
        if (ratings != null && !ratings.isEmpty()) {
            this.ratings = ratings;
        }
    }

    public void setReleaseDate(final String releaseDate, String source) {
        if (StringTools.isNotValidString(releaseDate)) {
            // nothing to do
            return;
        }

        String parseDate = DateTimeTools.parseDateToString(releaseDate);

        if (StringTools.isValidString(parseDate)) {
            if (!parseDate.equalsIgnoreCase(this.releaseDate)) {
                setDirty(DirtyFlag.INFO);
                this.releaseDate = parseDate;
            }
            setOverrideSource(OverrideFlag.RELEASEDATE, source);
        }
    }

    public void setResolution(String resolution, String source) {
        if (StringTools.isValidString(resolution)) {
            if (!resolution.equalsIgnoreCase(this.resolution)) {
                setDirty(DirtyFlag.INFO);
                this.resolution = resolution;
            }
            setOverrideSource(OverrideFlag.RESOLUTION, source);
        }
    }

    public void setResolution(String width, String height, String source) {
        if (StringTools.isValidString(width) && StringTools.isValidString(height)) {
            setResolution(width + "x" + height, source);
        }
    }

    public void setRuntime(String runtime, String source) {
        if (StringTools.isValidString(runtime)) {
            if (!runtime.equalsIgnoreCase(this.runtime)) {
                setDirty(DirtyFlag.INFO);
                // Escape the first "0" AlloCine gives sometimes
                if (runtime.startsWith("0")) {
                    this.runtime = runtime.substring(1).trim();
                } else {
                    this.runtime = runtime.trim();
                }
            }
            setOverrideSource(OverrideFlag.RUNTIME, source);
        }
    }

    public void setSubtitles(String subtitles) {
        this.subtitles = validateString(subtitles, this.subtitles);
    }

    public void setTitle(String title, String source) {
        if (StringTools.isValidString(title)) {
            if (!title.equals(this.title)) {
                setDirty(DirtyFlag.INFO);
                this.title = title;
            }
            setOverrideSource(OverrideFlag.TITLE, source);

            // If we don't have a original title, then use the title
            if (StringTools.isNotValidString(originalTitle)) {
                setOriginalTitle(title, source);
            }
        }
    }

    public void setTitleSort(final String title) {
        if (title.equals(titleSort)) {
            return;
        }

        String newTitle;
        if (StringUtils.isBlank(title)) {
            newTitle = UNKNOWN;
        } else {
            newTitle = title;
        }

        if (!newTitle.equals(this.titleSort)) {
            int idx = 0;
            while (idx < newTitle.length() && !Character.isLetterOrDigit(newTitle.charAt(idx))) {
                idx++;
            }

            // Issue 1908: Replace all non-standard characters in the title sort
            this.titleSort = StringTools.stringMapReplacement(newTitle.substring(idx));
            setDirty(DirtyFlag.INFO);
        }
    }

    public void setOriginalTitle(String originalTitle, String source) {
        if (StringTools.isValidString(originalTitle)) {
            if (!originalTitle.equals(this.originalTitle)) {
                setDirty(DirtyFlag.INFO);
                this.originalTitle = originalTitle;
            }
            setOverrideSource(OverrideFlag.ORIGINALTITLE, source);
        }
    }

    /**
     * Get the video codec. You should use the getCodec methods instead
     *
     * @return
     * @deprecated
     */
    @Deprecated
    public String getVideoCodec() {
        for (Codec videoCodec : codecs) {
            if (videoCodec.getCodecType() == CodecType.VIDEO) {
                if (StringTools.isValidString(videoCodec.getCodecIdHint())) {
                    return videoCodec.getCodecIdHint();
                }
                if (StringTools.isValidString(videoCodec.getCodec())) {
                    return videoCodec.getCodec();
                }
                if (StringTools.isValidString(videoCodec.getCodecFormat())) {
                    return videoCodec.getCodecFormat();
                }
                if (StringTools.isValidString(videoCodec.getCodecId())) {
                    return videoCodec.getCodecId();
                }
            }
        }

        return UNKNOWN;
    }

    public void setVideoOutput(String videoOutput, String source) {
        if (StringTools.isValidString(videoOutput)) {
            if (!videoOutput.equalsIgnoreCase(this.videoOutput)) {
                setDirty(DirtyFlag.INFO);
                this.videoOutput = videoOutput;
            }
            setOverrideSource(OverrideFlag.VIDEOOUTPUT, source);
        }
    }

    public void setVideoSource(String videoSource, String source) {
        if (StringTools.isValidString(videoSource)) {
            if (!videoSource.equalsIgnoreCase(this.videoSource)) {
                setDirty(DirtyFlag.INFO);
                this.videoSource = videoSource;
            }
            setOverrideSource(OverrideFlag.VIDEOSOURCE, source);
        }
    }

    public void setYear(String year, String source) {
        if (StringTools.isValidString(year)) {
            if (!year.equalsIgnoreCase(this.year)) {
                setDirty(DirtyFlag.INFO);
                this.year = year;
            }
            setOverrideSource(OverrideFlag.YEAR, source);
        }
    }

    public void setBudget(String budget) {
        this.budget = validateString(budget, this.budget);
    }

    public String getQuote() {
        return quote;
    }

    public void setQuote(String quote, String source) {
        if (StringTools.isValidString(quote)) {
            if (!quote.equalsIgnoreCase(this.quote)) {
                setDirty(DirtyFlag.INFO);
                this.quote = quote;
            }
            setOverrideSource(OverrideFlag.QUOTE, source);
        }
    }

    /**
     * Validate the testString to ensure it is correct before setting the Dirty INFO flag if it is different
     *
     * @param testString
     * @param currentString
     * @return
     */
    private String validateString(String testString, String currentString) {
        String newString;
        if (StringUtils.isBlank(testString)) {
            newString = UNKNOWN;
        } else {
            newString = testString;
        }

        if (!newString.equalsIgnoreCase(currentString)) {
            setDirty(DirtyFlag.INFO);
        }
        return newString;
    }

    @XmlTransient
    public File getFile() {
        return file;
    }

    public void setFile(File file) {
        this.file = file;
    }

    @XmlTransient
    public File getContainerFile() {
        return containerFile;
    }

    public void setContainerFile(File containerFile) {
        this.containerFile = containerFile;
    }

    public long getLastModifiedTimestamp() {
        if (!isSetMaster()) {
            synchronized (this) {
                long lastModifiedTimeStamp = 0L;
                for (MovieFile mf : getMovieFiles()) {
                    lastModifiedTimeStamp = Math.max(lastModifiedTimeStamp, mf.getLastModified());
                }
                //make sure the fileDate is correct too
                addFileDate(new Date(lastModifiedTimeStamp));
                return lastModifiedTimeStamp;
            }
        }

        // Sets don't hold the time/date of their files, so just return the time of the file
        return getFileDate().getTime();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(idMap).append(title)
                .append(titleSort).append(year).append(releaseDate).append(ratings).append(top250).append(posterURL)
                .append(bannerURL).append(fanartURL).append(plot).append(outline).append(directors)
                .append(countries).append(company).append(runtime).append(getSeason()).append(language)
                .append(subtitles).append(container).append(codecs).append(resolution).append(videoSource)
                .append(videoOutput).append(fps).append(certification).append(cast).append(writers).append(genres)
                .append(libraryDescription).append(prebuf).toString();
    }

    /**
     * Sets the "extra" flag to mark this file as an extra. Will trigger the "dirty" setting too
     *
     * @param extra Boolean flag, true=extra file, false=normal file
     */
    public void setExtra(boolean extra) {
        setDirty(DirtyFlag.INFO);
        this.extra = extra;
        if (extra) {
            genres.clear();
        }
    }

    /**
     * This function will return true if the movie can have trailers.
     *
     * @return
     */
    public boolean canHaveTrailers() {
        if (isExtra() || getMovieType().equals(Movie.TYPE_TVSHOW)) {
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    public void setTrailerExchange(Boolean trailerExchange) {
        if (this.trailerExchange != trailerExchange) {
            setDirty(DirtyFlag.INFO);
            this.trailerExchange = trailerExchange;
        }
    }

    /**
     * Set the date of the last trailers scan
     *
     * @param lastScan date of the last trailers scan
     */
    public void setTrailerLastScan(String lastScan) {
        try {
            if (StringTools.isValidString(lastScan)) {
                setTrailerLastScan(DateTime.parse(lastScan).toMillis());
            } else {
                setTrailerLastScan(0);
            }
        } catch (IllegalArgumentException ex) {
            LOG.debug("Unable to parse TrailerLastScan date from '{}', Error: {}", lastScan, ex.getMessage());
            setTrailerLastScan(0);
        }
    }

    /**
     * Set the date of the last trailers scan
     *
     * @param lastScan date of the last trailers scan (milliseconds offset from the Epoch)
     */
    public void setTrailerLastScan(long lastScan) {
        if (lastScan != this.trailerLastScan) {
            setDirty(DirtyFlag.INFO);
            this.trailerLastScan = lastScan;
        }
    }

    /**
     * Get the date of the last trailers scan
     *
     * @return the date of the last trailers scan (milliseconds offset from the Epoch)
     */
    public long getTrailerLastScan() {
        return trailerLastScan;
    }

    /**
     * @return Boolean flag indicating if this file is an extra
     */
    @XmlAttribute(name = "isExtra")
    public boolean isExtra() {
        return extra;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#isTrailerExchange()
     */
    @XmlJavaTypeAdapter(BooleanYesNoAdapter.class)
    @Override
    public Boolean isTrailerExchange() {
        return trailerExchange;
    }

    public void setMovieType(String movieType) {
        this.movieType = validateString(movieType, this.movieType);
    }

    public String getMovieType() {
        return this.movieType;
    }

    public void setFormatType(String formatType) {
        this.formatType = validateString(formatType, this.formatType);
    }

    public String getFormatType() {
        return this.formatType;
    }

    public void setVideoType(String videoType) {
        this.videoType = validateString(videoType, this.videoType);
    }

    public String getVideoType() {
        return this.videoType;
    }

    public String getLibraryPath() {
        return libraryPath;
    }

    public void setLibraryPath(String libraryPath) {
        this.libraryPath = libraryPath;
    }

    @Deprecated
    public String getAudioChannels() {
        StringBuilder sb = new StringBuilder();
        boolean firstChannel = Boolean.TRUE;

        for (Codec codec : codecs) {
            if (codec.getCodecType().equals(CodecType.AUDIO)) {
                if (firstChannel) {
                    firstChannel = Boolean.FALSE;
                } else {
                    sb.append(SPACE_SLASH_SPACE);
                }
                sb.append(codec.getCodecChannels());
            }
        }
        return sb.toString();
    }

    public int getTop250() {
        return top250;
    }

    public void setTop250(String top250, String source) {
        setTop250(NumberUtils.toInt(top250), source);
    }

    public void setTop250(int top250, String source) {
        if (top250 > 0) {
            if (top250 != this.top250) {
                setDirty(DirtyFlag.INFO);
                this.top250 = top250;
            }
            setOverrideSource(OverrideFlag.TOP250, source);
        }
    }

    public String getLibraryDescription() {
        return libraryDescription;
    }

    public void setLibraryDescription(String libraryDescription) {
        this.libraryDescription = validateString(libraryDescription, this.libraryDescription);
    }

    public long getPrebuf() {
        return prebuf;
    }

    public void setPrebuf(long prebuf) {
        this.prebuf = prebuf;
    }

    @XmlTransient
    public boolean isScrapeLibrary() {
        return scrapeLibrary;
    }

    public void setScrapeLibrary(boolean scrapeLibrary) {
        this.scrapeLibrary = scrapeLibrary;
    }

    public static void addSortIgnorePrefixes(String prefix) {
        SORT_IGNORE_PREFIXES.add(prefix);
    }

    /**
     * Take the passed DTO and update the movie information
     *
     * @param dto
     */
    public void mergeFileNameDTO(MovieFileNameDTO dto) {
        setExtra(dto.isExtra());

        if (OverrideTools.checkOverwriteTitle(this, SOURCE_FILENAME)) {
            setTitle(dto.getTitle(), SOURCE_FILENAME);
        }

        Codec tempCodec;
        if (StringTools.isValidString(dto.getAudioCodec())) {
            tempCodec = new Codec(CodecType.AUDIO);
            tempCodec.setCodec(dto.getAudioCodec());
            tempCodec.setCodecSource(CodecSource.FILENAME);
            addCodec(tempCodec);
        }

        if (StringTools.isValidString(dto.getVideoCodec())) {
            tempCodec = new Codec(CodecType.VIDEO);
            tempCodec.setCodec(dto.getVideoCodec());
            tempCodec.setCodecSource(CodecSource.FILENAME);
            addCodec(tempCodec);
        }

        if (OverrideTools.checkOverwriteVideoSource(this, SOURCE_FILENAME)) {
            setVideoSource(dto.getVideoSource(), SOURCE_FILENAME);
        }

        if (OverrideTools.checkOverwriteContainer(this, SOURCE_FILENAME)) {
            setContainer(dto.getContainer(), SOURCE_FILENAME);
        }

        if ((dto.getFps() > 0) && OverrideTools.checkOverwriteFPS(this, SOURCE_FILENAME)) {
            setFps(dto.getFps(), SOURCE_FILENAME);
        }

        setMovieIds(dto.getMovieIds());

        if (dto.getSeason() > -1) {
            // Mark the movie as a TV Show
            setMovieType(Movie.TYPE_TVSHOW);
        }

        for (MovieFileNameDTO.SetDTO set : dto.getSets()) {
            addSet(set.getTitle(), set.getIndex() >= 0 ? set.getIndex() : null);
        }

        if (OverrideTools.checkOverwriteYear(this, SOURCE_FILENAME)) {
            setYear(dto.getYear() > 0 ? String.valueOf(dto.getYear()) : null, SOURCE_FILENAME);
        }

        if (!dto.getLanguages().isEmpty() && OverrideTools.checkOverwriteLanguage(this, SOURCE_FILENAME)) {
            // TODO more languages?
            setLanguage(dto.getLanguages().get(0), SOURCE_FILENAME);
        }

        // set video type
        if (dto.getHdResolution() != null) {
            setVideoType(TYPE_VIDEO_HD);
        }

        // set video output
        if (OverrideTools.checkOverwriteVideoOutput(this, SOURCE_FILENAME)) {

            String videoOut = "";
            if (dto.getHdResolution() != null) {

                if (StringTools.isValidString(videoOutput)) {
                    videoOut = videoOutput;
                }

                switch (dto.getFps()) {
                case 23:
                    videoOut = "1080p 23.976Hz";
                    break;
                case 24:
                    videoOut = "1080p 24Hz";
                    break;
                case 25:
                    videoOut = "1080p 25Hz";
                    break;
                case 29:
                    videoOut = "1080p 29.97Hz";
                    break;
                case 30:
                    videoOut = "1080p 30Hz";
                    break;
                case 50:
                    if (StringUtils.isNotBlank(videoOut)) {
                        videoOut += " ";
                    }
                    videoOut += "50Hz";
                    break;
                case 59:
                    videoOut = "1080p 59.94Hz";
                    break;
                case 60:
                    if (StringUtils.isNotBlank(videoOut)) {
                        videoOut += " ";
                    }
                    videoOut += "60Hz";
                    break;
                default:
                    if (StringUtils.isBlank(videoOut)) {
                        videoOut = Movie.UNKNOWN;
                    } else {
                        videoOut += " 60Hz";
                    }
                    break;
                }
            } else {
                switch (dto.getFps()) {
                case 23:
                    videoOut = "23p";
                    break;
                case 24:
                    videoOut = "24p";
                    break;
                case 29:
                case 30:
                case 60:
                    videoOut = "NTSC";
                    break;
                case 25:
                case 49:
                case 50:
                    videoOut = "PAL";
                    break;
                default:
                    videoOut = Movie.UNKNOWN;
                    break;
                }
            }

            // set video output
            setVideoOutput(videoOut, SOURCE_FILENAME);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.moviejukebox.model.IMovieBasicInformation#isSetMaster()
     */
    @XmlAttribute(name = "isSet")
    @Override
    public boolean isSetMaster() {
        return setMaster;
    }

    public void setSetMaster(boolean setMaster) {
        this.setMaster = setMaster;
    }

    @XmlAttribute(name = "setSize")
    public int getSetSize() {
        return this.setSize;
    }

    public void setSetSize(final int size) {
        this.setSize = size;
    }

    /**
     * Store the latest filedate for a set of movie files. Synchronized so that the comparisons don't overlap
     *
     * @param fileDate
     */
    public synchronized void addFileDate(Date fileDate) {
        if (fileDate == null) {
            return;
        }

        if (this.fileDate == null) {
            this.fileDate = fileDate;
        } else if (fileDate.after(this.fileDate)) {
            this.fileDate = fileDate;
        }
    }

    /**
     * Overwrite the file date
     *
     * @param fileDate
     */
    public synchronized void setFileDate(Date fileDate) {
        this.fileDate = fileDate;
    }

    public Date getFileDate() {
        return fileDate;
    }

    public void setFileSize(long fileSize) {
        this.fileSize = this.fileSize + fileSize;
    }

    public long getFileSize() {
        return fileSize;
    }

    public String getFileSizeString() {
        return StringTools.formatFileSize(fileSize);
    }

    public void setAspectRatio(String aspectRatio, String source) {
        if (StringTools.isValidString(aspectRatio)) {
            if (!aspectRatio.equalsIgnoreCase(this.aspect)) {
                setDirty(DirtyFlag.INFO);
                this.aspect = aspectRatio;
            }
            setOverrideSource(OverrideFlag.ASPECTRATIO, source);
        }
    }

    public String getAspectRatio() {
        return aspect;
    }

    // ***** All the graphics methods go here *****
    // ***** Posters
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getPosterURL() {
        return posterURL;
    }

    /**
     * Should be called only from ArtworkScanner. Avoid calling this inside MoviePlugin Also called from MovieNFOScanner
     *
     * @param posterURL
     */
    public void setPosterURL(String posterURL) {
        if (StringTools.isValidString(posterURL)) {
            // If the artwork URL is different, then change it, otheriwse leave alone.
            if (!posterURL.equalsIgnoreCase(this.posterURL)) {
                setDirty(DirtyFlag.POSTER, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.posterURL = posterURL;
            }
        } else {
            this.posterURL = UNKNOWN;
        }
    }

    @XmlElement(name = "posterFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getPosterFilename() {
        return posterFilename;
    }

    public void setPosterFilename(String posterFilename) {
        if (StringTools.isValidString(posterFilename)) {
            // create the directory hash if needed
            if (DIR_HASH) {
                this.posterFilename = FileTools.createDirHash(posterFilename);
            } else {
                this.posterFilename = posterFilename;
            }
        } else {
            this.posterFilename = UNKNOWN;
        }
    }

    @XmlElement(name = "detailPosterFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getDetailPosterFilename() {
        return detailPosterFilename;
    }

    public void setDetailPosterFilename(String detailPosterFilename) {
        if (StringTools.isValidString(detailPosterFilename)) {
            // create the directory hash if needed
            if (DIR_HASH) {
                this.detailPosterFilename = FileTools.createDirHash(detailPosterFilename);
            } else {
                this.detailPosterFilename = detailPosterFilename;
            }
        } else {
            this.detailPosterFilename = UNKNOWN;
        }
    }

    // ***** Thumbnails
    @XmlElement(name = "thumbnail")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getThumbnailFilename() {
        return this.thumbnailFilename;
    }

    public void setThumbnailFilename(String thumbnailFilename) {
        if (StringTools.isValidString(thumbnailFilename)) {
            // create the directory hash if needed
            if (DIR_HASH) {
                this.thumbnailFilename = FileTools.createDirHash(thumbnailFilename);
            } else {
                this.thumbnailFilename = thumbnailFilename;
            }
        } else {
            this.thumbnailFilename = UNKNOWN;
        }
    }

    // ***** Footer
    @XmlElement(name = "footer")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public List<String> getFooterFilename() {
        return this.footerFilename;
    }

    public void setFooterFilename(final String footerFilename, Integer inx) {
        // Can't change the passed parameter
        String ff;
        if (StringUtils.isBlank(footerFilename)) {
            ff = UNKNOWN;
        } else // create the directory hash if needed
        if (DIR_HASH) {
            ff = FileTools.createDirHash(FileTools.makeSafeFilename(footerFilename));
        } else {
            ff = FileTools.makeSafeFilename(footerFilename);
        }

        if (this.footerFilename.size() <= inx) {
            while (this.footerFilename.size() < inx) {
                this.footerFilename.add(UNKNOWN);
            }
            this.footerFilename.add(ff);
        } else {
            this.footerFilename.set(inx, ff);
        }
    }

    // ***** Fanart
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getFanartURL() {
        return fanartURL;
    }

    public void setFanartURL(String fanartURL) {
        if (StringTools.isValidString(fanartURL)) {
            if (!fanartURL.equalsIgnoreCase(this.fanartURL)) {
                setDirty(DirtyFlag.FANART, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);

                this.fanartURL = fanartURL;
            }
        } else {
            this.fanartURL = UNKNOWN;
        }
    }

    @XmlElement(name = "fanartFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getFanartFilename() {
        return fanartFilename;
    }

    public void setFanartFilename(String fanartFilename) {
        if (StringTools.isValidString(fanartFilename)) {
            // create the directory hash if needed
            if (DIR_HASH) {
                this.fanartFilename = FileTools.createDirHash(fanartFilename);
            } else {
                this.fanartFilename = fanartFilename;
            }
        } else {
            this.fanartFilename = UNKNOWN;
        }
    }

    // ***** Banners
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getBannerURL() {
        return bannerURL;
    }

    public void setBannerURL(String bannerURL) {
        if (StringTools.isValidString(bannerURL)) {
            if (!bannerURL.equalsIgnoreCase(this.bannerURL)) {
                setDirty(DirtyFlag.BANNER, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.bannerURL = bannerURL;
            }
        } else {
            this.bannerURL = UNKNOWN;
        }
    }

    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getBannerFilename() {
        return bannerFilename;
    }

    public void setBannerFilename(String bannerFilename) {
        if (StringTools.isValidString(bannerFilename)) {
            // create the directory hash if needed
            if (DIR_HASH) {
                this.bannerFilename = FileTools.createDirHash(bannerFilename);
            } else {
                this.bannerFilename = bannerFilename;
            }
        } else {
            this.bannerFilename = UNKNOWN;
        }
    }

    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getWideBannerFilename() {
        return wideBannerFilename;
    }

    public void setWideBannerFilename(String wideBannerFilename) {
        if (StringTools.isValidString(wideBannerFilename)) {
            // create the directory hash if needed
            if (DIR_HASH) {
                this.wideBannerFilename = FileTools.createDirHash(wideBannerFilename);
            } else {
                this.wideBannerFilename = wideBannerFilename;
            }
        } else {
            this.wideBannerFilename = UNKNOWN;
        }
    }

    // ***** ClearLogo
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getClearLogoURL() {
        return clearLogoURL;
    }

    public void setClearLogoURL(String clearLogoURL) {
        if (StringTools.isValidString(clearLogoURL)) {
            if (!clearLogoURL.equalsIgnoreCase(this.clearLogoURL)) {
                setDirty(DirtyFlag.CLEARLOGO, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.clearLogoURL = clearLogoURL;
            }
        } else {
            this.clearArtURL = UNKNOWN;
        }
    }

    @XmlElement(name = "clearLogoFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getClearLogoFilename() {
        return clearLogoFilename;
    }

    public void setClearLogoFilename(String clearLogoFilename) {
        if (StringTools.isValidString(clearLogoFilename)) {
            this.clearLogoFilename = clearLogoFilename;
        } else {
            this.clearLogoFilename = UNKNOWN;
        }
    }

    // ***** ClearArt
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getClearArtURL() {
        return clearArtURL;
    }

    public void setClearArtURL(String clearArtURL) {
        if (StringTools.isValidString(clearArtURL)) {
            if (!clearArtURL.equalsIgnoreCase(this.clearArtURL)) {
                setDirty(DirtyFlag.CLEARART, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.clearArtURL = clearArtURL;
            }
        } else {
            this.clearArtURL = UNKNOWN;
        }
    }

    @XmlElement(name = "clearArtFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getClearArtFilename() {
        return clearArtFilename;
    }

    public void setClearArtFilename(String clearArtFilename) {
        if (StringTools.isValidString(clearArtFilename)) {
            this.clearArtFilename = clearArtFilename;
        } else {
            this.clearArtFilename = UNKNOWN;
        }
    }

    // ***** TvThumb
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getTvThumbURL() {
        return tvThumbURL;
    }

    public void setTvThumbURL(String tvThumbURL) {
        if (StringTools.isValidString(tvThumbURL)) {
            if (!tvThumbURL.equalsIgnoreCase(this.tvThumbURL)) {
                setDirty(DirtyFlag.TVTHUMB, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.tvThumbURL = tvThumbURL;
            }
        } else {
            this.tvThumbURL = UNKNOWN;
        }
    }

    @XmlElement(name = "tvThumbFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getTvThumbFilename() {
        return tvThumbFilename;
    }

    public void setTvThumbFilename(String tvThumbFilename) {
        if (StringTools.isValidString(tvThumbFilename)) {
            this.tvThumbFilename = tvThumbFilename;
        } else {
            this.tvThumbFilename = UNKNOWN;
        }
    }

    // ***** SeasonThumb
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getSeasonThumbURL() {
        return seasonThumbURL;
    }

    public void setSeasonThumbURL(String seasonThumbURL) {
        if (StringTools.isValidString(seasonThumbURL)) {
            if (!seasonThumbURL.equalsIgnoreCase(this.seasonThumbURL)) {
                setDirty(DirtyFlag.SEASONTHUMB, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.seasonThumbURL = seasonThumbURL;
            }
        } else {
            this.seasonThumbURL = UNKNOWN;
        }
    }

    @XmlElement(name = "seasonThumbFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getSeasonThumbFilename() {
        return seasonThumbFilename;
    }

    public void setSeasonThumbFilename(String seasonThumbFilename) {
        if (StringTools.isValidString(seasonThumbFilename)) {
            this.seasonThumbFilename = seasonThumbFilename;
        } else {
            this.seasonThumbFilename = UNKNOWN;
        }
    }

    // ***** MovieDisc
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getMovieDiscURL() {
        return movieDiscURL;
    }

    public void setMovieDiscURL(String movieDiscURL) {
        if (StringTools.isValidString(movieDiscURL)) {
            if (!movieDiscURL.equalsIgnoreCase(this.movieDiscURL)) {
                setDirty(DirtyFlag.MOVIEDISC, Boolean.TRUE);
                setDirty(DirtyFlag.INFO);
                this.movieDiscURL = movieDiscURL;
            }
        } else {
            this.movieDiscURL = UNKNOWN;
        }
    }

    @XmlElement(name = "movieDiscFile")
    @XmlJavaTypeAdapter(UrlCodecAdapter.class)
    public String getMovieDiscFilename() {
        return movieDiscFilename;
    }

    public void setMovieDiscFilename(String movieDiscFilename) {
        if (StringTools.isValidString(movieDiscFilename)) {
            this.movieDiscFilename = movieDiscFilename;
        } else {
            this.movieDiscFilename = UNKNOWN;
        }
    }

    // ***** END of graphics *****
    public Map<String, String> getIndexes() {
        return indexes;
    }

    public void addIndex(String key, String index) {
        if (key != null && index != null) {
            indexes.put(key, index);
        }
    }

    public void setIndexes(Map<String, String> indexes) {
        this.indexes = new HashMap<>(indexes);
    }

    public String getTagline() {
        return tagline;
    }

    public void setTagline(String tagline, String source) {
        if (StringTools.isValidString(tagline)) {
            if (!tagline.equalsIgnoreCase(this.tagline)) {
                setDirty(DirtyFlag.INFO);
                this.tagline = tagline;
            }
            setOverrideSource(OverrideFlag.TAGLINE, source);
        }
    }

    @XmlElement(name = "isWatched")
    public boolean isWatched() {
        // set to watched if one watched category is true 
        return (watchedNFO || watchedFile);
    }

    @XmlTransient
    public boolean isWatchedNFO() {
        return watchedNFO;
    }

    @XmlTransient
    public boolean isWatchedFile() {
        return watchedFile;
    }

    public void setWatchedNFO(boolean watched) {
        this.watchedNFO = watched;
    }

    public void setWatchedFile(boolean watched) {
        this.watchedFile = watched;
    }

    /**
     * Look at the associated movie files and return the latest date a file was watched
     *
     * @return
     */
    public long getWatchedDate() {
        long returnDate = 0;

        for (MovieFile mf : movieFiles) {
            if (mf.isWatched() && mf.getWatchedDate() > returnDate) {
                returnDate = mf.getWatchedDate();
            }
        }

        return returnDate;
    }

    public String getShowStatus() {
        return showStatus;
    }

    public void setShowStatus(String showStatus) {
        this.showStatus = showStatus;
    }

    public void setMovieScanner(MovieDatabasePlugin movieScanner) {
        if (movieScanner != null) {
            this.movieScanner = movieScanner;
        }
    }

    @XmlTransient
    public MovieDatabasePlugin getMovieScanner() {
        return movieScanner;
    }

    /**
     * Copy the movie
     *
     * @param aMovie
     * @return
     */
    public static Movie newInstance(Movie aMovie) {
        Movie newMovie = new Movie();

        newMovie.baseName = aMovie.baseName;
        newMovie.baseFilename = aMovie.baseFilename;

        newMovie.title = aMovie.title;
        newMovie.titleSort = aMovie.titleSort;
        newMovie.originalTitle = aMovie.originalTitle;
        newMovie.year = aMovie.year;
        newMovie.releaseDate = aMovie.releaseDate;
        newMovie.plot = aMovie.plot;
        newMovie.outline = aMovie.outline;
        newMovie.quote = aMovie.quote;
        newMovie.tagline = aMovie.tagline;
        newMovie.company = aMovie.company;
        newMovie.runtime = aMovie.runtime;
        newMovie.language = aMovie.language;
        newMovie.videoType = aMovie.videoType;
        newMovie.subtitles = aMovie.subtitles;
        newMovie.container = aMovie.container;
        newMovie.resolution = aMovie.resolution;
        newMovie.aspect = aMovie.aspect;
        newMovie.videoSource = aMovie.videoSource;
        newMovie.videoOutput = aMovie.videoOutput;
        newMovie.fps = aMovie.fps;
        newMovie.certification = aMovie.certification;
        newMovie.showStatus = aMovie.showStatus;
        newMovie.scrapeLibrary = aMovie.scrapeLibrary;
        newMovie.extra = aMovie.extra;
        newMovie.trailerExchange = aMovie.trailerExchange;
        newMovie.trailerLastScan = aMovie.trailerLastScan;
        newMovie.libraryPath = aMovie.libraryPath;
        newMovie.movieType = aMovie.movieType;
        newMovie.formatType = aMovie.formatType;
        newMovie.top250 = aMovie.top250;
        newMovie.libraryDescription = aMovie.libraryDescription;
        newMovie.prebuf = aMovie.prebuf;
        newMovie.posterURL = aMovie.posterURL;
        newMovie.posterFilename = aMovie.posterFilename;
        newMovie.detailPosterFilename = aMovie.detailPosterFilename;
        newMovie.thumbnailFilename = aMovie.thumbnailFilename;
        newMovie.fanartURL = aMovie.fanartURL;
        newMovie.fanartFilename = aMovie.fanartFilename;
        newMovie.bannerURL = aMovie.bannerURL;
        newMovie.bannerFilename = aMovie.bannerFilename;
        newMovie.wideBannerFilename = aMovie.wideBannerFilename;
        newMovie.clearArtURL = aMovie.clearArtURL;
        newMovie.clearArtFilename = aMovie.clearArtFilename;
        newMovie.clearLogoURL = aMovie.clearLogoURL;
        newMovie.clearLogoFilename = aMovie.clearLogoFilename;
        newMovie.tvThumbURL = aMovie.tvThumbURL;
        newMovie.tvThumbFilename = aMovie.tvThumbFilename;
        newMovie.seasonThumbURL = aMovie.seasonThumbURL;
        newMovie.seasonThumbFilename = aMovie.seasonThumbFilename;
        newMovie.movieDiscURL = aMovie.movieDiscURL;
        newMovie.movieDiscFilename = aMovie.movieDiscFilename;
        newMovie.fileDate = aMovie.fileDate;
        newMovie.fileSize = aMovie.fileSize;
        newMovie.watchedFile = aMovie.watchedFile;
        newMovie.watchedNFO = aMovie.watchedNFO;
        newMovie.first = aMovie.first;
        newMovie.previous = aMovie.first;
        newMovie.next = aMovie.next;
        newMovie.last = aMovie.last;
        newMovie.file = aMovie.file;
        newMovie.containerFile = aMovie.containerFile;
        newMovie.setMaster = aMovie.setMaster;
        newMovie.setSize = aMovie.setSize;

        newMovie.idMap = new HashMap<>(aMovie.idMap);
        newMovie.countries = new LinkedHashSet<>(aMovie.countries);
        newMovie.ratings = new HashMap<>(aMovie.ratings);
        newMovie.directors = new LinkedHashSet<>(aMovie.directors);
        newMovie.sets = new HashMap<>(aMovie.sets);
        newMovie.genres = new TreeSet<>(aMovie.genres);
        newMovie.cast = new LinkedHashSet<>(aMovie.cast);
        newMovie.writers = new LinkedHashSet<>(aMovie.writers);
        newMovie.awards = new ArrayList<>(aMovie.awards);
        newMovie.people = new ArrayList<>(aMovie.people);
        newMovie.indexes = new HashMap<>(aMovie.indexes);
        newMovie.movieFiles = new TreeSet<>(aMovie.movieFiles);
        newMovie.extraFiles = new TreeSet<>(aMovie.extraFiles);
        newMovie.dirtyFlags = EnumSet.copyOf(aMovie.dirtyFlags);
        newMovie.codecs = new LinkedHashSet<>(aMovie.codecs);
        newMovie.footerFilename = new ArrayList<>(aMovie.footerFilename);
        newMovie.overrideSources = new EnumMap<>(aMovie.overrideSources);

        return newMovie;
    }

    public Set<Codec> getCodecs() {
        return codecs;
    }

    public void setCodecs(Set<Codec> codecs) {
        this.codecs = codecs;
        setDirty(DirtyFlag.INFO);
    }

    /**
     * Add a codec to the video file.
     *
     * @param newCodec
     */
    public void addCodec(final Codec newCodec) {
        // Check to see if the codec already exists
        boolean alreadyExists = Boolean.FALSE;
        // Store the codecs to delete in an array to prevent a concurent modification exception
        List<Codec> codecsToDelete = new ArrayList<>();

        for (Codec existingCodec : codecs) {
            if (existingCodec.getCodecType() != newCodec.getCodecType()) {
                // Codecs are not the same type.
                continue;
            }

            // Checks to see if the Type, Codec, Language and Channels are the same
            if (existingCodec.equals(newCodec)) {
                // Check to see if the codec is better than an existing one
                if (existingCodec.getCodecSource().isBetter(newCodec.getCodecSource())) {
                    // Found an existing codec which is better
                    alreadyExists = Boolean.TRUE;
                } else {
                    // Found an existing codec, but newer is better source
                    codecsToDelete.add(existingCodec);
                }

                // No need to check further codecs
                break;
            }
        }

        // Delete codecs if it is not required
        for (Codec codecToDelete : codecsToDelete) {
            codecs.remove(codecToDelete);
            setDirty(DirtyFlag.INFO);
        }
        codecsToDelete.clear();

        // If the codec does not exist, add it to the list
        if (!alreadyExists) {
            this.codecs.add(newCodec);
            setDirty(DirtyFlag.INFO);
        }
    }

    public static List<String> getSortIgnorePrefixes() {
        return SORT_IGNORE_PREFIXES;
    }

    /**
     * Check to see if any of the movie's ID was set to 0 or -1, in which case the user has disabled online scanning for this movie.
     *
     * @return
     */
    public boolean skipOnlineScan() {
        boolean ignore = false;
        for (String id : getIdMap().values()) {
            if (skipOnlineScan(id)) {
                ignore = true;
                break;
            }
        }
        return ignore;
    }

    /**
     * Check to see if an ID was set to 0 or -1, in which case the user has disabled online scanning for this movie.
     *
     * @param id
     * @return
     */
    public boolean skipOnlineScan(String id) {
        return StringTools.isNotValidString(id) || "0".equals(id) || "-1".equals(id);
    }
}