org.tinymediamanager.core.movie.MovieList.java Source code

Java tutorial

Introduction

Here is the source code for org.tinymediamanager.core.movie.MovieList.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;

import static org.tinymediamanager.core.Constants.CERTIFICATION;
import static org.tinymediamanager.core.Constants.MEDIA_FILES;
import static org.tinymediamanager.core.Constants.MEDIA_INFORMATION;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.h2.mvstore.MVMap;
import org.jdesktop.observablecollections.ObservableCollections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinymediamanager.core.AbstractModelObject;
import org.tinymediamanager.core.Constants;
import org.tinymediamanager.core.MediaFileType;
import org.tinymediamanager.core.MediaSource;
import org.tinymediamanager.core.Message;
import org.tinymediamanager.core.Message.MessageLevel;
import org.tinymediamanager.core.MessageManager;
import org.tinymediamanager.core.Utils;
import org.tinymediamanager.core.entities.MediaFile;
import org.tinymediamanager.core.entities.MediaFileAudioStream;
import org.tinymediamanager.core.movie.entities.Movie;
import org.tinymediamanager.core.movie.entities.MovieSet;
import org.tinymediamanager.scraper.MediaScraper;
import org.tinymediamanager.scraper.MediaSearchOptions;
import org.tinymediamanager.scraper.MediaSearchResult;
import org.tinymediamanager.scraper.ScraperType;
import org.tinymediamanager.scraper.entities.Certification;
import org.tinymediamanager.scraper.entities.MediaLanguages;
import org.tinymediamanager.scraper.entities.MediaType;
import org.tinymediamanager.scraper.mediaprovider.IMovieMetadataProvider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;

import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.ObservableElementList;

/**
 * The Class MovieList.
 * 
 * @author Manuel Laggner
 */
public class MovieList extends AbstractModelObject {
    private static final Logger LOGGER = LoggerFactory.getLogger(MovieList.class);
    private static MovieList instance;

    private final MovieSettings movieSettings;
    private ObservableElementList<Movie> movieList;
    private List<MovieSet> movieSetList = new ArrayList<MovieSet>(0);
    private PropertyChangeListener tagListener;
    private List<String> tagsObservable;
    private List<String> videoCodecsObservable;
    private List<String> audioCodecsObservable;
    private List<Certification> certificationsObservable;
    private final Comparator<MovieSet> movieSetComparator = new MovieSetComparator();

