org.tinymediamanager.core.movie.entities.Movie.java Source code

Java tutorial

Introduction

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

Source

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

import static org.tinymediamanager.core.Constants.ACTORS;
import static org.tinymediamanager.core.Constants.CERTIFICATION;
import static org.tinymediamanager.core.Constants.COUNTRY;
import static org.tinymediamanager.core.Constants.DATA_SOURCE;
import static org.tinymediamanager.core.Constants.DIRECTOR;
import static org.tinymediamanager.core.Constants.EDITION;
import static org.tinymediamanager.core.Constants.EDITION_AS_STRING;
import static org.tinymediamanager.core.Constants.GENRE;
import static org.tinymediamanager.core.Constants.GENRES_AS_STRING;
import static org.tinymediamanager.core.Constants.HAS_NFO_FILE;
import static org.tinymediamanager.core.Constants.IMDB;
import static org.tinymediamanager.core.Constants.MEDIA_SOURCE;
import static org.tinymediamanager.core.Constants.MOVIESET;
import static org.tinymediamanager.core.Constants.MOVIESET_TITLE;
import static org.tinymediamanager.core.Constants.PRODUCERS;
import static org.tinymediamanager.core.Constants.RELEASE_DATE;
import static org.tinymediamanager.core.Constants.RELEASE_DATE_AS_STRING;
import static org.tinymediamanager.core.Constants.RUNTIME;
import static org.tinymediamanager.core.Constants.SORT_TITLE;
import static org.tinymediamanager.core.Constants.SPOKEN_LANGUAGES;
import static org.tinymediamanager.core.Constants.TAG;
import static org.tinymediamanager.core.Constants.TAGS_AS_STRING;
import static org.tinymediamanager.core.Constants.TITLE_FOR_UI;
import static org.tinymediamanager.core.Constants.TITLE_SORTABLE;
import static org.tinymediamanager.core.Constants.TMDB;
import static org.tinymediamanager.core.Constants.TOP250;
import static org.tinymediamanager.core.Constants.TRAILER;
import static org.tinymediamanager.core.Constants.TRAKT;
import static org.tinymediamanager.core.Constants.VIDEO_IN_3D;
import static org.tinymediamanager.core.Constants.WATCHED;
import static org.tinymediamanager.core.Constants.WRITER;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.xml.bind.annotation.XmlTransient;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinymediamanager.core.MediaFileType;
import org.tinymediamanager.core.MediaSource;
import org.tinymediamanager.core.Utils;
import org.tinymediamanager.core.entities.MediaEntity;
import org.tinymediamanager.core.entities.MediaFile;
import org.tinymediamanager.core.movie.MovieArtworkHelper;
import org.tinymediamanager.core.movie.MovieEdition;
import org.tinymediamanager.core.movie.MovieList;
import org.tinymediamanager.core.movie.MovieMediaFileComparator;
import org.tinymediamanager.core.movie.MovieModuleManager;
import org.tinymediamanager.core.movie.MovieNfoNaming;
import org.tinymediamanager.core.movie.MovieRenamer;
import org.tinymediamanager.core.movie.MovieScraperMetadataConfig;
import org.tinymediamanager.core.movie.MovieTrailerQuality;
import org.tinymediamanager.core.movie.MovieTrailerSources;
import org.tinymediamanager.core.movie.connector.MovieConnectors;
import org.tinymediamanager.core.movie.connector.MovieToKodiNfoConnector;
import org.tinymediamanager.core.movie.connector.MovieToMpNfoConnector;
import org.tinymediamanager.core.movie.connector.MovieToXbmcNfoConnector;
import org.tinymediamanager.core.movie.tasks.MovieActorImageFetcher;
import org.tinymediamanager.core.movie.tasks.MovieTrailerDownloadTask;
import org.tinymediamanager.core.threading.TmmTaskManager;
import org.tinymediamanager.scraper.MediaMetadata;
import org.tinymediamanager.scraper.MediaScrapeOptions;
import org.tinymediamanager.scraper.MediaScraper;
import org.tinymediamanager.scraper.ScraperType;
import org.tinymediamanager.scraper.entities.Certification;
import org.tinymediamanager.scraper.entities.MediaArtwork;
import org.tinymediamanager.scraper.entities.MediaArtwork.MediaArtworkType;
import org.tinymediamanager.scraper.entities.MediaCastMember;
import org.tinymediamanager.scraper.entities.MediaGenres;
import org.tinymediamanager.scraper.entities.MediaType;
import org.tinymediamanager.scraper.mediaprovider.IMovieSetMetadataProvider;
import org.tinymediamanager.scraper.util.StrgUtils;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;

/**
 * The main class for movies.
 * 
 * @author Manuel Laggner / Myron Boyle
 */
public class Movie extends MediaEntity {
    @XmlTransient
    private static final Logger LOGGER = LoggerFactory.getLogger(Movie.class);
    private static final Comparator<MediaFile> MEDIA_FILE_COMPARATOR = new MovieMediaFileComparator();
    private static final Comparator<MovieTrailer> TRAILER_QUALITY_COMPARATOR = new MovieTrailer.QualityComparator();

    @JsonProperty
    private String sortTitle = "";
    @JsonProperty
    private String tagline = "";
    @JsonProperty
    private int runtime = 0;
    @JsonProperty
    private String director = "";
    @JsonProperty
    private String writer = "";
    @JsonProperty
    private String dataSource = "";
    @JsonProperty
    private boolean watched = false;
    @JsonProperty
    private boolean isDisc = false;
    @JsonProperty
    private String spokenLanguages = "";
    @JsonProperty
    private boolean subtitles = false;
    @JsonProperty
    private String country = "";
    @JsonProperty
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private Date releaseDate = null;
    @JsonProperty
    private boolean multiMovieDir = false; // we detected more movies in
                                           // same folder
    @JsonProperty
    private int top250 = 0;
    @JsonProperty
    private MediaSource mediaSource = MediaSource.UNKNOWN; // DVD, Bluray, etc
    @JsonProperty
    private boolean videoIn3D = false;
    @JsonProperty
    private Certification certification = Certification.NOT_RATED;
    @JsonProperty
    private UUID movieSetId;
    @JsonProperty
    private MovieEdition edition = MovieEdition.NONE;
    @JsonProperty
    private boolean stacked = false;
    @JsonProperty
    private boolean offline = false;

    @JsonProperty
    private List<String> genres = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<String> tags = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<String> extraThumbs = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<String> extraFanarts = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<MovieActor> actors = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<MovieProducer> producers = new CopyOnWriteArrayList<>();
    @JsonProperty
    private List<MovieTrailer> trailer = new CopyOnWriteArrayList<>();

    private MovieSet movieSet;
    private String titleSortable = "";
    private Date lastWatched = null;
    private List<MediaGenres> genresForAccess = new CopyOnWriteArrayList<>();

    /**
     * Instantiates a new movie. To initialize the propertychangesupport after loading
     */
    public Movie() {
        // register for dirty flag listener
        super();
    }