    /**
     * Instantiates a new movie list.
     */
    private MovieList() {
        // create all lists
        tagsObservable = ObservableCollections.observableList(new CopyOnWriteArrayList<String>());
        videoCodecsObservable = ObservableCollections.observableList(new CopyOnWriteArrayList<String>());
        audioCodecsObservable = ObservableCollections.observableList(new CopyOnWriteArrayList<String>());
        certificationsObservable = ObservableCollections.observableList(new CopyOnWriteArrayList<Certification>());

        // the tag listener: its used to always have a full list of all tags used in tmm
        tagListener = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                // listen to changes of tags
                if ("tag".equals(evt.getPropertyName())) {
                    Movie movie = (Movie) evt.getSource();
                    updateTags(movie);
                }
                if (MEDIA_FILES.equals(evt.getPropertyName()) || MEDIA_INFORMATION.equals(evt.getPropertyName())) {
                    Movie movie = (Movie) evt.getSource();
                    updateMediaInformationLists(movie);
                }
                if (CERTIFICATION.equals(evt.getPropertyName())) {
                    Movie movie = (Movie) evt.getSource();
                    updateCertifications(movie);
                }
            }
        };

        movieSettings = MovieModuleManager.MOVIE_SETTINGS;
    }

    /**
     * Gets the single instance of MovieList.
     * 
     * @return single instance of MovieList
     */
    public synchronized static MovieList getInstance() {
        if (MovieList.instance == null) {
            MovieList.instance = new MovieList();
        }
        return MovieList.instance;
    }

    /**
     * Adds the movie.
     * 
     * @param movie
     *          the movie
     */
    public void addMovie(Movie movie) {
        if (!movieList.contains(movie)) {
            int oldValue = movieList.size();
            movieList.add(movie);

            updateTags(movie);
            movie.addPropertyChangeListener(tagListener);
            firePropertyChange("movies", null, movieList);
            firePropertyChange("movieCount", oldValue, movieList.size());
        }
    }

    /**
     * Removes the datasource.
     * 
     * @param path
     *          the path
     */
    public void removeDatasource(String path) {
        if (StringUtils.isEmpty(path)) {
            return;
        }

        List<Movie> moviesToRemove = new ArrayList<>();
        for (int i = movieList.size() - 1; i >= 0; i--) {
            Movie movie = movieList.get(i);
            if (new File(path).equals(new File(movie.getDataSource()))) {
                moviesToRemove.add(movie);
            }
        }

        removeMovies(moviesToRemove);
    }

    /**
     * Gets the unscraped movies.
     * 
     * @return the unscraped movies
     */
    public List<Movie> getUnscrapedMovies() {
        List<Movie> unscrapedMovies = new ArrayList<>();
        for (Movie movie : movieList) {
            if (!movie.isScraped()) {
                unscrapedMovies.add(movie);
            }
        }
        return unscrapedMovies;
    }

    /**
     * Gets the new movies or movies with new files
     * 
     * @return the new movies
     */
    public List<Movie> getNewMovies() {
        List<Movie> newMovies = new ArrayList<>();
        for (Movie movie : movieList) {
            if (movie.isNewlyAdded()) {
                newMovies.add(movie);
            }
        }
        return newMovies;
    }

    /**
     * remove given movies from the database
     * 
     * @param movies
     *          list of movies to remove
     */
    public void removeMovies(List<Movie> movies) {
        if (movies == null || movies.size() == 0) {
            return;
        }
        Set<MovieSet> modifiedMovieSets = new HashSet<>();
        int oldValue = movieList.size();

        // remove in inverse order => performance
        for (int i = movies.size() - 1; i >= 0; i--) {
            Movie movie = movies.get(i);
            movieList.remove(movie);
            if (movie.getMovieSet() != null) {
                MovieSet movieSet = movie.getMovieSet();

                movieSet.removeMovie(movie);
                modifiedMovieSets.add(movieSet);
                movie.setMovieSet(null);
            }
            try {
                MovieModuleManager.getInstance().removeMovieFromDb(movie);
            } catch (Exception e) {
                LOGGER.error("Error removing movie from DB: " + e.getMessage());
            }
        }

        // and now check if any of the modified moviesets are worth for deleting
        for (MovieSet movieSet : modifiedMovieSets) {
            if (movieSet.getMovies().isEmpty()) {
                removeMovieSet(movieSet);
            }
        }

        firePropertyChange("movies", null, movieList);
        firePropertyChange("movieCount", oldValue, movieList.size());
    }

    /**
     * delete the given movies from the database and physically
     * 
     * @param movies
     *          list of movies to delete
     */
    public void deleteMovies(List<Movie> movies) {
        if (movies == null || movies.size() == 0) {
            return;
        }
        Set<MovieSet> modifiedMovieSets = new HashSet<>();
        int oldValue = movieList.size();

        // remove in inverse order => performance
        for (int i = movies.size() - 1; i >= 0; i--) {
            Movie movie = movies.get(i);
            movie.deleteFilesSafely();
            movieList.remove(movie);
            if (movie.getMovieSet() != null) {
                MovieSet movieSet = movie.getMovieSet();
                movieSet.removeMovie(movie);
                modifiedMovieSets.add(movieSet);
                movie.setMovieSet(null);
            }
            try {
                MovieModuleManager.getInstance().removeMovieFromDb(movie);
            } catch (Exception e) {
                LOGGER.error("Error removing movie from DB: " + e.getMessage());
            }
        }

        // and now check if any of the modified moviesets are worth for deleting
        for (MovieSet movieSet : modifiedMovieSets) {
            removeMovieSet(movieSet);
        }

        firePropertyChange("movies", null, movieList);
        firePropertyChange("movieCount", oldValue, movieList.size());
    }

    /**
     * Gets the movies.
     * 
     * @return the movies
     */
    public ObservableElementList<Movie> getMovies() {
        if (movieList == null) {
            movieList = new ObservableElementList<>(GlazedLists.threadSafeList(new BasicEventList<Movie>()),
                    GlazedLists.beanConnector(Movie.class));
        }
        return movieList;
    }

    /**
     * Load movies from database.
     */
    void loadMoviesFromDatabase(MVMap<UUID, String> movieMap, ObjectMapper objectMapper) {
        // load movies
        movieList = new ObservableElementList<>(GlazedLists.threadSafeList(new BasicEventList<Movie>()),
                GlazedLists.beanConnector(Movie.class));
        ObjectReader movieObjectReader = objectMapper.readerFor(Movie.class);

        for (UUID uuid : movieMap.keyList()) {
            String json = "";
            try {
                json = movieMap.get(uuid);
                Movie movie = movieObjectReader.readValue(json);
                movie.setDbId(uuid);
                // for performance reasons we add movies directly
                movieList.add(movie);
            } catch (Exception e) {
                LOGGER.warn("problem decoding movie json string: ", e);
            }
        }
        LOGGER.info("found " + movieList.size() + " movies in database");
    }

    void loadMovieSetsFromDatabase(MVMap<UUID, String> movieSetMap, ObjectMapper objectMapper) {
        // load movie sets
        movieSetList = ObservableCollections
                .observableList(Collections.synchronizedList(new ArrayList<MovieSet>()));
        ObjectReader movieSetObjectReader = objectMapper.readerFor(MovieSet.class);

        for (UUID uuid : movieSetMap.keyList()) {
            try {
                MovieSet movieSet = movieSetObjectReader.readValue(movieSetMap.get(uuid));
                movieSet.setDbId(uuid);
                // for performance reasons we add movies sets directly
                movieSetList.add(movieSet);
            } catch (Exception e) {
                LOGGER.warn("problem decoding movie set json string: ", e);
            }
        }

        LOGGER.info("found " + movieSetList.size() + " movieSets in database");
    }

    void initDataAfterLoading() {
        // remove invalid movies which have no VIDEO files
        checkAndCleanupMediaFiles();

        // 3. initialize movies/movie sets (e.g. link with each others)
        for (Movie movie : movieList) {
            movie.initializeAfterLoading();
            updateTags(movie);
            updateMediaInformationLists(movie);
            updateCertifications(movie);
            movie.addPropertyChangeListener(tagListener);
        }

        for (MovieSet movieSet : movieSetList) {
            movieSet.initializeAfterLoading();
        }
    }

    public void persistMovie(Movie movie) {
        // remove this movie from the database
        try {
            MovieModuleManager.getInstance().persistMovie(movie);
        } catch (Exception e) {
            LOGGER.error("failed to persist movie: " + movie.getTitle());
        }
    }

    public void removeMovieFromDb(Movie movie) {
        // remove this movie from the database
        try {
            MovieModuleManager.getInstance().removeMovieFromDb(movie);
        } catch (Exception e) {
            LOGGER.error("failed to remove movie: " + movie.getTitle());
        }
    }

    public void persistMovieSet(MovieSet movieSet) {
        // remove this movie set from the database
        try {
            MovieModuleManager.getInstance().persistMovieSet(movieSet);
        } catch (Exception e) {
            LOGGER.error("failed to persist movie set: " + movieSet.getTitle());
        }
    }

    public void removeMovieSetFromDb(MovieSet movieSet) {
        // remove this movie set from the database
        try {
            MovieModuleManager.getInstance().removeMovieSetFromDb(movieSet);
        } catch (Exception e) {
            LOGGER.error("failed to remove movie set: " + movieSet.getTitle());
        }
    }

    public MovieSet lookupMovieSet(UUID uuid) {
        for (MovieSet movieSet : movieSetList) {
            if (movieSet.getDbId().equals(uuid)) {
                return movieSet;
            }
        }
        return null;
    }

    public Movie lookupMovie(UUID uuid) {
        for (Movie movie : movieList) {
            if (movie.getDbId().equals(uuid)) {
                return movie;
            }
        }
        return null;
    }

    /**
     * Gets the movie by path.
     * 
     * @param path
     *          the path
     * @return the movie by path
     * @deprecated use Java7 getMovieByPath(Path path) instead.
     */
    @Deprecated
    public synchronized Movie getMovieByPath(File path) {
        return getMovieByPath(path.toPath());
    }

    /**
     * Gets the movie by path.
     * 
     * @param path
     *          the path
     * @return the movie by path
     */
    public synchronized Movie getMovieByPath(Path path) {

        for (Movie movie : movieList) {
            if (movie.getPathNIO().compareTo(path.toAbsolutePath()) == 0) {
                LOGGER.debug(
                        "Ok, found already existing movie '" + movie.getTitle() + "' in DB (path: " + path + ")");
                return movie;
            }
        }

        return null;
    }

    /**
     * Gets a list of movies by same path.
     * 
     * @param path
     *          the path
     * @return the movie list
     * @deprecated use Java7 getMoviesByPath(Path path) instead.
     */
    @Deprecated
    public synchronized List<Movie> getMoviesByPath(File path) {
        return getMoviesByPath(path.toPath());
    }

    /**
     * Gets a list of movies by same path.
     * 
     * @param path
     *          the path
     * @return the movie list
     */
    public synchronized List<Movie> getMoviesByPath(Path path) {
        ArrayList<Movie> movies = new ArrayList<>();
        for (Movie movie : movieList) {
            if (Paths.get(movie.getPath()).compareTo(path) == 0) {
                movies.add(movie);
            }
        }
        return movies;
    }

    /**
     * Search for a movie with the default settings.
     * 
     * @param searchTerm
     *          the search term
     * @param movie
     *          the movie
     * @param metadataScraper
     *          the media scraper
     * @return the list
     */
    public List<MediaSearchResult> searchMovie(String searchTerm, Movie movie, MediaScraper metadataScraper) {
        return searchMovie(searchTerm, movie, metadataScraper, movieSettings.getScraperLanguage());
    }

    /**
     * Search movie with the chosen language.
     * 
     * @param searchTerm
     *          the search term
     * @param movie
     *          the movie
     * @param mediaScraper
     *          the media scraper
     * @param langu
     *          the language to search with
     * @return the list
     */
    public List<MediaSearchResult> searchMovie(String searchTerm, Movie movie, MediaScraper mediaScraper,
            MediaLanguages langu) {
        List<MediaSearchResult> sr = null;
        try {
            IMovieMetadataProvider provider;
            if (mediaScraper == null) {
                provider = (IMovieMetadataProvider) getDefaultMediaScraper().getMediaProvider();
            } else {
                provider = (IMovieMetadataProvider) mediaScraper.getMediaProvider();
            }

            boolean idFound = false;
            // set what we have, so the provider could chose from all :)
            MediaSearchOptions options = new MediaSearchOptions(MediaType.MOVIE);
            options.setLanguage(LocaleUtils.toLocale(langu.name()));
            options.setCountry(movieSettings.getCertificationCountry());
            if (movie != null) {
                if (Utils.isValidImdbId(movie.getImdbId())) {
                    options.setImdbId(movie.getImdbId());
                    idFound = true;
                }
                if (movie.getTmdbId() != 0) {
                    options.setTmdbId(movie.getTmdbId());
                    idFound = true;
                }
                options.setQuery(movie.getTitle());
                if (!movie.getYear().isEmpty()) {
                    try {
                        options.setYear(Integer.parseInt(movie.getYear()));
                    } catch (Exception ignored) {
                    }
                }
            }
            if (!searchTerm.isEmpty()) {
                if (idFound) {
                    // id found, so search for it
                    // except when searchTerm differs from movie title (we entered something to search for)
                    if (!searchTerm.equals(movie.getTitle())) {
                        options.setQuery(searchTerm);
                    }
                } else {
                    options.setQuery(searchTerm);
                }
            }

            LOGGER.info("=====================================================");
            LOGGER.info("Searching with scraper: " + provider.getProviderInfo().getId() + ", "
                    + provider.getProviderInfo().getVersion());
            LOGGER.info(options.toString());
            LOGGER.info("=====================================================");
            sr = provider.search(options);
            // if result is empty, try all scrapers
            if (sr.isEmpty() && movieSettings.isScraperFallback()) {
                for (MediaScraper ms : getAvailableMediaScrapers()) {
                    if (!ms.isEnabled()
                            || provider.getProviderInfo().equals(ms.getMediaProvider().getProviderInfo())
                            || ms.getMediaProvider().getProviderInfo().getName().startsWith("Kodi")) {
                        continue;
                    }
                    LOGGER.info("no result yet - trying alternate scraper: " + ms.getName());
                    try {
                        LOGGER.info("=====================================================");
                        LOGGER.info("Searching with alternate scraper: "
                                + ms.getMediaProvider().getProviderInfo().getId() + ", "
                                + provider.getProviderInfo().getVersion());
                        LOGGER.info(options.toString());
                        LOGGER.info("=====================================================");
                        sr = ((IMovieMetadataProvider) ms.getMediaProvider()).search(options);
                    } catch (Exception e) {
                        LOGGER.error("searchMovieFallback", e);
                        MessageManager.instance.pushMessage(new Message(MessageLevel.ERROR, movie,
                                "message.movie.searcherror", new String[] { ":", e.getLocalizedMessage() }));
                    }
                    if (!sr.isEmpty()) {
                        break;
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("searchMovie", e);
            MessageManager.instance.pushMessage(new Message(MessageLevel.ERROR, movie, "message.movie.searcherror",
                    new String[] { ":", e.getLocalizedMessage() }));
        }

        return sr;
    }

    public List<MediaScraper> getAvailableMediaScrapers() {
        List<MediaScraper> availableScrapers = MediaScraper.getMediaScrapers(ScraperType.MOVIE);
        Collections.sort(availableScrapers, new MovieMediaScraperComparator());
        return availableScrapers;
    }

    public MediaScraper getDefaultMediaScraper() {
        MediaScraper scraper = MediaScraper.getMediaScraperById(movieSettings.getMovieScraper(), ScraperType.MOVIE);
        if (scraper == null) {
            scraper = MediaScraper.getMediaScraperById(Constants.TMDB, ScraperType.MOVIE);
        }
        return scraper;
    }

    public MediaScraper getMediaScraperById(String providerId) {
        return MediaScraper.getMediaScraperById(providerId, ScraperType.MOVIE);
    }

    /**
     * get all available artwork scrapers.
     * 
     * @return the artwork scrapers
     */
    public List<MediaScraper> getAvailableArtworkScrapers() {
        List<MediaScraper> availableScrapers = MediaScraper.getMediaScrapers(ScraperType.MOVIE_ARTWORK);
        // we can use the MovieMediaScraperComparator here too, since TMDB should also be first
        Collections.sort(availableScrapers, new MovieMediaScraperComparator());
        return availableScrapers;
    }

    /**
     * get all specified artwork scrapers
     * 
     * @param providerIds
     *          a list of all specified scraper ids
     * @return the specified artwork scrapers
     */
    public List<MediaScraper> getArtworkScrapers(List<String> providerIds) {
        List<MediaScraper> artworkScrapers = new ArrayList<>();

        for (String providerId : providerIds) {
            if (StringUtils.isBlank(providerId)) {
                continue;
            }
            MediaScraper artworkScraper = MediaScraper.getMediaScraperById(providerId, ScraperType.MOVIE_ARTWORK);
            if (artworkScraper != null) {
                artworkScrapers.add(artworkScraper);
            }
        }

        return artworkScrapers;
    }

    /**
     * get all default (specified via settings) artwork scrapers
     * 
     * @return the specified artwork scrapers
     */
    public List<MediaScraper> getDefaultArtworkScrapers() {
        return getArtworkScrapers(movieSettings.getMovieArtworkScrapers());
    }

    /**
     * all available trailer scrapers.
     * 
     * @return the trailer scrapers
     */
    public List<MediaScraper> getAvailableTrailerScrapers() {
        List<MediaScraper> availableScrapers = MediaScraper.getMediaScrapers(ScraperType.MOVIE_TRAILER);
        // we can use the MovieMediaScraperComparator here too, since TMDB should also be first
        Collections.sort(availableScrapers, new MovieMediaScraperComparator());
        return availableScrapers;
    }

    /**
     * get all default (specified via settings) trailer scrapers
     * 
     * @return the specified trailer scrapers
     */
    public List<MediaScraper> getDefaultTrailerScrapers() {
        return getTrailerScrapers(movieSettings.getMovieTrailerScrapers());
    }

    /**
     * get all specified trailer scrapers.
     * 
     * @param providerIds
     *          the scrapers
     * @return the trailer providers
     */
    public List<MediaScraper> getTrailerScrapers(List<String> providerIds) {
        List<MediaScraper> trailerScrapers = new ArrayList<>();

        for (String providerId : providerIds) {
            if (StringUtils.isBlank(providerId)) {
                continue;
            }
            MediaScraper trailerScraper = MediaScraper.getMediaScraperById(providerId, ScraperType.MOVIE_TRAILER);
            if (trailerScraper != null) {
                trailerScrapers.add(trailerScraper);
            }
        }

        return trailerScrapers;
    }

    /**
     * all available subtitle scrapers.
     *
     * @return the subtitle scrapers
     */
    public List<MediaScraper> getAvailableSubtitleScrapers() {
        List<MediaScraper> availableScrapers = MediaScraper.getMediaScrapers(ScraperType.SUBTITLE);
        Collections.sort(availableScrapers, new MovieMediaScraperComparator());
        return availableScrapers;
    }

    /**
     * get all default (specified via settings) subtitle scrapers
     *
     * @return the specified subtitle scrapers
     */
    public List<MediaScraper> getDefaultSubtitleScrapers() {
        return getSubtitleScrapers(movieSettings.getMovieSubtitleScrapers());
    }

    /**
     * get all specified subtitle scrapers.
     *
     * @param providerIds
     *          the scrapers
     * @return the subtitle scrapers
     */
    public List<MediaScraper> getSubtitleScrapers(List<String> providerIds) {
        List<MediaScraper> subtitleScrapers = new ArrayList<>();

        for (String providerId : providerIds) {
            if (StringUtils.isBlank(providerId)) {
                continue;
            }
            MediaScraper subtitleScraper = MediaScraper.getMediaScraperById(providerId, ScraperType.SUBTITLE);
            if (subtitleScraper != null) {
                subtitleScrapers.add(subtitleScraper);
            }
        }

        return subtitleScrapers;
    }

    /**
     * Gets the movie count.
     * 
     * @return the movie count
     */
    public int getMovieCount() {
        int size = movieList.size();
        return size;
    }

    /**
     * Gets the movie set count.
     * 
     * @return the movie set count
     */
    public int getMovieSetCount() {
        int size = movieSetList.size();
        return size;
    }

    /**
     * Gets the tags in movies.
     * 
     * @return the tags in movies
     */
    public List<String> getTagsInMovies() {
        return tagsObservable;
    }

    /**
     * Update tags used in movies.
     * 
     * @param movie
     *          the movie
     */
    private void updateTags(Movie movie) {
        List<String> availableTags = new ArrayList<>(tagsObservable);
        for (String tagInMovie : new ArrayList<>(movie.getTags())) {
            boolean tagFound = false;
            for (String tag : availableTags) {
                if (tagInMovie.equals(tag)) {
                    tagFound = true;
                    break;
                }
            }
            if (!tagFound) {
                addTag(tagInMovie);
            }
        }
    }

    /**
     * Update media information used in movies.
     * 
     * @param movie
     *          the movie
     */
    private void updateMediaInformationLists(Movie movie) {
        // video codec
        List<String> availableCodecs = new ArrayList<>(videoCodecsObservable);
        for (MediaFile mf : movie.getMediaFiles(MediaFileType.VIDEO)) {
            String codec = mf.getVideoCodec();
            boolean codecFound = false;

            for (String mfCodec : availableCodecs) {
                if (mfCodec.equals(codec)) {
                    codecFound = true;
                    break;
                }
            }

            if (!codecFound) {
                addVideoCodec(codec);
            }
        }

        // audio codec
        availableCodecs = new ArrayList<>(audioCodecsObservable);
        for (MediaFile mf : movie.getMediaFiles(MediaFileType.VIDEO)) {
            for (MediaFileAudioStream audio : mf.getAudioStreams()) {
                String codec = audio.getCodec();
                boolean codecFound = false;
                for (String mfCodec : availableCodecs) {
                    if (mfCodec.equals(codec)) {
                        codecFound = true;
                        break;
                    }
                }

                if (!codecFound) {
                    addAudioCodec(codec);
                }
            }
        }
    }

    private void updateCertifications(Movie movie) {
        if (!certificationsObservable.contains(movie.getCertification())) {
            addCertification(movie.getCertification());
        }
    }

    public List<String> getVideoCodecsInMovies() {
        return videoCodecsObservable;
    }

    public List<String> getAudioCodecsInMovies() {
        return audioCodecsObservable;
    }

    public List<Certification> getCertificationsInMovies() {
        return certificationsObservable;
    }

    /**
     * Adds the tag.
     * 
     * @param newTag
     *          the new tag
     */
    private void addTag(String newTag) {
        if (StringUtils.isBlank(newTag)) {
            return;
        }

        synchronized (tagsObservable) {
            if (tagsObservable.contains(newTag)) {
                return;
            }
            tagsObservable.add(newTag);
        }

        firePropertyChange("tag", null, tagsObservable);
    }

    private void addVideoCodec(String newCodec) {
        if (StringUtils.isBlank(newCodec)) {
            return;
        }

        synchronized (videoCodecsObservable) {
            if (videoCodecsObservable.contains(newCodec)) {
                return;
            }
            videoCodecsObservable.add(newCodec);
        }

        firePropertyChange("videoCodec", null, videoCodecsObservable);
    }

    private void addAudioCodec(String newCodec) {
        if (StringUtils.isBlank(newCodec)) {
            return;
        }

        synchronized (audioCodecsObservable) {
            if (audioCodecsObservable.contains(newCodec)) {
                return;
            }
            audioCodecsObservable.add(newCodec);
        }

        firePropertyChange("audioCodec", null, audioCodecsObservable);
    }

    private void addCertification(Certification newCert) {
        if (newCert == null) {
            return;
        }

        synchronized (certificationsObservable) {
            if (certificationsObservable.contains(newCert)) {
                return;
            }
            certificationsObservable.add(newCert);
        }

        firePropertyChange("certification", null, certificationsObservable);
    }

    /**
     * Search duplicates.
     */
    public void searchDuplicates() {
        Map<String, Movie> imdbDuplicates = new HashMap<>();
        Map<Integer, Movie> tmdbDuplicates = new HashMap<>();

        for (Movie movie : movieList) {
            movie.clearDuplicate();

            // imdb duplicate search only works with given imdbid
            if (StringUtils.isNotEmpty(movie.getImdbId())) {
                // is there a movie with this imdbid sotred?
                if (imdbDuplicates.containsKey(movie.getImdbId())) {
                    // yes - set duplicate flag on both movies
                    movie.setDuplicate();
                    Movie movie2 = imdbDuplicates.get(movie.getImdbId());
                    movie2.setDuplicate();
                } else {
                    // no, store movie
                    imdbDuplicates.put(movie.getImdbId(), movie);
                }
            }

            // tmdb duplicate search only works with with given tmdb id
            if (movie.getTmdbId() > 0) {
                // is there a movie with this tmdbid sotred?
                if (tmdbDuplicates.containsKey(movie.getTmdbId())) {
                    // yes - set duplicate flag on both movies
                    movie.setDuplicate();
                    Movie movie2 = tmdbDuplicates.get(movie.getTmdbId());
                    movie2.setDuplicate();
                } else {
                    // no, store movie
                    tmdbDuplicates.put(movie.getTmdbId(), movie);
                }
            }
        }
    }

    /**
     * Gets the movie set list.
     * 
     * @return the movieSetList
     */
    public List<MovieSet> getMovieSetList() {
        if (movieSetList == null) {
            movieSetList = ObservableCollections
                    .observableList(Collections.synchronizedList(new ArrayList<MovieSet>()));
        }
        return movieSetList;
    }

    /**
     * get the movie set list in a sorted order
     * 
     * @return the movie set list (sorted)
     */
    public List<MovieSet> getSortedMovieSetList() {
        List<MovieSet> sortedMovieSets = new ArrayList<>(getMovieSetList());
        Collections.sort(sortedMovieSets, movieSetComparator);
        return sortedMovieSets;
    }

    /**
     * Sets the movie set list.
     * 
     * @param movieSetList
     *          the movieSetList to set
     */
    public void setMovieSetList(ObservableElementList<MovieSet> movieSetList) {
        this.movieSetList = movieSetList;
    }

    /**
     * Adds the movie set.
     * 
     * @param movieSet
     *          the movie set
     */
    public void addMovieSet(MovieSet movieSet) {
        int oldValue = movieSetList.size();
        this.movieSetList.add(movieSet);
        firePropertyChange("addedMovieSet", null, movieSet);
        firePropertyChange("movieSetCount", oldValue, movieSetList.size());
    }

    /**
     * Removes the movie set.
     * 
     * @param movieSet
     *          the movie set
     */
    public void removeMovieSet(MovieSet movieSet) {
        int oldValue = movieSetList.size();
        movieSet.removeAllMovies();

        try {
            movieSetList.remove(movieSet);
            MovieModuleManager.getInstance().removeMovieSetFromDb(movieSet);
        } catch (Exception e) {
            LOGGER.error("Error removing movie set from DB: " + e.getMessage());
        }

        firePropertyChange("removedMovieSet", null, movieSet);
        firePropertyChange("movieSetCount", oldValue, movieSetList.size());
    }

    private MovieSet findMovieSet(String title, int tmdbId) {
        // first search by tmdbId
        if (tmdbId > 0) {
            for (MovieSet movieSet : movieSetList) {
                if (movieSet.getTmdbId() == tmdbId) {
                    return movieSet;
                }
            }
        }

        // search for the movieset by name
        for (MovieSet movieSet : movieSetList) {
            if (movieSet.getTitle().equals(title)) {
                return movieSet;
            }
        }

        return null;
    }

    public synchronized MovieSet getMovieSet(String title, int tmdbId) {
        MovieSet movieSet = findMovieSet(title, tmdbId);

        if (movieSet == null && StringUtils.isNotBlank(title)) {
            movieSet = new MovieSet(title);
            movieSet.saveToDb();
            addMovieSet(movieSet);
        }

        return movieSet;
    }

    /**
     * Sort movies in movie set.
     * 
     * @param movieSet
     *          the movie set
     */
    public void sortMoviesInMovieSet(MovieSet movieSet) {
        if (movieSet.getMovies().size() > 1) {
            movieSet.sortMovies();
        }
        firePropertyChange("sortedMovieSets", null, movieSetList);
    }

    /**
     * check if there are movies without (at least) one VIDEO mf
     */
    private void checkAndCleanupMediaFiles() {
        List<Movie> moviesToRemove = new ArrayList<>();
        for (Movie movie : movieList) {
            List<MediaFile> mfs = movie.getMediaFiles(MediaFileType.VIDEO);
            if (mfs.isEmpty()) {
                // mark movie for removal
                moviesToRemove.add(movie);
            }
        }

        if (!moviesToRemove.isEmpty()) {
            removeMovies(moviesToRemove);
            LOGGER.warn("movies without VIDEOs detected");

            // and push a message
            // also delay it so that the UI has time to start up
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(15000);
                    } catch (Exception ignored) {
                    }
                    Message message = new Message(MessageLevel.SEVERE, "tmm.movies",
                            "message.database.corrupteddata");
                    MessageManager.instance.pushMessage(message);
                }
            });
            thread.start();
        }
    }

    /**
     * invalidate the title sortable upon changes to the sortable prefixes
     */
    public void invalidateTitleSortable() {
        for (Movie movie : new ArrayList<>(movieList)) {
            movie.clearTitleSortable();
        }
    }

    /**
     * create a new offline movie with the given title in the specified data source
     * 
     * @param title
     *          the given title
     * @param datasource
     *          the data source to create the offline movie in
     */
    public void addOfflineMovie(String title, String datasource) {
        addOfflineMovie(title, datasource, MediaSource.UNKNOWN);
    }

    /**
     * create a new offline movie with the given title in the specified data source
     * 
     * @param title
     *          the given title
     * @param datasource
     *          the data source to create the offline movie in
     * @param mediaSource
     *          the media source to be set for the offline movie
     */
    public void addOfflineMovie(String title, String datasource, MediaSource mediaSource) {
        // first crosscheck if the data source is in our settings
        if (!movieSettings.getMovieDataSource().contains(datasource)) {
            return;
        }

        // check if there is already an identical stub folder
        int i = 1;
        Path stubFolder = Paths.get(datasource, title);
        while (Files.exists(stubFolder)) {
            stubFolder = Paths.get(datasource, title + "(" + i++ + ")");
        }

        Path stubFile = stubFolder.resolve(title + ".disc");

        // create the stub file
        try {
            Files.createDirectory(stubFolder);
            Files.createFile(stubFile);
        } catch (IOException e) {
            LOGGER.error("could not create stub file: " + e.getMessage());
            return;
        }

        // create a movie and set it as MF
        MediaFile mf = new MediaFile(stubFile);
        mf.gatherMediaInformation();
        Movie movie = new Movie();

        movie.setTitle(title);
        movie.setPath(stubFolder.toAbsolutePath().toString());
        movie.setDataSource(datasource);
        movie.setMediaSource(mediaSource);
        movie.setDateAdded(new Date());
        movie.addToMediaFiles(mf);
        movie.setOffline(true);
        movie.setNewlyAdded(true);
        addMovie(movie);
        movie.saveToDb();
    }

    private class MovieSetComparator implements Comparator<MovieSet> {
        @Override
        public int compare(MovieSet o1, MovieSet o2) {
            if (o1 == null || o2 == null || o1.getTitleSortable() == null || o2.getTitleSortable() == null) {
                return 0;
            }
            return o1.getTitleSortable().compareToIgnoreCase(o2.getTitleSortable());
        }
    }

    private class MovieMediaScraperComparator implements Comparator<MediaScraper> {
        @Override
        public int compare(MediaScraper o1, MediaScraper o2) {
            return o1.getId().compareTo(o2.getId());
        }
    }
}