    @Override
    protected Comparator<MediaFile> getMediaFileComparator() {
        return MEDIA_FILE_COMPARATOR;
    }

    /**
     * checks if this movie has been scraped.<br>
     * On a fresh DB, just reading local files, everything is again "unscraped". <br>
     * detect minimum of filled values as "scraped"
     * 
     * @return isScraped
     */
    @Override
    public boolean isScraped() {
        return scraped || getHasMetadata();
    }

    /**
     * Gets the sort title.
     * 
     * @return the sort title
     */
    public String getSortTitle() {
        return sortTitle;
    }

    /**
     * Sets the sort title.
     * 
     * @param newValue
     *          the new sort title
     */
    public void setSortTitle(String newValue) {
        String oldValue = this.sortTitle;
        this.sortTitle = newValue;
        firePropertyChange(SORT_TITLE, oldValue, newValue);
    }

    /**
     * Returns the sortable variant of title<br>
     * eg "The Bourne Legacy" -> "Bourne Legacy, The".
     * 
     * @return the title in its sortable format
     */
    public String getTitleSortable() {
        if (StringUtils.isEmpty(titleSortable)) {
            titleSortable = Utils.getSortableName(this.getTitle());
        }
        return titleSortable;
    }

    public void clearTitleSortable() {
        titleSortable = "";
    }

    /**
     * Gets the checks for nfo file.
     * 
     * @return the checks for nfo file
     */
    public Boolean getHasNfoFile() {
        List<MediaFile> mf = getMediaFiles(MediaFileType.NFO);
        if (mf != null && mf.size() > 0) {
            return true;
        }

        return false;
    }

    /**
     * doe we have basic metadata filled?<br>
     * like plot and year to take another fields into account always produces false positives (there are documentaries out there, which do not have
     * actors or either a producer in the meta data DBs..)
     * 
     * @return true/false
     */
    public Boolean getHasMetadata() {
        if (!plot.isEmpty() && !(year.isEmpty() || year.equals("0"))) {
            return true;
        }
        return false;
    }

    /**
     * Gets the checks for images.
     * 
     * @return the checks for images
     */
    public Boolean getHasImages() {
        if (!StringUtils.isEmpty(getArtworkFilename(MediaFileType.POSTER))
                && !StringUtils.isEmpty(getArtworkFilename(MediaFileType.FANART))) {
            return true;
        }
        return false;
    }

    /**
     * Gets the checks for trailer.
     * 
     * @return the checks for trailer
     */
    public Boolean getHasTrailer() {
        if (trailer != null && trailer.size() > 0) {
            return true;
        }

        // check if there is a mediafile (trailer)
        if (!getMediaFiles(MediaFileType.TRAILER).isEmpty()) {
            return true;
        }

        return false;
    }

    /**
     * Gets the title for ui.
     * 
     * @return the title for ui
     */
    public String getTitleForUi() {
        String titleForUi = title;
        if (StringUtils.isNotBlank(year)) {
            titleForUi += " (" + year + ")";
        }
        return titleForUi;
    }

    /**
     * Initialize after loading.
     */
    @Override
    public void initializeAfterLoading() {
        super.initializeAfterLoading();

        // remove empty tag and null values
        Utils.removeEmptyStringsFromList(tags);
        Utils.removeEmptyStringsFromList(genres);

        // load genres
        for (String genre : new ArrayList<>(genres)) {
            addGenre(MediaGenres.getGenre(genre));
        }

        // link with movie set
        if (movieSetId != null) {
            movieSet = MovieList.getInstance().lookupMovieSet(movieSetId);
        }
    }

    /**
     * Adds the actor.
     * 
     * @param obj
     *          the obj
     */
    public void addActor(MovieActor obj) {
        // and re-set movie path the actors
        if (StringUtils.isBlank(obj.getEntityRoot())) {
            obj.setEntityRoot(getPathNIO().toString());
        }

        actors.add(obj);
        firePropertyChange(ACTORS, null, this.getActors());
    }

    /**
     * Gets the trailers
     * 
     * @return the trailers
     */
    public List<MovieTrailer> getTrailer() {
        return this.trailer;
    }

    /**
     * Adds the trailer.
     * 
     * @param obj
     *          the obj
     */
    public void addTrailer(MovieTrailer obj) {
        trailer.add(obj);
        firePropertyChange(TRAILER, null, trailer);
    }

    /**
     * Removes the all trailers.
     */
    public void removeAllTrailers() {
        trailer.clear();
        firePropertyChange(TRAILER, null, trailer);
    }

    /**
     * Adds the to tags.
     * 
     * @param newTag
     *          the new tag
     */
    public void addToTags(String newTag) {
        if (StringUtils.isBlank(newTag)) {
            return;
        }

        for (String tag : tags) {
            if (tag.equals(newTag)) {
                return;
            }
        }

        tags.add(newTag);
        firePropertyChange(TAG, null, newTag);
        firePropertyChange(TAGS_AS_STRING, null, newTag);
    }

    /**
     * Removes the from tags.
     * 
     * @param removeTag
     *          the remove tag
     */
    public void removeFromTags(String removeTag) {
        tags.remove(removeTag);
        firePropertyChange(TAG, null, removeTag);
        firePropertyChange(TAGS_AS_STRING, null, removeTag);
    }

    /**
     * Sets the tags.
     * 
     * @param newTags
     *          the new tags
     */
    @JsonSetter
    public void setTags(List<String> newTags) {
        // two way sync of tags

        // first, add new ones
        for (String tag : newTags) {
            if (!this.tags.contains(tag)) {
                this.tags.add(tag);
            }
        }

        // second remove old ones
        for (int i = this.tags.size() - 1; i >= 0; i--) {
            String tag = this.tags.get(i);
            if (!newTags.contains(tag)) {
                this.tags.remove(tag);
            }
        }

        Utils.removeEmptyStringsFromList(tags);

        firePropertyChange(TAG, null, newTags);
        firePropertyChange(TAGS_AS_STRING, null, newTags);
    }

    /**
     * Gets the tag as string.
     * 
     * @return the tag as string
     */
    public String getTagsAsString() {
        StringBuilder sb = new StringBuilder();
        for (String tag : tags) {
            if (!StringUtils.isEmpty(sb)) {
                sb.append(", ");
            }
            sb.append(tag);
        }
        return sb.toString();
    }

    /**
     * Gets the tags.
     * 
     * @return the tags
     */
    public List<String> getTags() {
        return this.tags;
    }

    /**
     * Gets the data source.
     * 
     * @return the data source
     */
    public String getDataSource() {
        return dataSource;
    }

    /**
     * Sets the data source.
     * 
     * @param newValue
     *          the new data source
     */
    public void setDataSource(String newValue) {
        String oldValue = this.dataSource;
        this.dataSource = newValue;
        firePropertyChange(DATA_SOURCE, oldValue, newValue);
    }

    /** has movie local (or any mediafile inline) subtitles? */
    public boolean hasSubtitles() {
        if (this.subtitles) {
            return true; // local ones found
        }

        if (getMediaFiles(MediaFileType.SUBTITLE).size() > 0) {
            return true;
        }

        for (MediaFile mf : getMediaFiles(MediaFileType.VIDEO)) {
            if (mf.hasSubtitles()) {
                return true;
            }
        }

        return false;
    }

    /** set subtitles */
    public void setSubtitles(boolean sub) {
        this.subtitles = sub;
    }

    /**
     * Searches for actor images, and matches them to our "actors", updating the thumb url
     * 
     * @deprecated thumbPath is generated dynamic - no need for storage
     */
    @Deprecated
    public void findActorImages() {
        if (MovieModuleManager.MOVIE_SETTINGS.isWriteActorImages()) {
            // get all files from the actors path
            try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(getPathNIO())) {
                for (Path path : directoryStream) {
                    if (Utils.isRegularFile(path)) {

                        for (MovieActor actor : getActors()) {
                            if (StringUtils.isBlank(actor.getThumbPath())) {
                                // yay, actor with empty image
                                String name = actor.getNameForStorage();
                                if (name.equals(path.getFileName().toString())) {
                                    actor.setThumbPath(path.toAbsolutePath().toString());
                                }
                            }
                        }

                    }
                }
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Gets the actors.
     * 
     * @return the actors
     */
    public List<MovieActor> getActors() {
        return this.actors;
    }

    /**
     * Gets the director.
     * 
     * @return the director
     */
    public String getDirector() {
        return director;
    }

    /**
     * Gets the imdb id.
     * 
     * @return the imdb id
     */
    public String getImdbId() {
        Object obj = ids.get(IMDB);
        if (obj == null || !Utils.isValidImdbId(obj.toString())) {
            return "";
        }
        return obj.toString();
    }

    /**
     * Gets the tmdb id.
     * 
     * @return the tmdb id
     */
    public int getTmdbId() {
        int id = 0;
        try {
            id = Integer.parseInt(String.valueOf(ids.get(TMDB)));
        } catch (Exception e) {
            return 0;
        }
        return id;
    }

    /**
     * Sets the tmdb id.
     * 
     * @param newValue
     *          the new tmdb id
     */
    public void setTmdbId(int newValue) {
        int oldValue = getTmdbId();
        ids.put(TMDB, newValue);
        firePropertyChange("tmdbId", oldValue, newValue);
    }

    /**
     * Gets the TraktTV id.
     * 
     * @return the TraktTV id
     */
    public int getTraktId() {
        int id = 0;
        try {
            id = Integer.parseInt(String.valueOf(ids.get(TRAKT)));
        } catch (Exception e) {
            return 0;
        }
        return id;
    }

    /**
     * Sets the TraktTV id.
     * 
     * @param newValue
     *          the new TraktTV id
     */
    public void setTraktId(int newValue) {
        int oldValue = getTraktId();
        ids.put(TRAKT, newValue);
        firePropertyChange("traktId", oldValue, newValue);
    }

    /**
     * Gets the runtime in minutes
     * 
     * @return the runtime
     */
    public int getRuntime() {
        int runtimeFromMi = getRuntimeFromMediaFilesInMinutes();
        if (MovieModuleManager.MOVIE_SETTINGS.isRuntimeFromMediaInfo() && runtimeFromMi > 0) {
            return runtimeFromMi;
        }
        return runtime == 0 ? runtimeFromMi : runtime;
    }

    /**
     * Gets the tagline.
     * 
     * @return the tagline
     */
    public String getTagline() {
        return tagline;
    }

    /**
     * Gets the writer.
     * 
     * @return the writer
     */
    public String getWriter() {
        return writer;
    }

    /**
     * Checks for file.
     * 
     * @param filename
     *          the filename
     * @return true, if successful
     */
    public boolean hasFile(String filename) {
        if (StringUtils.isEmpty(filename)) {
            return false;
        }

        for (MediaFile file : new ArrayList<>(getMediaFiles())) {
            if (filename.compareTo(file.getFilename()) == 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Removes the actor.
     * 
     * @param obj
     *          the obj
     */
    public void removeActor(MovieActor obj) {
        actors.remove(obj);
        firePropertyChange(ACTORS, null, this.getActors());
    }

    /**
     * Gets the extra thumbs.
     * 
     * @return the extra thumbs
     */
    public List<String> getExtraThumbs() {
        return extraThumbs;
    }

    /**
     * Sets the extra thumbs.
     * 
     * @param extraThumbs
     *          the new extra thumbs
     */
    @JsonSetter
    public void setExtraThumbs(List<String> extraThumbs) {
        this.extraThumbs.clear();
        this.extraThumbs.addAll(extraThumbs);
    }

    /**
     * Gets the extra fanarts.
     * 
     * @return the extra fanarts
     */
    public List<String> getExtraFanarts() {
        return extraFanarts;
    }

    /**
     * Sets the extra fanarts.
     * 
     * @param extraFanarts
     *          the new extra fanarts
     */
    @JsonSetter
    public void setExtraFanarts(List<String> extraFanarts) {
        this.extraFanarts.clear();
        this.extraFanarts.addAll(extraFanarts);
    }

    /**
     * Sets the imdb id.
     * 
     * @param newValue
     *          the new imdb id
     */
    public void setImdbId(String newValue) {
        if (!Utils.isValidImdbId(newValue)) {
            newValue = "";
        }
        String oldValue = getImdbId();
        ids.put(IMDB, newValue);
        firePropertyChange("imdbId", oldValue, newValue);
    }

    /**
     * Sets the metadata.
     * 
     * @param metadata
     *          the new metadata
     * @param config
     *          the config
     */
    public void setMetadata(MediaMetadata metadata, MovieScraperMetadataConfig config) {
        if (metadata == null) {
            LOGGER.error("metadata was null");
            return;
        }

        // check if metadata has at least a name
        if (StringUtils.isEmpty(metadata.getTitle())) {
            LOGGER.warn("wanted to save empty metadata for " + getTitle());
            return;
        }

        setIds(metadata.getIds());

        // set chosen metadata
        if (config.isTitle()) {
            setTitle(metadata.getTitle());
        }

        if (config.isOriginalTitle()) {
            setOriginalTitle(metadata.getOriginalTitle());
        }

        if (config.isTagline()) {
            setTagline(metadata.getTagline());
        }

        if (config.isPlot()) {
            setPlot(metadata.getPlot());
        }

        if (config.isYear()) {
            if (metadata.getYear() != 0) {
                setYear(Integer.toString(metadata.getYear()));
            } else {
                setYear("");
            }
            setReleaseDate(metadata.getReleaseDate());
        }

        if (config.isRating()) {
            setRating(metadata.getRating());
            setVotes(metadata.getVoteCount());
            setTop250(metadata.getTop250());
        }

        if (config.isRuntime()) {
            setRuntime(metadata.getRuntime());
        }

        setSpokenLanguages(StringUtils.join(metadata.getSpokenLanguages(), ", "));
        setCountry(StringUtils.join(metadata.getCountries(), ", "));

        // certifications
        if (config.isCertification()) {
            if (metadata.getCertifications() != null && metadata.getCertifications().size() > 0) {
                setCertification(metadata.getCertifications().get(0));
            }
        }

        // cast
        if (config.isCast()) {
            setProductionCompany(StringUtils.join(metadata.getProductionCompanies(), ", "));
            List<MovieActor> actors = new ArrayList<>();
            List<MovieProducer> producers = new ArrayList<>();
            String director = "";
            String writer = "";
            for (MediaCastMember member : metadata.getCastMembers()) {
                switch (member.getType()) {
                case ACTOR:
                    MovieActor actor = new MovieActor();
                    actor.setName(member.getName());
                    actor.setCharacter(member.getCharacter());
                    actor.setThumbUrl(member.getImageUrl());
                    actors.add(actor);
                    break;

                case DIRECTOR:
                    if (!StringUtils.isEmpty(director)) {
                        director += ", ";
                    }
                    director += member.getName();
                    break;

                case WRITER:
                    if (!StringUtils.isEmpty(writer)) {
                        writer += ", ";
                    }
                    writer += member.getName();
                    break;

                case PRODUCER:
                    MovieProducer producer = new MovieProducer();
                    producer.setName(member.getName());
                    producer.setRole(member.getPart());
                    producer.setThumbUrl(member.getImageUrl());
                    producers.add(producer);
                    break;

                default:
                    break;
                }
            }
            setActors(actors);
            setDirector(director);
            setWriter(writer);
            setProducers(producers);
            writeActorImages();
        }

        // genres
        if (config.isGenres()) {
            setGenres(metadata.getGenres());
        }

        // tags
        if (config.isTags()) {
            for (String tag : metadata.getTags()) {
                addToTags(tag);
            }
        }

        // set scraped
        setScraped(true);

        // create MovieSet
        if (config.isCollection()) {
            int col = 0;
            try {
                col = (int) metadata.getId(MediaMetadata.TMDB_SET);
            } catch (Exception ignored) {
            }
            if (col != 0) {
                MovieSet movieSet = MovieList.getInstance().getMovieSet(metadata.getCollectionName(), col);
                if (movieSet != null && movieSet.getTmdbId() == 0) {
                    movieSet.setTmdbId(col);
                    // get movieset metadata
                    try {
                        List<MediaScraper> sets = MediaScraper.getMediaScrapers(ScraperType.MOVIE_SET);
                        if (sets != null && sets.size() > 0) {
                            MediaScraper first = sets.get(0); // just get first
                            IMovieSetMetadataProvider mp = ((IMovieSetMetadataProvider) first.getMediaProvider());
                            MediaScrapeOptions options = new MediaScrapeOptions(MediaType.MOVIE_SET);
                            options.setTmdbId(col);
                            options.setLanguage(LocaleUtils
                                    .toLocale(MovieModuleManager.MOVIE_SETTINGS.getScraperLanguage().name()));
                            options.setCountry(MovieModuleManager.MOVIE_SETTINGS.getCertificationCountry());

                            MediaMetadata info = mp.getMetadata(options);
                            if (info != null && StringUtils.isNotBlank(info.getTitle())) {
                                movieSet.setTitle(info.getTitle());
                                movieSet.setPlot(info.getPlot());

                                if (!info.getMediaArt(MediaArtworkType.POSTER).isEmpty()) {
                                    movieSet.setArtworkUrl(
                                            info.getMediaArt(MediaArtworkType.POSTER).get(0).getDefaultUrl(),
                                            MediaFileType.POSTER);
                                }
                                if (!info.getMediaArt(MediaArtworkType.BACKGROUND).isEmpty()) {
                                    movieSet.setArtworkUrl(
                                            info.getMediaArt(MediaArtworkType.BACKGROUND).get(0).getDefaultUrl(),
                                            MediaFileType.FANART);
                                }
                            }
                        }
                    } catch (Exception e) {
                    }
                }

                // add movie to movieset
                if (movieSet != null) {
                    // first remove from "old" movieset
                    setMovieSet(null);

                    // add to new movieset
                    // movieSet.addMovie(this);
                    setMovieSet(movieSet);
                    movieSet.insertMovie(this);
                    movieSet.saveToDb();
                }
            }
        }

        // update DB
        writeNFO();
        saveToDb();

        // rename the movie if that has been chosen in the settings
        if (MovieModuleManager.MOVIE_SETTINGS.isMovieRenameAfterScrape()) {
            MovieRenamer.renameMovie(this);
        }
    }

    /**
     * Sets the trailers; first one is "inNFO" if not a local one.
     * 
     * @param trailers
     *          the new trailers
     */
    @JsonSetter
    public void setTrailers(List<MovieTrailer> trailers) {
        MovieTrailer preferredTrailer = null;
        removeAllTrailers();

        // set preferred trailer
        if (MovieModuleManager.MOVIE_SETTINGS.isUseTrailerPreference()) {
            MovieTrailerQuality desiredQuality = MovieModuleManager.MOVIE_SETTINGS.getTrailerQuality();
            MovieTrailerSources desiredSource = MovieModuleManager.MOVIE_SETTINGS.getTrailerSource();

            // search for quality and provider
            for (MovieTrailer trailer : trailers) {
                if (desiredQuality.containsQuality(trailer.getQuality())
                        && desiredSource.containsSource(trailer.getProvider())) {
                    trailer.setInNfo(Boolean.TRUE);
                    preferredTrailer = trailer;
                    break;
                }
            }

            // search for quality
            if (preferredTrailer == null) {
                for (MovieTrailer trailer : trailers) {
                    if (desiredQuality.containsQuality(trailer.getQuality())) {
                        trailer.setInNfo(Boolean.TRUE);
                        preferredTrailer = trailer;
                        break;
                    }
                }
            }

            // if not yet one has been found; sort by quality descending and take the first one which is lower or equal to the desired quality
            if (preferredTrailer == null) {
                List<MovieTrailer> sortedTrailers = new ArrayList<>(trailers);
                Collections.sort(sortedTrailers, TRAILER_QUALITY_COMPARATOR);
                for (MovieTrailer trailer : sortedTrailers) {
                    if (desiredQuality.ordinal() >= MovieTrailerQuality.getMovieTrailerQuality(trailer.getQuality())
                            .ordinal()) {
                        trailer.setInNfo(Boolean.TRUE);
                        preferredTrailer = trailer;
                        break;
                    }
                }
            }
        } // end if MovieModuleManager.MOVIE_SETTINGS.isUseTrailerPreference()

        // if not yet one has been found; sort by quality descending and take the first one
        if (preferredTrailer == null && !trailers.isEmpty()) {
            List<MovieTrailer> sortedTrailers = new ArrayList<>(trailers);
            Collections.sort(sortedTrailers, TRAILER_QUALITY_COMPARATOR);
            preferredTrailer = sortedTrailers.get(0);
            preferredTrailer.setInNfo(Boolean.TRUE);
        }

        // add trailers
        if (preferredTrailer != null) {
            addTrailer(preferredTrailer);
        }
        for (MovieTrailer trailer : trailers) {
            // preferred trailer has already been added
            if (preferredTrailer != null && preferredTrailer == trailer) {
                continue;
            }

            // if still no preferred trailer has been set, then mark the first one
            if (preferredTrailer == null && this.trailer.size() == 0 && !trailer.getUrl().startsWith("file")) {
                trailer.setInNfo(Boolean.TRUE);
            }

            addTrailer(trailer);
        }

        if (MovieModuleManager.MOVIE_SETTINGS.isUseTrailerPreference()
                && MovieModuleManager.MOVIE_SETTINGS.isAutomaticTrailerDownload()
                && getMediaFiles(MediaFileType.TRAILER).isEmpty() && !trailer.isEmpty()) {
            MovieTrailer trailer = this.trailer.get(0);
            MovieTrailerDownloadTask task = new MovieTrailerDownloadTask(trailer, this);
            TmmTaskManager.getInstance().addDownloadTask(task);
        }

        // persist
        writeNFO();
        saveToDb();
    }

    /**
     * Gets the metadata.
     * 
     * @return the metadata
     */
    public MediaMetadata getMetadata() {
        MediaMetadata md = new MediaMetadata("");

        for (Entry<String, Object> entry : ids.entrySet()) {
            md.setId(entry.getKey(), entry.getValue());
        }

        md.setTitle(title);
        md.setOriginalTitle(originalTitle);
        md.setTagline(tagline);
        md.setPlot(plot);
        try {
            md.setYear(Integer.parseInt(year));
        } catch (Exception ignored) {
        }
        md.setRating(rating);
        md.setVoteCount(votes);
        md.setRuntime(runtime);
        md.addCertification(certification);

        return md;
    }

    /**
     * Sets the artwork.
     * 
     * @param md
     *          the md
     * @param config
     *          the config
     */
    public void setArtwork(MediaMetadata md, MovieScraperMetadataConfig config) {
        setArtwork(md.getMediaArt(MediaArtworkType.ALL), config);
    }

    /**
     * Sets the artwork.
     * 
     * @param artwork
     *          the artwork
     * @param config
     *          the config
     */
    public void setArtwork(List<MediaArtwork> artwork, MovieScraperMetadataConfig config) {
        if (config.isArtwork()) {
            MovieArtworkHelper.setArtwork(this, artwork);
        }
    }

    /**
     * Sets the actors.
     * 
     * @param newActors
     *          the new actors
     */
    @JsonSetter
    public void setActors(List<MovieActor> newActors) {
        // two way sync of actors

        // first remove unused
        for (int i = actors.size() - 1; i >= 0; i--) {
            MovieActor actor = actors.get(i);
            if (!newActors.contains(actor)) {
                actors.remove(actor);
            }
        }

        // second add the new ones
        for (int i = 0; i < newActors.size(); i++) {
            MovieActor actor = newActors.get(i);
            if (!actors.contains(actor)) {
                try {
                    actors.add(i, actor);
                } catch (IndexOutOfBoundsException e) {
                    actors.add(actor);
                }
            } else {
                int indexOldList = actors.indexOf(actor);
                if (i != indexOldList) {
                    MovieActor oldActor = actors.remove(indexOldList);
                    try {
                        actors.add(i, oldActor);
                    } catch (IndexOutOfBoundsException e) {
                        actors.add(oldActor);
                    }
                }
            }
        }

        // and re-set movie path to the actors
        for (MovieActor actor : actors) {
            if (StringUtils.isBlank(actor.getEntityRoot())) {
                actor.setEntityRoot(getPathNIO().toString());
            }
        }

        // third - rename thumbs if needed
        // NAH - thumb is always dynamic now - so if name doesnt change, nothing to rename
        // actor writing/caching is done somewhere else...

        // if (MovieModuleManager.MOVIE_SETTINGS.isWriteActorImages()) {
        // Path actorDir = getPathNIO().resolve(MovieActor.ACTOR_DIR);
        //
        // for (MovieActor actor : actors) {
        // if (StringUtils.isNotBlank(actor.getThumbPath())) {
        // try {
        // // build expected filename
        // Path actorName = actorDir.resolve(actor.getNameForStorage() + "." + FilenameUtils.getExtension(actor.getThumbPath()));
        // Path oldFile = Paths.get(actor.getThumbPath());
        // Utils.moveFileSafe(oldFile, actorName);
        // }
        // catch (IOException e) {
        // LOGGER.warn("couldn't rename actor thumb (" + actor.getThumbPath() + "): " + e.getMessage());
        // }
        // }
        // }
        // }

        firePropertyChange(ACTORS, null, this.getActors());
    }

    @Override
    public void setTitle(String newValue) {
        String oldValue = this.title;
        super.setTitle(newValue);

        firePropertyChange(TITLE_FOR_UI, oldValue, newValue);

        oldValue = this.titleSortable;
        titleSortable = "";
        firePropertyChange(TITLE_SORTABLE, oldValue, titleSortable);
    }

    /**
     * Sets the runtime in minutes
     * 
     * @param newValue
     *          the new runtime
     */
    public void setRuntime(int newValue) {
        int oldValue = this.runtime;
        this.runtime = newValue;
        firePropertyChange(RUNTIME, oldValue, newValue);
    }

    /**
     * Sets the tagline.
     * 
     * @param newValue
     *          the new tagline
     */
    public void setTagline(String newValue) {
        String oldValue = this.tagline;
        this.tagline = newValue;
        firePropertyChange("tagline", oldValue, newValue);
    }

    /**
     * Sets the year.
     * 
     * @param newValue
     *          the new year
     */
    @Override
    public void setYear(String newValue) {
        String oldValue = year;
        super.setYear(newValue);

        firePropertyChange(TITLE_FOR_UI, oldValue, newValue);
    }

    /**
     * all XBMC supported NFO names. (without path!)
     * 
     * @param nfo
     *          the nfo
     * @return the nfo filename
     */
    public String getNfoFilename(MovieNfoNaming nfo) {
        List<MediaFile> mfs = getMediaFiles(MediaFileType.VIDEO);
        if (mfs != null && mfs.size() > 0) {
            String name = mfs.get(0).getFilename();
            if (isStacked()) {
                // when movie IS stacked, remove stacking marker, else keep it!
                name = Utils.cleanStackingMarkers(name);
            }
            return getNfoFilename(nfo, name);
        } else {
            return getNfoFilename(nfo, ""); // no video files
        }
    }

    /**
     * all XBMC supported NFO names. (without path!)
     * 
     * @param nfo
     *          the nfo filenaming
     * @param newMovieFilename
     *          the new/desired movie filename (stacking marker should already be set correct here!)
     * @return the nfo filename
     */
    public String getNfoFilename(MovieNfoNaming nfo, String newMovieFilename) {
        String filename = "";
        switch (nfo) {
        case FILENAME_NFO:
            if (isDisc()) {
                // if filename is activated, we generate them accordingly MF(1)
                // but if disc, fixate this
                if (Files.exists(getPathNIO().resolve("VIDEO_TS.ifo"))
                        || Files.exists(getPathNIO().resolve("VIDEO_TS"))) {
                    filename = "VIDEO_TS.nfo";
                } else if (Files.exists(getPathNIO().resolve("index.bdmv"))) {
                    filename = "index.nfo";
                } else if (Files.exists(getPathNIO().resolve("BDMV"))) {
                    filename = "BDMV.nfo";
                }
            } else {
                String movieFilename = FilenameUtils.getBaseName(newMovieFilename);
                filename += movieFilename + ".nfo";
            }
            break;
        case MOVIE_NFO:
            filename += "movie.nfo";
            break;
        case DISC_NFO:
            if (isDisc()) {
                Path dir = getPathNIO().resolve("VIDEO_TS");
                if (Files.isDirectory(dir)) {
                    filename = dir.resolve("VIDEO_TS.nfo").toString();
                }
                dir = getPathNIO().resolve("BDMV");
                if (Files.isDirectory(dir)) {
                    filename = dir.resolve("index.nfo").toString();
                }
            }
            break;
        default:
            filename = "";
            break;
        }
        // LOGGER.trace("getNfoFilename: '" + newMovieFilename + "' / " + nfo + " -> '" + filename + "'");
        return filename;
    }

    /**
     * get trailer name (w/o extension)<br>
     * &lt;moviefile&gt;-trailer.ext
     * 
     * @return the trailer basename
     */
    public String getTrailerBasename() {
        List<MediaFile> mfs = getMediaFiles(MediaFileType.VIDEO);
        if (mfs != null && mfs.size() > 0) {
            return FilenameUtils.getBaseName(Utils.cleanStackingMarkers(mfs.get(0).getFilename()));
        }
        return null;
    }

    /**
     * download the specified type of artwork for this movie
     *
     * @param type
     *          the chosen artwork type to be downloaded
     */
    public void downloadArtwork(MediaFileType type) {
        MovieArtworkHelper.downloadArtwork(this, type);
    }

    /**
     * Write actor images.
     */
    public void writeActorImages() {
        // check if actor images shall be written
        if (!MovieModuleManager.MOVIE_SETTINGS.isWriteActorImages() || isMultiMovieDir()) {
            return;
        }

        MovieActorImageFetcher task = new MovieActorImageFetcher(this);
        TmmTaskManager.getInstance().addImageDownloadTask(task);
    }

    /**
     * Write nfo.
     */
    public void writeNFO() {
        if (MovieModuleManager.MOVIE_SETTINGS.getMovieNfoFilenames().isEmpty()) {
            LOGGER.info("Not writing any NFO file, because NFO filename preferences were empty...");
            return;
        }
        if (MovieModuleManager.MOVIE_SETTINGS.getMovieConnector() == MovieConnectors.MP) {
            MovieToMpNfoConnector.setData(this);
        } else if (MovieModuleManager.MOVIE_SETTINGS.getMovieConnector() == MovieConnectors.XBMC) {
            MovieToXbmcNfoConnector.setData(this);
        } else {
            MovieToKodiNfoConnector.setData(this);
        }
        firePropertyChange(HAS_NFO_FILE, false, true);
    }

    /**
     * Sets the director.
     * 
     * @param newValue
     *          the new director
     */
    public void setDirector(String newValue) {
        String oldValue = this.director;
        this.director = newValue;
        firePropertyChange(DIRECTOR, oldValue, newValue);
    }

    /**
     * Sets the writer.
     * 
     * @param newValue
     *          the new writer
     */
    public void setWriter(String newValue) {
        String oldValue = this.writer;
        this.writer = newValue;
        firePropertyChange(WRITER, oldValue, newValue);
    }

    /**
     * Gets the genres.
     * 
     * @return the genres
     */
    public List<MediaGenres> getGenres() {
        return genresForAccess;
    }

    /**
     * Adds the genre.
     * 
     * @param newValue
     *          the new value
     */
    public void addGenre(MediaGenres newValue) {
        if (!genresForAccess.contains(newValue)) {
            genresForAccess.add(newValue);
            if (!genres.contains(newValue.name())) {
                genres.add(newValue.name());
            }
            firePropertyChange(GENRE, null, newValue);
            firePropertyChange(GENRES_AS_STRING, null, newValue);
        }
    }

    /**
     * Sets the genres.
     * 
     * @param genres
     *          the new genres
     */
    @JsonSetter
    public void setGenres(List<MediaGenres> genres) {
        // two way sync of genres

        // first, add new ones
        for (MediaGenres genre : genres) {
            if (!this.genresForAccess.contains(genre)) {
                this.genresForAccess.add(genre);
                if (!this.genres.contains(genre.name())) {
                    this.genres.add(genre.name());
                }
            }
        }

        // second remove old ones
        for (int i = this.genresForAccess.size() - 1; i >= 0; i--) {
            MediaGenres genre = this.genresForAccess.get(i);
            if (!genres.contains(genre)) {
                this.genresForAccess.remove(genre);
                this.genres.remove(genre.name());
            }
        }

        firePropertyChange(GENRE, null, genres);
        firePropertyChange(GENRES_AS_STRING, null, genres);
    }

    /**
     * Removes the genre.
     * 
     * @param genre
     *          the genre
     */
    public void removeGenre(MediaGenres genre) {
        if (genresForAccess.contains(genre)) {
            genresForAccess.remove(genre);
            genres.remove(genre.name());
            firePropertyChange(GENRE, null, genre);
            firePropertyChange(GENRES_AS_STRING, null, genre);
        }
    }

    /**
     * Gets the certifications.
     * 
     * @return the certifications
     */
    public Certification getCertification() {
        return certification;
    }

    /**
     * Sets the certifications.
     * 
     * @param newValue
     *          the new certifications
     */
    public void setCertification(Certification newValue) {
        this.certification = newValue;
        firePropertyChange(CERTIFICATION, null, newValue);
    }

    /**
     * Gets the checks for rating.
     * 
     * @return the checks for rating
     */
    public boolean getHasRating() {
        if (rating > 0 || scraped) {
            return true;
        }
        return false;
    }

    /**
     * Gets the genres as string.
     * 
     * @return the genres as string
     */
    public String getGenresAsString() {
        StringBuilder sb = new StringBuilder();
        for (MediaGenres genre : genresForAccess) {
            if (!StringUtils.isEmpty(sb)) {
                sb.append(", ");
            }
            sb.append(genre != null ? genre.getLocalizedName() : "null");
        }
        return sb.toString();
    }

    /**
     * Checks if is watched.
     * 
     * @return true, if is watched
     */
    public boolean isWatched() {
        return watched;
    }

    /**
     * Sets the watched.
     * 
     * @param newValue
     *          the new watched
     */
    public void setWatched(boolean newValue) {
        boolean oldValue = this.watched;
        this.watched = newValue;
        firePropertyChange(WATCHED, oldValue, newValue);
    }

    /**
     * Checks if this movie is in a folder with other movies and not in an own folder<br>
     * so disable everything except renaming
     * 
     * @return true, if in datasource root
     */
    public boolean isMultiMovieDir() {
        return multiMovieDir;
    }

    /**
     * Sets the flag, that the movie is not in an own folder<br>
     * so disable everything except renaming
     * 
     * @param multiDir
     *          true/false
     */
    public void setMultiMovieDir(boolean multiDir) {
        this.multiMovieDir = multiDir;
    }

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

    /**
     * Gets the movie set.
     * 
     * @return the movieset
     */
    public MovieSet getMovieSet() {
        return movieSet;
    }

    /**
     * Sets the movie set.
     * 
     * @param newValue
     *          the new movie set
     */
    public void setMovieSet(MovieSet newValue) {
        MovieSet oldValue = this.movieSet;
        this.movieSet = newValue;

        if (newValue == null) {
            movieSetId = null;
        } else {
            movieSetId = newValue.getDbId();
        }

        firePropertyChange(MOVIESET, oldValue, newValue);
        firePropertyChange(MOVIESET_TITLE, oldValue, newValue);
    }

    public void movieSetTitleChanged() {
        firePropertyChange(MOVIESET_TITLE, null, "");
    }

    public String getMovieSetTitle() {
        if (movieSet != null) {
            return movieSet.getTitle();
        }
        return "";
    }

    /**
     * Removes the from movie set.
     */
    public void removeFromMovieSet() {
        if (movieSet != null) {
            movieSet.removeMovie(this);
        }
        setMovieSet(null);
    }

    /**
     * is this a disc movie folder (video_ts / bdmv)?.
     * 
     * @return true, if is disc
     */
    public boolean isDisc() {
        return isDisc;
    }

    /**
     * is this a disc movie folder (video_ts / bdmv)?.
     * 
     * @param isDisc
     *          the new disc
     */
    public void setDisc(boolean isDisc) {
        this.isDisc = isDisc;
    }

    /**
     * Gets the media info video format (i.e. 720p).
     */
    public String getMediaInfoVideoFormat() {
        List<MediaFile> videos = getMediaFiles(MediaFileType.VIDEO);
        if (videos.size() > 0) {
            MediaFile mediaFile = videos.get(0);
            return mediaFile.getVideoFormat();
        }

        return "";
    }

    /**
     * Gets the media info video codec (i.e. divx)
     */
    public String getMediaInfoVideoCodec() {
        List<MediaFile> videos = getMediaFiles(MediaFileType.VIDEO);
        if (videos.size() > 0) {
            MediaFile mediaFile = videos.get(0);
            return mediaFile.getVideoCodec();
        }

        return "";
    }

    public int getMediaInfoVideoBitrate() {
        List<MediaFile> videos = getMediaFiles(MediaFileType.VIDEO);
        if (videos.size() > 0) {
            MediaFile mediaFile = videos.get(0);
            return mediaFile.getOverallBitRate();
        }

        return 0;
    }

    /**
     * Gets the media info audio codec (i.e mp3) and channels (i.e. 6 at 5.1 sound)
     */
    public String getMediaInfoAudioCodecAndChannels() {
        List<MediaFile> videos = getMediaFiles(MediaFileType.VIDEO);
        if (videos.size() > 0) {
            MediaFile mediaFile = videos.get(0);
            return mediaFile.getAudioCodec() + "_" + mediaFile.getAudioChannels();
        }

        return "";
    }

    public String getMediaInfoVideoResolution() {

        return "";
    }

    public void setSpokenLanguages(String newValue) {
        String oldValue = this.spokenLanguages;
        this.spokenLanguages = newValue;
        firePropertyChange(SPOKEN_LANGUAGES, oldValue, newValue);
    }

    public String getSpokenLanguages() {
        return this.spokenLanguages;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String newValue) {
        String oldValue = this.country;
        this.country = newValue;
        firePropertyChange(COUNTRY, oldValue, newValue);
    }

    public MediaSource getMediaSource() {
        return mediaSource;
    }

    public void setMediaSource(MediaSource newValue) {
        MediaSource oldValue = this.mediaSource;
        this.mediaSource = newValue;
        firePropertyChange(MEDIA_SOURCE, oldValue, newValue);
    }

    /**
     * Gets the images to cache.
     */
    public List<Path> getImagesToCache() {
        // image files
        List<Path> filesToCache = new ArrayList<>();
        for (MediaFile mf : getMediaFiles()) {
            if (mf.isGraphic()) {
                filesToCache.add(mf.getFileAsPath());
            }
        }

        // actor image files
        if (MovieModuleManager.MOVIE_SETTINGS.isWriteActorImages()) {
            for (MovieActor actor : actors) {
                Path imagePath = actor.getStoragePath();
                if (imagePath != null) {
                    filesToCache.add(imagePath);
                }
            }
        }

        return filesToCache;
    }

    public List<MediaFile> getMediaFilesContainingAudioStreams() {
        List<MediaFile> mediaFilesWithAudioStreams = new ArrayList<>(1);

        // get the audio streams from the first video file
        List<MediaFile> videoFiles = getMediaFiles(MediaFileType.VIDEO);
        if (videoFiles.size() > 0) {
            MediaFile videoFile = videoFiles.get(0);
            mediaFilesWithAudioStreams.add(videoFile);
        }

        // get all extra audio streams
        for (MediaFile audioFile : getMediaFiles(MediaFileType.AUDIO)) {
            mediaFilesWithAudioStreams.add(audioFile);
        }

        return mediaFilesWithAudioStreams;
    }

    public List<MediaFile> getMediaFilesContainingSubtitles() {
        List<MediaFile> mediaFilesWithSubtitles = new ArrayList<>(1);

        for (MediaFile mediaFile : getMediaFiles(MediaFileType.VIDEO, MediaFileType.SUBTITLE)) {
            if (mediaFile.hasSubtitles()) {
                mediaFilesWithSubtitles.add(mediaFile);
            }
        }

        return mediaFilesWithSubtitles;
    }

    public int getRuntimeFromMediaFiles() {
        int runtime = 0;
        for (MediaFile mf : getMediaFiles(MediaFileType.VIDEO)) {
            runtime += mf.getDuration();
        }
        return runtime;
    }

    public int getRuntimeFromMediaFilesInMinutes() {
        return getRuntimeFromMediaFiles() / 60;
    }

    public Date getReleaseDate() {
        return releaseDate;
    }

    @JsonIgnore
    public void setReleaseDate(Date newValue) {
        Date oldValue = this.releaseDate;
        this.releaseDate = newValue;
        firePropertyChange(RELEASE_DATE, oldValue, newValue);
        firePropertyChange(RELEASE_DATE_AS_STRING, oldValue, newValue);
    }

    /**
     * release date as yyyy-mm-dd<br>
     * https://xkcd.com/1179/ :P
     */
    public String getReleaseDateFormatted() {
        if (this.releaseDate == null) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd").format(this.releaseDate);
    }

    /**
     * Gets the first aired as a string, formatted in the system locale.
     */
    public String getReleaseDateAsString() {
        if (this.releaseDate == null) {
            return "";
        }
        return SimpleDateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(releaseDate);
    }

    /**
     * convenient method to set the release date (parsed from string).
     */
    public void setReleaseDate(String dateAsString) throws ParseException {
        setReleaseDate(StrgUtils.parseDate(dateAsString));
    }

    public Date getLastWatched() {
        return lastWatched;
    }

    public void setLastWatched(Date lastWatched) {
        this.lastWatched = lastWatched;
    }

    @Override
    public void saveToDb() {
        // update/insert this movie to the database
        MovieList.getInstance().persistMovie(this);
    }

    @Override
    public void deleteFromDb() {
        // remove this movie from the database
        MovieList.getInstance().removeMovieFromDb(this);
    }

    @Override
    public synchronized void callbackForWrittenArtwork(MediaArtworkType type) {
        if (MovieModuleManager.MOVIE_SETTINGS.getMovieConnector() == MovieConnectors.MP) {
            writeNFO();
        }
    }

    public List<MediaFile> getVideoFiles() {
        return getMediaFiles(MediaFileType.VIDEO);
    }

    /**
     * gets the basename (without stacking)
     * 
     * @return the video base name (without stacking)
     */
    public String getVideoBasenameWithoutStacking() {
        MediaFile mf = getMediaFiles(MediaFileType.VIDEO).get(0);
        return FilenameUtils.getBaseName(mf.getFilenameWithoutStacking());
    }

    public int getTop250() {
        return top250;
    }

    public void setVideoIn3D(boolean newValue) {
        boolean oldValue = this.videoIn3D;
        this.videoIn3D = newValue;
        firePropertyChange(VIDEO_IN_3D, oldValue, newValue);
    }

    public boolean isVideoIn3D() {
        String video3DFormat = "";
        List<MediaFile> videos = getMediaFiles(MediaFileType.VIDEO);
        if (videos.size() > 0) {
            MediaFile mediaFile = videos.get(0);
            video3DFormat = mediaFile.getVideo3DFormat();
        }

        return videoIn3D || StringUtils.isNotBlank(video3DFormat);
    }

    public void setTop250(int newValue) {
        int oldValue = this.top250;
        this.top250 = newValue;
        firePropertyChange(TOP250, oldValue, newValue);
    }

    public void addProducer(MovieProducer obj) {
        // and re-set movie path of the producer
        if (StringUtils.isBlank(obj.getEntityRoot())) {
            obj.setEntityRoot(getPathNIO().toString());
        }

        producers.add(obj);

        firePropertyChange(PRODUCERS, null, producers);
    }

    public void removeProducer(MovieProducer obj) {
        producers.remove(obj);
        firePropertyChange(PRODUCERS, null, producers);
    }

    @JsonSetter
    public void setProducers(List<MovieProducer> newProducers) {
        // two way sync of producers
        // first remove unused
        for (int i = producers.size() - 1; i >= 0; i--) {
            MovieProducer producer = producers.get(i);
            if (!newProducers.contains(producer)) {
                producers.remove(producer);
            }
        }

        // second add the new ones
        for (int i = 0; i < newProducers.size(); i++) {
            MovieProducer producer = newProducers.get(i);
            if (!producers.contains(producer)) {
                // new producer
                try {
                    producers.add(i, producer);
                } catch (IndexOutOfBoundsException e) {
                    producers.add(producer);
                }
            } else {
                int indexOldList = producers.indexOf(producer);
                if (i != indexOldList) {
                    MovieProducer oldProducer = producers.remove(indexOldList);
                    try {
                        producers.add(i, oldProducer);
                    } catch (IndexOutOfBoundsException e) {
                        producers.add(oldProducer);
                    }
                }
            }
        }

        // and re-set movie path to the producers
        for (MovieProducer producer : producers) {
            if (StringUtils.isBlank(producer.getEntityRoot())) {
                producer.setEntityRoot(getPathNIO().toString());
            }
        }

        firePropertyChange(PRODUCERS, null, producers);
    }

    public List<MovieProducer> getProducers() {
        return this.producers;
    }

    /**
     * Is the movie "stacked" (more than one video file)
     * 
     * @return true if the movie is stacked; false otherwise
     */
    public boolean isStacked() {
        return stacked;
    }

    public void setStacked(boolean stacked) {
        this.stacked = stacked;
    }

    /**
     * ok, we might have detected some stacking MFs.<br>
     * But if we only have ONE video file, reset stacking markers in this case<br>
     * eg: "Harry Potter 7 - Part 1" is not stacked<br>
     * CornerCase: what if HP7 has more files...?
     */
    public void reEvaluateStacking() {
        List<MediaFile> mfs = getMediaFiles(MediaFileType.VIDEO);
        if (mfs.size() > 1 && !isDisc()) {
            // ok, more video files means stacking (if not a disc folder)
            this.setStacked(true);
            for (MediaFile mf : getMediaFiles(MediaFileType.VIDEO, MediaFileType.AUDIO, MediaFileType.SUBTITLE)) {
                mf.detectStackingInformation();
            }
        } else {
            // only ONE video? remove any stacking markers from MFs
            this.setStacked(false);
            for (MediaFile mf : getMediaFiles(MediaFileType.VIDEO, MediaFileType.AUDIO, MediaFileType.SUBTITLE)) {
                mf.removeStackingInformation();
            }
        }
    }

    /**
     * <b>PHYSICALLY</b> deletes a complete Movie by moving it to datasource backup folder<br>
     * DS\.backup\&lt;moviename&gt;
     */
    public boolean deleteFilesSafely() {
        // backup
        if (isMultiMovieDir()) {
            boolean ok = true;
            for (MediaFile mf : getMediaFiles()) {
                if (!mf.deleteSafely(getDataSource())) {
                    ok = false;
                }
            }
            return ok;
        } else {
            return Utils.deleteDirectorySafely(getPathNIO(), getDataSource());
        }
    }

    public MovieEdition getEdition() {
        return edition;
    }

    public String getEditionAsString() {
        return edition.toString();
    }

    public void setOffline(boolean newValue) {
        boolean oldValue = this.offline;
        this.offline = newValue;
        firePropertyChange("offline", oldValue, newValue);
    }

    public boolean isOffline() {
        return offline;
    }

    public void setEdition(MovieEdition newValue) {
        MovieEdition oldValue = this.edition;
        this.edition = newValue;
        firePropertyChange(EDITION, oldValue, newValue);
        firePropertyChange(EDITION_AS_STRING, oldValue, newValue);
    }
}