com.moviejukebox.MovieJukebox.java Source code

Java tutorial

Introduction

Here is the source code for com.moviejukebox.MovieJukebox.java

Source

/*
 *      Copyright (c) 2004-2016 YAMJ Members
 *      https://github.com/orgs/YAMJ/people
 *
 *      This file is part of the Yet Another Movie Jukebox (YAMJ) project.
 *
 *      YAMJ is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      any later version.
 *
 *      YAMJ is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with YAMJ.  If not, see <http://www.gnu.org/licenses/>.
 *
 *      Web: https://github.com/YAMJ/yamj-v2
 *
 */
package com.moviejukebox;

import static com.moviejukebox.tools.PropertiesUtil.*;
import static com.moviejukebox.tools.StringTools.*;

import com.moviejukebox.model.*;
import com.moviejukebox.model.artwork.ArtworkType;
import com.moviejukebox.model.comparator.PersonComparator;
import com.moviejukebox.model.enumerations.DirtyFlag;
import com.moviejukebox.model.enumerations.JukeboxStatistic;
import com.moviejukebox.model.enumerations.OverrideFlag;
import com.moviejukebox.plugin.*;
import com.moviejukebox.reader.MovieJukeboxLibraryReader;
import com.moviejukebox.reader.MovieJukeboxXMLReader;
import com.moviejukebox.scanner.*;
import com.moviejukebox.scanner.artwork.*;
import com.moviejukebox.tools.*;
import com.moviejukebox.tools.PropertiesUtil.KeywordMap;
import com.moviejukebox.tools.cache.CacheMemory;
import com.moviejukebox.writer.CompleteMoviesWriter;
import com.moviejukebox.writer.MovieJukeboxHTMLWriter;
import com.moviejukebox.writer.MovieJukeboxXMLWriter;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.PropertyConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MovieJukebox {

    private static final String LOG_FILENAME = "moviejukebox";
    private static final Logger LOG = LoggerFactory.getLogger(MovieJukebox.class);
    private static Collection<MediaLibraryPath> mediaLibraryPaths;
    private static final String SKIN_DIR = "mjb.skin.dir";
    private static final String EXT_PNG = "png";
    public static final String EXT_DOT_XML = ".xml";
    private static final String DUMMY_JPG = "dummy.jpg";
    private static final String RIGHT = "right";
    private static final String LEFT = "left";
    private static final String BOTH = "both";
    private static final String THUMBNAILS = "thumbnails";
    private static final String POSTERS = "posters";
    private final String movieLibraryRoot;
    private static String userPropertiesName = "./moviejukebox.properties";
    // Jukebox parameters
    private static Jukebox jukebox;
    private static boolean jukeboxPreserve = Boolean.FALSE;
    private static boolean jukeboxClean = Boolean.FALSE;
    // Overwrite flags
    private final boolean forcePosterOverwrite;
    private final boolean forceThumbnailOverwrite;
    private final boolean forceBannerOverwrite;
    private final boolean forceSkinOverwrite;
    private final boolean forceIndexOverwrite;
    private final boolean forceFooterOverwrite;
    // Scanner Tokens
    private static String posterToken;
    private static String thumbnailToken;
    private static String bannerToken;
    private static String wideBannerToken;
    private static String defaultSource;
    private static String fanartToken;
    private static String footerToken;
    private static String posterExtension;
    private static String thumbnailExtension;
    private static String bannerExtension;
    private static String fanartExtension;
    private static Integer footerCount;
    private static final List<String> FOOTER_NAME = new ArrayList<>();
    private static final List<Boolean> FOOTER_ENABLE = new ArrayList<>();
    private static final List<Integer> FOOTER_WIDTH = new ArrayList<>();
    private static final List<Integer> FOOTER_HEIGHT = new ArrayList<>();
    private static final List<String> FOOTER_EXTENSION = new ArrayList<>();
    private static boolean fanartMovieDownload;
    private static boolean fanartTvDownload;
    private static boolean videoimageDownload;
    private static boolean bannerDownload;
    private static boolean photoDownload;
    private static boolean backdropDownload;
    private final boolean setIndexFanart;
    private static boolean skipIndexGeneration = Boolean.FALSE;
    private static boolean skipHtmlGeneration = Boolean.FALSE;
    private static boolean skipPlaylistGeneration = Boolean.FALSE;
    private static boolean dumpLibraryStructure = Boolean.FALSE;
    private static boolean showMemory = Boolean.FALSE;
    private static boolean peopleScan = Boolean.FALSE;
    private static boolean peopleScrape = Boolean.TRUE;
    private static int peopleMax = 10;
    private static int popularity = 5;
    private static String peopleFolder = "";
    private static final Collection<String> PHOTO_EXTENSIONS = new ArrayList<>();
    // These are pulled from the Manifest.MF file that is created by the build script
    private static final GitRepositoryState GIT = new GitRepositoryState();
    private static boolean trailersScannerEnable;
    private static int maxThreadsProcess = 1;
    private static int maxThreadsDownload = 1;
    private static boolean enableWatchScanner;
    private static boolean enableWatchTraktTv;
    private static boolean enableCompleteMovies;
    // Exit codes
    private static final int EXIT_NORMAL = 0;
    private static final int EXIT_SCAN_LIMIT = 1;
    // Directories to exclude from dump command
    private static final String[] EXCLUDED = { "dumpDir", ".svn", "src", "test", "bin", "skins" };
    private static final String SKIN_DEFAULT = "./skins/default";
    private static final String LOG_FINISHED = "Finished: {} ({}/{})";
    private static final String LIT_RESOURCES = "resources";

    public MovieJukebox(String source, String jukeboxRoot) {
        this.movieLibraryRoot = source;
        String jukeboxTempLocation = FileTools
                .getCanonicalPath(PropertiesUtil.getProperty("mjb.jukeboxTempDir", "./temp"));

        String detailsDirName = getProperty("mjb.detailsDirName", "Jukebox");

        jukebox = new Jukebox(jukeboxRoot, jukeboxTempLocation, detailsDirName);

        MovieJukebox.skipPlaylistGeneration = PropertiesUtil.getBooleanProperty("mjb.skipPlaylistGeneration",
                Boolean.FALSE);

        MovieJukebox.fanartMovieDownload = PropertiesUtil.getBooleanProperty("fanart.movie.download",
                Boolean.FALSE);
        MovieJukebox.fanartTvDownload = PropertiesUtil.getBooleanProperty("fanart.tv.download", Boolean.FALSE);

        setIndexFanart = PropertiesUtil.getBooleanProperty("mjb.sets.indexFanart", Boolean.FALSE);

        fanartToken = getProperty("mjb.scanner.fanartToken", ".fanart");
        bannerToken = getProperty("mjb.scanner.bannerToken", ".banner");
        wideBannerToken = getProperty("mjb.scanner.wideBannerToken", ".wide");
        posterToken = getProperty("mjb.scanner.posterToken", "_large");
        thumbnailToken = getProperty("mjb.scanner.thumbnailToken", "_small");
        footerToken = getProperty("mjb.scanner.footerToken", ".footer");

        posterExtension = getProperty("posters.format", EXT_PNG);
        thumbnailExtension = getProperty("thumbnails.format", EXT_PNG);
        bannerExtension = getProperty("banners.format", EXT_PNG);
        fanartExtension = getProperty("fanart.format", "jpg");

        footerCount = PropertiesUtil.getIntProperty("mjb.footer.count", 0);
        for (int i = 0; i < MovieJukebox.footerCount; i++) {
            FOOTER_ENABLE.add(PropertiesUtil.getBooleanProperty("mjb.footer." + i + ".enable", Boolean.FALSE));
            String fName = getProperty("mjb.footer." + i + ".name", "footer." + i);
            FOOTER_NAME.add(fName);
            FOOTER_WIDTH.add(PropertiesUtil.getIntProperty(fName + ".width", 400));
            FOOTER_HEIGHT.add(PropertiesUtil.getIntProperty(fName + ".height", 80));
            FOOTER_EXTENSION.add(getProperty(fName + ".format", EXT_PNG));
        }

        trailersScannerEnable = PropertiesUtil.getBooleanProperty("trailers.scanner.enable", Boolean.TRUE);

        defaultSource = PropertiesUtil.getProperty("filename.scanner.source.default", Movie.UNKNOWN);

        File libraryFile = new File(source);
        if (libraryFile.exists() && libraryFile.isFile() && source.toUpperCase().endsWith("XML")) {
            LOG.debug("Parsing library file : {}", source);
            mediaLibraryPaths = MovieJukeboxLibraryReader.parse(libraryFile);
        } else if (libraryFile.exists() && libraryFile.isDirectory()) {
            LOG.debug("Library path is : {}", source);
            mediaLibraryPaths = new ArrayList<>();
            MediaLibraryPath mlp = new MediaLibraryPath();
            mlp.setPath(source);
            // We'll get the new playerpath value first, then the nmt path so it overrides the default player path
            String playerRootPath = getProperty("mjb.playerRootPath", "");
            if (StringUtils.isBlank(playerRootPath)) {
                playerRootPath = getProperty("mjb.nmtRootPath",
                        "file:///opt/sybhttpd/localhost.drives/SATA_DISK/Video/");
            }
            mlp.setPlayerRootPath(playerRootPath);
            mlp.setScrapeLibrary(Boolean.TRUE);
            mlp.setExcludes(null);
            mediaLibraryPaths.add(mlp);
        }

        // Check to see if we need to read the jukebox_details.xml file and process, otherwise, just create the file.
        JukeboxProperties.readDetailsFile(jukebox, mediaLibraryPaths);

        // Read these properties after the JukeboxProperties have been read to ensure that changes are picked up
        this.forcePosterOverwrite = PropertiesUtil.getBooleanProperty("mjb.forcePostersOverwrite", Boolean.FALSE);
        this.forceThumbnailOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceThumbnailsOverwrite",
                Boolean.FALSE);
        this.forceBannerOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceBannersOverwrite", Boolean.FALSE);
        this.forceSkinOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceSkinOverwrite", Boolean.FALSE);
        this.forceIndexOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceIndexOverwrite", Boolean.FALSE);
        this.forceFooterOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceFooterOverwrite", Boolean.FALSE);
    }

    public static void main(String[] args) throws Throwable {
        JukeboxStatistics.setTimeStart(System.currentTimeMillis());

        // Create the log file name here, so we can change it later (because it's locked
        System.setProperty("file.name", LOG_FILENAME);
        PropertyConfigurator.configure("properties/log4j.properties");

        LOG.info("Yet Another Movie Jukebox {}", GitRepositoryState.getVersion());
        LOG.info("~~~ ~~~~~~~ ~~~~~ ~~~~~~~ {}", StringUtils.repeat("~", GitRepositoryState.getVersion().length()));
        LOG.info("https://github.com/YAMJ/yamj-v2");
        LOG.info("Copyright (c) 2004-2016 YAMJ Members");
        LOG.info("");
        LOG.info("This software is licensed under the GNU General Public License v3+");
        LOG.info("See this page: https://github.com/YAMJ/yamj-v2/wiki/License");
        LOG.info("");
        LOG.info(" Revision SHA: {} {}", GIT.getCommitId(), GIT.getDirty() ? "(Custom Build)" : "");
        LOG.info("  Commit Date: {}", GIT.getCommitTime());
        LOG.info("   Build Date: {}", GIT.getBuildTime());
        LOG.info("");
        LOG.info(" Java Version: {}", GitRepositoryState.getJavaVersion());
        LOG.info("");

        if (!SystemTools.validateInstallation()) {
            LOG.info("ABORTING.");
            return;
        }

        String movieLibraryRoot = null;
        String jukeboxRoot = null;
        Map<String, String> cmdLineProps = new LinkedHashMap<>();

        try {
            for (int i = 0; i < args.length; i++) {
                String arg = args[i];
                if ("-v".equalsIgnoreCase(arg)) {
                    // We've printed the version, so quit now
                    return;
                } else if ("-t".equalsIgnoreCase(arg)) {
                    String pin = args[++i];

                    // load the apikeys.properties file
                    if (!setPropertiesStreamName("./properties/apikeys.properties", Boolean.TRUE)) {
                        return;
                    }

                    // authorize to Trakt.TV
                    TraktTV.getInstance().initialize().authorizeWithPin(pin);

                    // We've authorized access to Trakt.TV, so quit now
                    return;
                } else if ("-o".equalsIgnoreCase(arg)) {
                    jukeboxRoot = args[++i];
                    PropertiesUtil.setProperty("mjb.jukeboxRoot", jukeboxRoot);
                } else if ("-c".equalsIgnoreCase(arg)) {
                    jukeboxClean = Boolean.TRUE;
                    PropertiesUtil.setProperty("mjb.jukeboxClean", TRUE);
                } else if ("-k".equalsIgnoreCase(arg)) {
                    setJukeboxPreserve(Boolean.TRUE);
                } else if ("-p".equalsIgnoreCase(arg)) {
                    userPropertiesName = args[++i];
                } else if ("-i".equalsIgnoreCase(arg)) {
                    skipIndexGeneration = Boolean.TRUE;
                    PropertiesUtil.setProperty("mjb.skipIndexGeneration", TRUE);
                } else if ("-h".equalsIgnoreCase(arg)) {
                    skipHtmlGeneration = Boolean.TRUE;
                    PropertiesUtil.setProperty("mjb.skipHtmlGeneration", Boolean.TRUE);
                } else if ("-dump".equalsIgnoreCase(arg)) {
                    dumpLibraryStructure = Boolean.TRUE;
                } else if ("-memory".equalsIgnoreCase(arg)) {
                    showMemory = Boolean.TRUE;
                    PropertiesUtil.setProperty("mjb.showMemory", Boolean.TRUE);
                } else if (arg.startsWith("-D")) {
                    String propLine = arg.length() > 2 ? arg.substring(2) : args[++i];
                    int propDiv = propLine.indexOf('=');
                    if (-1 != propDiv) {
                        cmdLineProps.put(propLine.substring(0, propDiv), propLine.substring(propDiv + 1));
                    }
                } else if (arg.startsWith("-")) {
                    help();
                    return;
                } else {
                    movieLibraryRoot = args[i];
                }
            }
        } catch (Exception error) {
            LOG.error("Wrong arguments specified");
            help();
            return;
        }

        // Save the name of the properties file for use later
        setProperty("userPropertiesName", userPropertiesName);

        LOG.info("Processing started at {}", new Date());
        LOG.info("");

        // Load the moviejukebox-default.properties file
        if (!setPropertiesStreamName("./properties/moviejukebox-default.properties", Boolean.TRUE)) {
            return;
        }

        // Load the user properties file "moviejukebox.properties"
        // No need to abort if we don't find this file
        // Must be read before the skin, because this may contain an override skin
        setPropertiesStreamName(userPropertiesName, Boolean.FALSE);

        // Grab the skin from the command-line properties
        if (cmdLineProps.containsKey(SKIN_DIR)) {
            setProperty(SKIN_DIR, cmdLineProps.get(SKIN_DIR));
        }

        // Load the skin.properties file
        if (!setPropertiesStreamName(getProperty(SKIN_DIR, SKIN_DEFAULT) + "/skin.properties", Boolean.TRUE)) {
            return;
        }

        // Load the skin-user.properties file (ignore the error)
        setPropertiesStreamName(getProperty(SKIN_DIR, SKIN_DEFAULT) + "/skin-user.properties", Boolean.FALSE);

        // Load the overlay.properties file (ignore the error)
        String overlayRoot = getProperty("mjb.overlay.dir", Movie.UNKNOWN);
        overlayRoot = (PropertiesUtil.getBooleanProperty("mjb.overlay.skinroot", Boolean.TRUE)
                ? (getProperty(SKIN_DIR, SKIN_DEFAULT) + File.separator)
                : "") + (StringTools.isValidString(overlayRoot) ? (overlayRoot + File.separator) : "");
        setPropertiesStreamName(overlayRoot + "overlay.properties", Boolean.FALSE);

        // Load the apikeys.properties file
        if (!setPropertiesStreamName("./properties/apikeys.properties", Boolean.TRUE)) {
            return;
        }

        // This is needed to update the static reference for the API Keys in the pattern formatter
        // because the formatter is initialised before the properties files are read
        FilteringLayout.addApiKeys();

        // Load the rest of the command-line properties
        for (Map.Entry<String, String> propEntry : cmdLineProps.entrySet()) {
            setProperty(propEntry.getKey(), propEntry.getValue());
        }

        // Read the information about the skin
        SkinProperties.readSkinVersion();
        // Display the information about the skin
        SkinProperties.printSkinVersion();

        StringBuilder properties = new StringBuilder("{");
        for (Map.Entry<Object, Object> propEntry : PropertiesUtil.getEntrySet()) {
            properties.append(propEntry.getKey());
            properties.append("=");
            properties.append(propEntry.getValue());
            properties.append(",");
        }
        properties.replace(properties.length() - 1, properties.length(), "}");

        // Print out the properties to the log file.
        LOG.debug("Properties: {}", properties.toString());

        // Check for mjb.skipIndexGeneration and set as necessary
        // This duplicates the "-i" functionality, but allows you to have it in the property file
        skipIndexGeneration = PropertiesUtil.getBooleanProperty("mjb.skipIndexGeneration", Boolean.FALSE);

        if (PropertiesUtil.getBooleanProperty("mjb.people", Boolean.FALSE)) {
            peopleScan = Boolean.TRUE;
            peopleScrape = PropertiesUtil.getBooleanProperty("mjb.people.scrape", Boolean.TRUE);
            peopleMax = PropertiesUtil.getIntProperty("mjb.people.maxCount", 10);
            popularity = PropertiesUtil.getIntProperty("mjb.people.popularity", 5);

            // Issue 1947: Cast enhancement - option to save all related files to a specific folder
            peopleFolder = PropertiesUtil.getProperty("mjb.people.folder", "");
            if (isNotValidString(peopleFolder)) {
                peopleFolder = "";
            } else if (!peopleFolder.endsWith(File.separator)) {
                peopleFolder += File.separator;
            }
            StringTokenizer st = new StringTokenizer(
                    PropertiesUtil.getProperty("photo.scanner.photoExtensions", "jpg,jpeg,gif,bmp,png"), ",;| ");
            while (st.hasMoreTokens()) {
                PHOTO_EXTENSIONS.add(st.nextToken());
            }
        }

        // Check for mjb.skipHtmlGeneration and set as necessary
        // This duplicates the "-h" functionality, but allows you to have it in the property file
        skipHtmlGeneration = PropertiesUtil.getBooleanProperty("mjb.skipHtmlGeneration", Boolean.FALSE);

        // Look for the parameter in the properties file if it's not been set on the command line
        // This way we don't overwrite the setting if it's not found and defaults to FALSE
        showMemory = PropertiesUtil.getBooleanProperty("mjb.showMemory", Boolean.FALSE);

        // This duplicates the "-c" functionality, but allows you to have it in the property file
        jukeboxClean = PropertiesUtil.getBooleanProperty("mjb.jukeboxClean", Boolean.FALSE);

        MovieFilenameScanner.setSkipKeywords(
                tokenizeToArray(getProperty("filename.scanner.skip.keywords", ""), ",;| "),
                PropertiesUtil.getBooleanProperty("filename.scanner.skip.caseSensitive", Boolean.TRUE));
        MovieFilenameScanner.setSkipRegexKeywords(
                tokenizeToArray(getProperty("filename.scanner.skip.keywords.regex", ""), ","),
                PropertiesUtil.getBooleanProperty("filename.scanner.skip.caseSensitive.regex", Boolean.TRUE));
        MovieFilenameScanner.setExtrasKeywords(
                tokenizeToArray(getProperty("filename.extras.keywords", "trailer,extra,bonus"), ",;| "));
        MovieFilenameScanner.setMovieVersionKeywords(tokenizeToArray(
                getProperty("filename.movie.versions.keywords", "remastered,directors cut,extended cut,final cut"),
                ",;|"));
        MovieFilenameScanner.setLanguageDetection(
                PropertiesUtil.getBooleanProperty("filename.scanner.language.detection", Boolean.TRUE));
        final KeywordMap languages = PropertiesUtil.getKeywordMap("filename.scanner.language.keywords", null);
        if (!languages.isEmpty()) {
            MovieFilenameScanner.clearLanguages();
            for (String lang : languages.getKeywords()) {
                String values = languages.get(lang);
                if (values != null) {
                    MovieFilenameScanner.addLanguage(lang, values, values);
                } else {
                    LOG.info("No values found for language code '{}'", lang);
                }
            }
        }
        final KeywordMap sourceKeywords = PropertiesUtil.getKeywordMap("filename.scanner.source.keywords",
                "HDTV,PDTV,DVDRip,DVDSCR,DSRip,CAM,R5,LINE,HD2DVD,DVD,DVD5,DVD9,HRHDTV,MVCD,VCD,TS,VHSRip,BluRay,HDDVD,D-THEATER,SDTV");
        MovieFilenameScanner.setSourceKeywords(sourceKeywords.getKeywords(), sourceKeywords);

        String temp = getProperty("sorting.strip.prefixes");
        if (temp != null) {
            StringTokenizer st = new StringTokenizer(temp, ",");
            while (st.hasMoreTokens()) {
                String token = st.nextToken().trim();
                if (token.startsWith("\"") && token.endsWith("\"")) {
                    token = token.substring(1, token.length() - 1);
                }
                Movie.addSortIgnorePrefixes(token.toLowerCase());
            }
        }

        enableWatchScanner = PropertiesUtil.getBooleanProperty("watched.scanner.enable", Boolean.TRUE);
        enableWatchTraktTv = PropertiesUtil.getBooleanProperty("watched.trakttv.enable", Boolean.FALSE);
        enableCompleteMovies = PropertiesUtil.getBooleanProperty("complete.movies.enable", Boolean.TRUE);

        // Check to see if don't have a root, check the property file
        if (StringTools.isNotValidString(movieLibraryRoot)) {
            movieLibraryRoot = getProperty("mjb.libraryRoot");
            if (StringTools.isValidString(movieLibraryRoot)) {
                LOG.info("Got libraryRoot from properties file: {}", movieLibraryRoot);
            } else {
                LOG.error("No library root found!");
                help();
                return;
            }
        }

        if (jukeboxRoot == null) {
            jukeboxRoot = getProperty("mjb.jukeboxRoot");
            if (jukeboxRoot == null) {
                LOG.info("jukeboxRoot is null in properties file. Please fix this as it may cause errors.");
            } else {
                LOG.info("Got jukeboxRoot from properties file: {}", jukeboxRoot);
            }
        }

        File f = new File(movieLibraryRoot);
        if (f.exists() && f.isDirectory() && jukeboxRoot == null) {
            jukeboxRoot = movieLibraryRoot;
        }

        if (movieLibraryRoot == null) {
            help();
            return;
        }

        if (jukeboxRoot == null) {
            LOG.info("Wrong arguments specified: you must define the jukeboxRoot property (-o) !");
            help();
            return;
        }

        if (!f.exists()) {
            LOG.error("Directory or library configuration file '{}', not found.", movieLibraryRoot);
            return;
        }

        FileTools.initUnsafeChars();
        FileTools.initSubtitleExtensions();

        // make canonical names
        jukeboxRoot = FileTools.getCanonicalPath(jukeboxRoot);
        movieLibraryRoot = FileTools.getCanonicalPath(movieLibraryRoot);
        MovieJukebox ml = new MovieJukebox(movieLibraryRoot, jukeboxRoot);
        if (dumpLibraryStructure) {
            LOG.warn(
                    "WARNING !!! A dump of your library directory structure will be generated for debug purpose. !!! Library won't be built or updated");
            ml.makeDumpStructure();
        } else {
            ml.generateLibrary();
        }

        // Now rename the log files
        renameLogFile();

        if (ScanningLimit.isLimitReached()) {
            LOG.warn("Scanning limit of {} was reached, please re-run to complete processing.",
                    ScanningLimit.getLimit());
            System.exit(EXIT_SCAN_LIMIT);
        } else {
            System.exit(EXIT_NORMAL);
        }
    }

    /**
     * Append the library filename or the date/time to the log filename
     *
     * @param logFilename
     */
    private static void renameLogFile() {
        StringBuilder newLogFilename = new StringBuilder(LOG_FILENAME); // Use the base log filename
        boolean renameFile = Boolean.FALSE;

        String libraryName = "_Library";
        if (PropertiesUtil.getBooleanProperty("mjb.appendLibraryToLogFile", Boolean.FALSE)) {
            renameFile = Boolean.TRUE;
            for (final MediaLibraryPath mediaLibrary : mediaLibraryPaths) {
                if (isValidString(mediaLibrary.getDescription())) {
                    libraryName = "_" + mediaLibrary.getDescription();
                    libraryName = FileTools.makeSafeFilename(libraryName);
                    break;
                }
            }

            newLogFilename.append(libraryName);
        }

        if (PropertiesUtil.getBooleanProperty("mjb.appendDateToLogFile", Boolean.FALSE)) {
            renameFile = Boolean.TRUE;
            newLogFilename.append("_")
                    .append(JukeboxStatistics.getTime(JukeboxStatistics.JukeboxTimes.START, "yyyy-MM-dd-kkmmss"));
        }

        String logDir = PropertiesUtil.getProperty("mjb.logFileDirectory", "");
        if (StringTools.isValidString(logDir)) {
            renameFile = Boolean.TRUE;
            // Add the file separator if we need to
            logDir += logDir.trim().endsWith(File.separator) ? "" : File.separator;

            newLogFilename.insert(0, logDir);
        }

        if (renameFile) {
            // File (or directory) with old name
            File oldLogFile = new File(LOG_FILENAME + ".log");

            // File with new name
            File newLogFile = new File(newLogFilename.toString() + ".log");

            // Try and create the directory if needed, but don't stop the rename if we can't
            if (StringTools.isValidString(logDir)) {
                FileTools.makeDirsForFile(newLogFile);
            }

            // First we need to tell Log4J to change the name of the current log file to something else so it unlocks the file
            System.setProperty("file.name", PropertiesUtil.getProperty("mjb.jukeboxTempDir", "./temp")
                    + File.separator + LOG_FILENAME + ".tmp");
            PropertyConfigurator.configure("properties/log4j.properties");

            // Rename file (or directory)
            if (!oldLogFile.renameTo(newLogFile)) {
                System.err.println("Error renaming log file.");
            }

            // Try and rename the ERROR file too.
            oldLogFile = new File(LOG_FILENAME + ".ERROR.log");
            if (oldLogFile.length() > 0) {
                newLogFile = new File(newLogFilename.toString() + ".ERROR.log");
                if (!oldLogFile.renameTo(newLogFile)) {
                    System.err.println("Error renaming ERROR log file.");
                }
            } else if (!oldLogFile.delete()) {
                System.err.println("Error deleting ERROR log file.");
            }
        }
    }

    @SuppressWarnings("static-method")
    private void makeDumpStructure() {
        LOG.debug("Dumping library directory structure for debug");

        for (final MediaLibraryPath mediaLibrary : mediaLibraryPaths) {
            String mediaLibraryRoot = mediaLibrary.getPath();
            LOG.debug("Dumping media library {}", mediaLibraryRoot);
            File scanDir = new File(mediaLibraryRoot);
            if (scanDir.isFile()) {
                mediaLibraryRoot = scanDir.getParentFile().getAbsolutePath();
            } else {
                mediaLibraryRoot = scanDir.getAbsolutePath();
            }
            // Create library root dir into dump (keeping full path)

            String libraryRoot = mediaLibraryRoot.replaceAll(":", "_").replaceAll(Pattern.quote(File.separator),
                    "-");
            File libraryRootDump = new File("./dumpDir/" + libraryRoot);
            FileTools.makeDirs(libraryRootDump);
            // libraryRootDump.deleteOnExit();
            dumpDir(new File(mediaLibraryRoot), libraryRootDump);
            LOG.info("Dumping YAMJ root dir");
            // Dump YAMJ root for properties file
            dumpDir(new File("."), libraryRootDump);
            // libraryRootDump.deleteOnExit();
        }
    }

    private static boolean isExcluded(File file) {

        for (String string : EXCLUDED) {
            if (file.getName().endsWith(string)) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    private static void dumpDir(File sourceDir, File destDir) {
        String[] extensionToCopy = { "nfo", "NFO", "properties", "xml", "xsl" };
        LOG.info("Dumping  : {} to {}", sourceDir, destDir);
        File[] files = sourceDir.listFiles();
        for (File file : files) {
            try {
                if (!isExcluded(file)) {
                    String fileName = file.getName();
                    File newFile = new File(destDir.getAbsolutePath() + File.separator + fileName);

                    if (file.isDirectory()) {
                        newFile.mkdir();
                        dumpDir(file, newFile);
                    } else {
                        // Make an empty one.
                        newFile.createNewFile();

                        // Copy NFO / properties / .XML
                        if (ArrayUtils.contains(extensionToCopy, fileName.substring(fileName.length() - 3))) {
                            LOG.info("Coyping {} to {}", file, newFile);
                            FileTools.copyFile(file, newFile);
                        } else {
                            LOG.info("Creating dummy for {}", file);
                        }
                    }
                    //newFile.deleteOnExit();
                } else {
                    LOG.debug("Excluding : {}", file);
                }
            } catch (IOException e) {
                LOG.error("Dump error : {}", e.getMessage());
            }
        }
    }

    private static void help() {
        LOG.info("");
        LOG.info("Usage:");
        LOG.info("");
        LOG.info("Generates an HTML library for your movies library.");
        LOG.info("");
        LOG.info("MovieJukebox libraryRoot [-o jukeboxRoot]");
        LOG.info("");
        LOG.info("  libraryRoot       : OPTIONAL");
        LOG.info("                      This parameter must be specified either on the");
        LOG.info("                      command line or as mjb.libraryRoot in the properties file.");
        LOG.info("                      This parameter can be either: ");
        LOG.info("                      - An existing directory (local or network)");
        LOG.info("                        This is where your movie files are stored.");
        LOG.info("                        In this case -o is optional.");
        LOG.info("");
        LOG.info("                      - An XML configuration file specifying one or");
        LOG.info("                        many directories to be scanned for movies.");
        LOG.info("                        In this case -o option is MANDATORY.");
        LOG.info("                        Please check README.TXT for further information.");
        LOG.info("");
        LOG.info("  -o jukeboxRoot    : OPTIONAL (when not using XML libraries file)");
        LOG.info("                      output directory (local or network directory)");
        LOG.info("                      This is where the jukebox file will be written to");
        LOG.info("                      by default the is the same as the movieLibraryRoot");
        LOG.info("");
        LOG.info("  -c                : OPTIONAL");
        LOG.info("                      Clean the jukebox directory after running.");
        LOG.info("                      This will delete any unused files from the jukebox");
        LOG.info("                      directory at the end of the run.");
        LOG.info("");
        LOG.info("  -k                : OPTIONAL");
        LOG.info("                      Scan the output directory first. Any movies that already");
        LOG.info("                      exist but aren't found in any of the scanned libraries will");
        LOG.info("                      be preserved verbatim.");
        LOG.info("");
        LOG.info("  -i                : OPTIONAL");
        LOG.info("                      Skip the indexing of the library and generation of the");
        LOG.info("                      HTML pages. This should only be used with an external");
        LOG.info("                      front end, such as NMTServer.");
        LOG.info("");
        LOG.info("  -p propertiesFile : OPTIONAL");
        LOG.info("                      The properties file to use instead of moviejukebox.properties");
        LOG.info("");
        LOG.info("  -memory           : OPTIONAL");
        LOG.info("                      Display and log the memory used by moviejukebox");
        LOG.info("");
        LOG.info("  -t pin            : OPTIONAL");
        LOG.info("                      Set authentication pin from Trakt.TV to authorize this installation.");
        LOG.info(
                "                      Please visit 'https://trakt.tv/pin/9882' and use the given pin for authorization");
    }

    private void generateLibrary() throws Throwable {

        /**
         * ******************************************************************************
         * @author Gabriel Corneanu
         *
         * The tools used for parallel processing are NOT thread safe (some operations are, but not all) therefore all are added to
         * a container which is instantiated one per thread
         *
         * - xmlWriter looks thread safe<br>
         * - htmlWriter was not thread safe<br>
         * - getTransformer is fixed (simple workaround)<br>
         * - MovieImagePlugin : not clear, made thread specific for safety<br>
         * - MediaInfoScanner : not sure, made thread specific
         *
         * Also important: <br>
         * The library itself is not thread safe for modifications (API says so) it could be adjusted with concurrent versions, but
         * it needs many changes it seems that it is safe for subsequent reads (iterators), so leave for now...
         *
         * - DatabasePluginController is also fixed to be thread safe (plugins map for each thread)
         *
         */
        class ToolSet {

            private final MovieImagePlugin imagePlugin = MovieJukebox
                    .getImagePlugin(getProperty("mjb.image.plugin", "com.moviejukebox.plugin.DefaultImagePlugin"));
            private final MovieImagePlugin backgroundPlugin = MovieJukebox.getBackgroundPlugin(
                    getProperty("mjb.background.plugin", "com.moviejukebox.plugin.DefaultBackgroundPlugin"));
            private final MediaInfoScanner miScanner = new MediaInfoScanner();
            private final OpenSubtitlesPlugin subtitlePlugin = new OpenSubtitlesPlugin();
            private final TrailerScanner trailerScanner = new TrailerScanner();
            // FANART.TV TV Artwork Scanners
            private final ArtworkScanner clearArtScanner = new FanartTvScanner(ArtworkType.CLEARART);
            private final ArtworkScanner clearLogoScanner = new FanartTvScanner(ArtworkType.CLEARLOGO);
            private final ArtworkScanner tvThumbScanner = new FanartTvScanner(ArtworkType.TVTHUMB);
            private final ArtworkScanner seasonThumbScanner = new FanartTvScanner(ArtworkType.SEASONTHUMB);
            // FANART.TV Movie Artwork Scanners
            private final ArtworkScanner movieArtScanner = new FanartTvScanner(ArtworkType.MOVIEART);
            private final ArtworkScanner movieLogoScanner = new FanartTvScanner(ArtworkType.MOVIELOGO);
            private final ArtworkScanner movieDiscScanner = new FanartTvScanner(ArtworkType.MOVIEDISC);
        }

        final ThreadLocal<ToolSet> threadTools = new ThreadLocal<ToolSet>() {
            @Override
            protected ToolSet initialValue() {
                return new ToolSet();
            }
        };

        final MovieJukeboxXMLReader xmlReader = new MovieJukeboxXMLReader();
        final MovieJukeboxXMLWriter xmlWriter = new MovieJukeboxXMLWriter();
        final MovieJukeboxHTMLWriter htmlWriter = new MovieJukeboxHTMLWriter();

        File mediaLibraryRoot = new File(movieLibraryRoot);
        final File jukeboxDetailsRootFile = new FileTools.FileEx(jukebox.getJukeboxRootLocationDetails());

        MovieListingPlugin listingPlugin = getListingPlugin(
                getProperty("mjb.listing.plugin", "com.moviejukebox.plugin.MovieListingPluginBase"));

        videoimageDownload = PropertiesUtil.getBooleanProperty("mjb.includeVideoImages", Boolean.FALSE);
        bannerDownload = PropertiesUtil.getBooleanProperty("mjb.includeWideBanners", Boolean.FALSE);
        photoDownload = PropertiesUtil.getBooleanProperty("mjb.includePhoto", Boolean.FALSE);
        backdropDownload = PropertiesUtil.getBooleanProperty("mjb.includeBackdrop", Boolean.FALSE);
        boolean processExtras = PropertiesUtil.getBooleanProperty("filename.extras.process", Boolean.TRUE);
        boolean moviejukeboxListing = PropertiesUtil.getBooleanProperty("mjb.listing.generate", Boolean.FALSE);

        // Multi-thread: Processing thread settings
        maxThreadsProcess = Integer.parseInt(getProperty("mjb.MaxThreadsProcess", "0"));
        if (maxThreadsProcess <= 0) {
            maxThreadsProcess = Runtime.getRuntime().availableProcessors();
        }

        maxThreadsDownload = Integer.parseInt(getProperty("mjb.MaxThreadsDownload", "0"));
        if (maxThreadsDownload <= 0) {
            maxThreadsDownload = maxThreadsProcess;
        }

        LOG.info("Using {} processing threads and {} downloading threads...", maxThreadsProcess,
                maxThreadsDownload);
        if (maxThreadsDownload + maxThreadsProcess == 2) {
            // Display the note about the performance, otherwise assume that the user knows how to change
            // these parameters as they aren't set to the minimum
            LOG.info("See README.TXT for increasing performance using these settings.");
        }

        /*
         * ******************************************************************************
         *
         * PART 1 : Preparing the temporary environment
         *
         */
        SystemTools.showMemory();

        LOG.info("Preparing environment...");

        // create the ".mjbignore" and ".no_photo.nmj" file in the jukebox folder
        try {
            FileTools.makeDirs(jukebox.getJukeboxRootLocationDetailsFile());
            new File(jukebox.getJukeboxRootLocationDetailsFile(), ".mjbignore").createNewFile();
            FileTools.addJukeboxFile(".mjbignore");

            if (getBooleanProperty("mjb.nmjCompliant", Boolean.FALSE)) {
                new File(jukebox.getJukeboxRootLocationDetailsFile(), ".no_photo.nmj").createNewFile();
                FileTools.addJukeboxFile(".no_photo.nmj");
            }
        } catch (IOException error) {
            LOG.error("Failed creating jukebox directory. Ensure this directory is read/write!");
            LOG.error(SystemTools.getStackTrace(error));
            return;
        }

        // Delete the existing filecache.txt
        try {
            (new File("filecache.txt")).delete();
        } catch (Exception error) {
            LOG.error("Failed to delete the filecache.txt file.");
            LOG.error(SystemTools.getStackTrace(error));
            return;
        }

        // Save the current state of the preferences to the skin directory for use by the skin
        // The forceHtmlOverwrite is set by the user or by the JukeboxProperties if there has been a skin change
        if (PropertiesUtil.getBooleanProperty("mjb.forceHTMLOverwrite", Boolean.FALSE)
                || !(new File(PropertiesUtil.getPropertiesFilename(Boolean.TRUE))).exists()) {
            PropertiesUtil.writeProperties();
        }

        SystemTools.showMemory();

        LOG.info("Initializing...");
        try {
            FileTools.deleteDir(jukebox.getJukeboxTempLocation());
        } catch (Exception error) {
            LOG.error(
                    "Failed deleting the temporary jukebox directory ({}), please delete this manually and try again",
                    jukebox.getJukeboxTempLocation());
            return;
        }

        // Try and create the temp directory
        LOG.debug("Creating temporary jukebox location: {}", jukebox.getJukeboxTempLocation());
        FileTools.makeDirs(jukebox.getJukeboxTempLocationDetailsFile());

        /*
         * ******************************************************************************
         *
         * PART 2 : Scan movie libraries for files...
         *
         */
        SystemTools.showMemory();

        LOG.info("Scanning library directory {}", mediaLibraryRoot);
        LOG.info("Jukebox output goes to {}", jukebox.getJukeboxRootLocation());
        if (PropertiesUtil.getBooleanProperty("mjb.dirHash", Boolean.FALSE)) {
            // Add all folders 2 deep to the fileCache
            FileTools.fileCache.addDir(jukeboxDetailsRootFile, 2);
            /*
             * TODO: Need to watch for any issues when we have scanned the whole
             * jukebox, such as the watched folder, NFO folder, etc now existing
             * in the cache
             */
        } else {
            // If the dirHash is not needed, just scan to the root level plus the watched and people folders
            FileTools.fileCache.addDir(jukeboxDetailsRootFile, 0);

            // Add the watched folder
            File watchedFileHandle = new FileTools.FileEx(
                    jukebox.getJukeboxRootLocationDetails() + File.separator + "Watched");
            FileTools.fileCache.addDir(watchedFileHandle, 0);

            // Add the people folder if needed
            if (isValidString(peopleFolder)) {
                File peopleFolderHandle = new FileTools.FileEx(
                        jukebox.getJukeboxRootLocationDetails() + File.separator + peopleFolder);
                FileTools.fileCache.addDir(peopleFolderHandle, 0);
            }
        }

        ThreadExecutor<Void> tasks = new ThreadExecutor<>(maxThreadsProcess, maxThreadsDownload);

        final Library library = new Library();
        for (final MediaLibraryPath mediaLibraryPath : mediaLibraryPaths) {
            // Multi-thread parallel processing
            tasks.submit(new Callable<Void>() {
                @Override
                public Void call() {
                    LOG.debug("Scanning media library {}", mediaLibraryPath.getPath());
                    MovieDirectoryScanner mds = new MovieDirectoryScanner();
                    // scan uses synchronized method Library.addMovie
                    mds.scan(mediaLibraryPath, library);
                    System.out.print("\n");
                    return null;
                }
            });
        }
        tasks.waitFor();

        SystemTools.showMemory();

        // If the user asked to preserve the existing movies, scan the output directory as well
        if (isJukeboxPreserve()) {
            LOG.info("Scanning output directory for additional videos");
            OutputDirectoryScanner ods = new OutputDirectoryScanner(jukebox.getJukeboxRootLocationDetails());
            ods.scan(library);
        }

        // Now that everything's been scanned, add all extras to library
        library.mergeExtras();

        LOG.info("Found {} videos in your media library", library.size());
        LOG.info("Stored {} files in the info cache", FileTools.fileCache.size());

        if (enableWatchTraktTv) {
            // if Trakt.TV watched is enabled then refresh if necessary and preLoad watched data
            TraktTV.getInstance().initialize().refreshIfNecessary().preloadWatched();
        }

        JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.SCAN_END, System.currentTimeMillis());
        JukeboxStatistics.setStatistic(JukeboxStatistic.VIDEOS, library.size());

        tasks.restart();
        if (!library.isEmpty()) {
            // Issue 1882: Separate index files for each category
            boolean separateCategories = PropertiesUtil.getBooleanProperty("mjb.separateCategories", Boolean.FALSE);

            LOG.info("Searching for information on the video files...");
            int movieCounter = 0;
            for (final Movie movie : library.values()) {
                // Issue 997: Skip the processing of extras if not required
                if (movie.isExtra() && !processExtras) {
                    continue;
                }

                final int count = ++movieCounter;

                final String movieTitleExt = movie.getOriginalTitle()
                        + (movie.isTVShow() ? (" [Season " + movie.getSeason() + "]") : "")
                        + (movie.isExtra() ? " [Extra]" : "");

                if (movie.isTVShow()) {
                    JukeboxStatistics.increment(JukeboxStatistic.TVSHOWS);
                } else {
                    JukeboxStatistics.increment(JukeboxStatistic.MOVIES);
                }

                // Multi-thread parallel processing
                tasks.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws FileNotFoundException, XMLStreamException {

                        ToolSet tools = threadTools.get();

                        // Change the output message depending on the existance of the XML file
                        boolean xmlExists = FileTools.fileCache
                                .fileExists(StringTools.appendToPath(jukebox.getJukeboxRootLocationDetails(),
                                        movie.getBaseName()) + EXT_DOT_XML);
                        if (xmlExists) {
                            LOG.info("Checking existing video: {}", movieTitleExt);
                            JukeboxStatistics.increment(JukeboxStatistic.EXISTING_VIDEOS);
                        } else {
                            LOG.info("Processing new video: {}", movieTitleExt);
                            JukeboxStatistics.increment(JukeboxStatistic.NEW_VIDEOS);
                        }

                        if (ScanningLimit.getToken()) {

                            // First get movie data (title, year, director, genre, etc...)
                            library.toggleDirty(
                                    updateMovieData(xmlReader, tools.miScanner, jukebox, movie, library));

                            if (!movie.getMovieType().equals(Movie.REMOVE)) {
                                // Check for watched and unwatched files
                                if (enableWatchScanner || enableWatchTraktTv) { // Issue 1938
                                    library.toggleDirty(WatchedScanner.checkWatched(jukebox, movie));
                                }

                                // Get subtitle
                                tools.subtitlePlugin.generate(movie);

                                // Get Trailers
                                if (trailersScannerEnable) {
                                    tools.trailerScanner.getTrailers(movie);
                                }

                                // Then get this movie's poster
                                LOG.debug("Updating poster for: {}", movieTitleExt);
                                updateMoviePoster(jukebox, movie);

                                // Download episode images if required
                                if (videoimageDownload) {
                                    VideoImageScanner.scan(tools.imagePlugin, jukebox, movie);
                                }

                                // Get FANART only if requested
                                // Note that the FanartScanner will check if the file is newer / different
                                if ((fanartMovieDownload && !movie.isTVShow())
                                        || (fanartTvDownload && movie.isTVShow())) {
                                    FanartScanner.scan(tools.backgroundPlugin, jukebox, movie);
                                }

                                // Get BANNER if requested and is a TV show
                                if (bannerDownload && movie.isTVShow()) {
                                    if (!BannerScanner.scan(tools.imagePlugin, jukebox, movie)) {
                                        updateTvBanner(jukebox, movie, tools.imagePlugin);
                                    }
                                }

                                // Get ClearART/LOGOS/etc
                                if (movie.isTVShow()) {
                                    // Only scan using the TV Show artwork scanners
                                    tools.clearArtScanner.scan(jukebox, movie);
                                    tools.clearLogoScanner.scan(jukebox, movie);
                                    tools.tvThumbScanner.scan(jukebox, movie);
                                    tools.seasonThumbScanner.scan(jukebox, movie);
                                } else {
                                    // Only scan using the Movie artwork scanners
                                    tools.movieArtScanner.scan(jukebox, movie);
                                    tools.movieDiscScanner.scan(jukebox, movie);
                                    tools.movieLogoScanner.scan(jukebox, movie);
                                }

                                for (int i = 0; i < footerCount; i++) {
                                    if (FOOTER_ENABLE.get(i)) {
                                        updateFooter(jukebox, movie, tools.imagePlugin, i,
                                                forceFooterOverwrite || movie.isDirty());
                                    }
                                }

                                // If we are multipart, we need to make sure all archives have expanded names.
                                if (PropertiesUtil.getBooleanProperty("mjb.scanner.mediainfo.rar.extended.url",
                                        Boolean.FALSE)) {

                                    Collection<MovieFile> partsFiles = movie.getFiles();
                                    for (MovieFile mf : partsFiles) {
                                        String filename;

                                        filename = mf.getFile().getAbsolutePath();

                                        // Check the filename is a mediaInfo extension (RAR, ISO) ?
                                        if (tools.miScanner.extendedExtension(filename) == Boolean.TRUE) {

                                            if (mf.getArchiveName() == null) {
                                                LOG.debug("MovieJukebox: Attempting to get archive name for {}",
                                                        filename);
                                                String archive = tools.miScanner.archiveScan(filename);
                                                if (archive != null) {
                                                    LOG.debug("MovieJukebox: Setting archive name to {}", archive);
                                                    mf.setArchiveName(archive);
                                                } // got archivename
                                            } // not already set
                                        } // is extension
                                    } // for all files
                                } // property is set
                                if (!movie.isDirty()) {
                                    ScanningLimit.releaseToken();
                                }
                            } else {
                                ScanningLimit.releaseToken();
                                library.remove(movie);
                            }
                            LOG.info(LOG_FINISHED, movieTitleExt, count, library.size());
                        } else {
                            movie.setSkipped(true);
                            JukeboxProperties.setScanningLimitReached(Boolean.TRUE);
                            LOG.info("Skipped: {} ({}/{})", movieTitleExt, count, library.size());
                        }
                        // Show memory every (processing count) movies
                        if (showMemory && (count % maxThreadsProcess) == 0) {
                            SystemTools.showMemory();
                        }

                        return null;
                    }
                });
            }
            tasks.waitFor();

            // Add the new extra files (like trailers that were downloaded) to the library and to the corresponding movies
            library.mergeExtras();

            OpenSubtitlesPlugin.logOut();
            AniDbPlugin.anidbClose();

            JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.PROCESSING_END,
                    System.currentTimeMillis());

            if (peopleScan && peopleScrape && !ScanningLimit.isLimitReached()) {
                LOG.info("Searching for people information...");
                int peopleCounter = 0;
                Map<String, Person> popularPeople = new TreeMap<>();
                for (Movie movie : library.values()) {
                    // Issue 997: Skip the processing of extras if not required
                    if (movie.isExtra() && !processExtras) {
                        continue;
                    }

                    if (popularity > 0) {
                        for (Filmography person : movie.getPeople()) {
                            boolean exists = Boolean.FALSE;
                            String name = person.getName();
                            for (Map.Entry<String, Person> entry : popularPeople.entrySet()) {
                                if (entry.getKey().substring(3).equalsIgnoreCase(name)) {
                                    entry.getValue().addDepartment(person.getDepartment());
                                    entry.getValue().popularityUp(movie);
                                    exists = Boolean.TRUE;
                                }
                            }

                            if (!exists) {
                                Person p = new Person(person);
                                p.addDepartment(p.getDepartment());
                                String key = String.format("%03d", person.getOrder()) + person.getName();
                                popularPeople.put(key, p);
                                popularPeople.get(key).popularityUp(movie);
                            }
                        }
                    } else {
                        peopleCounter += movie.getPeople().size();
                    }
                }

                tasks.restart();
                if (popularity > 0) {
                    List<Person> as = new ArrayList<>(popularPeople.values());

                    Collections.sort(as, new PersonComparator());

                    List<Person> stars = new ArrayList<>();
                    Iterator<Person> itr = as.iterator();

                    while (itr.hasNext()) {
                        if (peopleCounter >= peopleMax) {
                            break;
                        }

                        Person person = itr.next();

                        if (popularity > person.getPopularity()) {
                            break;
                        }

                        stars.add(person);
                        peopleCounter++;
                    }

                    final int peopleCount = peopleCounter;
                    peopleCounter = 0;
                    for (final Person person : stars) {
                        final int count = ++peopleCounter;
                        final String personName = person.getName();
                        final Person p = new Person(person);

                        // Multi-thread parallel processing
                        tasks.submit(new Callable<Void>() {
                            @Override
                            public Void call() throws FileNotFoundException, XMLStreamException {

                                ToolSet tools = threadTools.get();

                                // Get person data (name, birthday, etc...), download photo
                                updatePersonData(xmlReader, jukebox, p, tools.imagePlugin);
                                library.addPerson(p);

                                LOG.info(LOG_FINISHED, personName, count, peopleCount);

                                // Show memory every (processing count) movies
                                if (showMemory && (count % maxThreadsProcess) == 0) {
                                    SystemTools.showMemory();
                                }

                                return null;
                            }
                        });
                    }
                } else {
                    final int peopleCount = peopleCounter;
                    peopleCounter = 0;
                    for (Movie movie : library.values()) {
                        // Issue 997: Skip the processing of extras if not required
                        if (movie.isExtra() && !processExtras) {
                            continue;
                        }
                        Map<String, Integer> typeCounter = new TreeMap<>();
                        for (Filmography person : movie.getPeople()) {
                            final int count = ++peopleCounter;
                            String job = person.getJob();
                            if (!typeCounter.containsKey(job)) {
                                typeCounter.put(job, 1);
                            } else if (typeCounter.get(job) == peopleMax) {
                                continue;
                            } else {
                                typeCounter.put(job, typeCounter.get(job) + 1);
                            }
                            final Person p = new Person(person);
                            final String personName = p.getName();

                            // Multi-thread parallel processing
                            tasks.submit(new Callable<Void>() {
                                @Override
                                public Void call() throws FileNotFoundException, XMLStreamException {

                                    ToolSet tools = threadTools.get();

                                    // Get person data (name, birthday, etc...), download photo and put to library
                                    updatePersonData(xmlReader, jukebox, p, tools.imagePlugin);
                                    library.addPerson(p);

                                    LOG.info(LOG_FINISHED, personName, count, peopleCount);

                                    // Show memory every (processing count) movies
                                    if (showMemory && (count % maxThreadsProcess) == 0) {
                                        SystemTools.showMemory();
                                    }

                                    return null;
                                }
                            });
                        }
                    }
                }
                tasks.waitFor();

                LOG.info("Add/update people information to the videos...");
                boolean dirty;
                for (Movie movie : library.values()) {
                    // Issue 997: Skip the processing of extras if not required
                    if (movie.isExtra() && !processExtras) {
                        continue;
                    }

                    for (Filmography person : movie.getPeople()) {
                        dirty = Boolean.FALSE;
                        for (Person p : library.getPeople()) {
                            if (Filmography.comparePersonName(person, p) || comparePersonId(person, p)) {
                                if (!person.getFilename().equals(p.getFilename())
                                        && isValidString(p.getFilename())) {
                                    person.setFilename(p.getFilename());
                                    dirty = Boolean.TRUE;
                                }
                                if (!person.getUrl().equals(p.getUrl()) && isValidString(p.getUrl())) {
                                    person.setUrl(p.getUrl());
                                    dirty = Boolean.TRUE;
                                }
                                for (Map.Entry<String, String> e : p.getIdMap().entrySet()) {
                                    if (isNotValidString(e.getValue())) {
                                        continue;
                                    }

                                    if (person.getId(e.getKey()).equals(e.getValue())) {
                                        continue;
                                    }

                                    person.setId(e.getKey(), e.getValue());
                                    dirty = Boolean.TRUE;
                                }

                                if (!person.getPhotoFilename().equals(p.getPhotoFilename())
                                        && isValidString(p.getPhotoFilename())) {
                                    person.setPhotoFilename(p.getPhotoFilename());
                                    dirty = Boolean.TRUE;
                                }

                                break;
                            }
                        }

                        if (dirty) {
                            movie.setDirty(DirtyFlag.INFO, Boolean.TRUE);
                        }
                    }

                    for (Person p : library.getPeople()) {
                        for (Filmography film : p.getFilmography()) {
                            if (Filmography.compareMovieAndFilm(movie, film)) {
                                film.setFilename(movie.getBaseName());
                                film.setTitle(movie.getTitle());
                                if (film.isDirty()) {
                                    p.setDirty();
                                }
                                break;
                            }
                        }
                    }
                }

                for (Person p : library.getPeople()) {
                    for (Filmography film : p.getFilmography()) {
                        if (film.isDirty() || StringTools.isNotValidString(film.getFilename())) {
                            continue;
                        }
                        dirty = Boolean.FALSE;
                        for (Movie movie : library.values()) {
                            if (movie.isExtra() && !processExtras) {
                                continue;
                            }
                            dirty = Filmography.compareMovieAndFilm(movie, film);
                            if (dirty) {
                                break;
                            }
                        }
                        if (!dirty) {
                            film.clearFilename();
                            p.setDirty();
                        }
                    }
                }

                JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.PEOPLE_END,
                        System.currentTimeMillis());
            }

            /*
             * ******************************************************************************
             *
             * PART 3 : Indexing the library
             *
             */
            SystemTools.showMemory();

            // This is for programs like NMTServer where they don't need the indexes.
            if (skipIndexGeneration) {
                LOG.info("Indexing of libraries skipped.");
            } else {
                LOG.info("Indexing libraries...");
                library.buildIndex(tasks);
                JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.INDEXING_END,
                        System.currentTimeMillis());
                SystemTools.showMemory();
            }

            /*
             * ******************************************************************************
             *
             * PART 3B - Indexing masters
             */
            LOG.info("Indexing masters...");
            /*
             * This is kind of a hack -- library.values() are the movies that
             * were found in the library and library.getMoviesList() are the
             * ones that are there now. So the movies that are in getMoviesList
             * but not in values are the index masters.
             */
            List<Movie> indexMasters = new ArrayList<>(library.getMoviesList());
            indexMasters.removeAll(library.values());

            JukeboxStatistics.setStatistic(JukeboxStatistic.SETS, indexMasters.size());

            // Multi-thread: Parallel Executor
            tasks.restart();
            final boolean autoCollection = PropertiesUtil.getBooleanProperty("themoviedb.collection",
                    Boolean.FALSE);
            final TheMovieDbPlugin tmdb = new TheMovieDbPlugin();

            for (final Movie movie : indexMasters) {
                // Multi-tread: Start Parallel Processing
                tasks.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws FileNotFoundException, XMLStreamException {
                        ToolSet tools = threadTools.get();

                        String safeSetMasterBaseName = FileTools.makeSafeFilename(movie.getBaseName());

                        /*
                         * The master's movie XML is used for generating the
                         * playlist it will be overwritten by the index XML
                         */
                        LOG.debug("Updating set artwork for: {}...", movie.getOriginalTitle());
                        // If we can find a set artwork file, use it; otherwise, stick with the first movie's artwork
                        String oldArtworkFilename = movie.getPosterFilename();

                        // Set a default poster name in case it's not found during the scan
                        movie.setPosterFilename(safeSetMasterBaseName + "." + posterExtension);
                        if (isNotValidString(PosterScanner.scan(jukebox, movie))) {
                            LOG.debug("Local set poster ({}) not found.", safeSetMasterBaseName);

                            String collectionId = movie.getId(TheMovieDbPlugin.CACHE_COLLECTION);
                            if (autoCollection && StringUtils.isNumeric(collectionId)) {
                                LOG.debug("MovieDb Collection detected with ID {}", collectionId);

                                movie.setPosterURL(tmdb.getCollectionPoster(Integer.parseInt(collectionId)));
                                movie.setFanartURL(tmdb.getCollectionFanart(Integer.parseInt(collectionId)));

                                updateMoviePoster(jukebox, movie);
                            } else {
                                movie.setPosterFilename(oldArtworkFilename);
                            }
                        }

                        // If this is a TV Show and we want to download banners, then also check for a banner Set file
                        if (movie.isTVShow() && bannerDownload) {
                            // Set a default banner filename in case it's not found during the scan
                            movie.setBannerFilename(safeSetMasterBaseName + bannerToken + "." + bannerExtension);
                            movie.setWideBannerFilename(
                                    safeSetMasterBaseName + wideBannerToken + "." + bannerExtension);
                            if (!BannerScanner.scan(tools.imagePlugin, jukebox, movie)) {
                                updateTvBanner(jukebox, movie, tools.imagePlugin);
                                LOG.debug("Local set banner ({}{}.*) not found.", safeSetMasterBaseName,
                                        bannerToken);
                            } else {
                                LOG.debug("Local set banner found, using {}", movie.getBannerFilename());
                            }
                        }

                        // Check for Set FANART
                        if (setIndexFanart) {
                            // Set a default fanart filename in case it's not found during the scan
                            movie.setFanartFilename(safeSetMasterBaseName + fanartToken + "." + fanartExtension);
                            if (!FanartScanner.scan(tools.backgroundPlugin, jukebox, movie)) {
                                LOG.debug("Local set fanart ({}{}.*) not found.", safeSetMasterBaseName,
                                        fanartToken);
                            } else {
                                LOG.debug("Local set fanart found, using {}", movie.getFanartFilename());
                            }
                        }

                        StringBuilder artworkFilename = new StringBuilder(safeSetMasterBaseName);
                        artworkFilename.append(thumbnailToken).append(".").append(thumbnailExtension);
                        movie.setThumbnailFilename(artworkFilename.toString());

                        artworkFilename = new StringBuilder(safeSetMasterBaseName);
                        artworkFilename.append(posterToken).append(".").append(posterExtension);
                        movie.setDetailPosterFilename(artworkFilename.toString());

                        // Generate footer filenames
                        for (int inx = 0; inx < footerCount; inx++) {
                            if (FOOTER_ENABLE.get(inx)) {
                                artworkFilename = new StringBuilder(safeSetMasterBaseName);
                                if (FOOTER_NAME.get(inx).contains("[")) {
                                    artworkFilename.append(footerToken).append("_").append(inx);
                                } else {
                                    artworkFilename.append(".").append(FOOTER_NAME.get(inx));
                                }
                                artworkFilename.append(".").append(FOOTER_EXTENSION.get(inx));
                                movie.setFooterFilename(artworkFilename.toString(), inx);
                            }
                        }

                        // No playlist for index masters
                        // htmlWriter.generatePlaylist(jukeboxDetailsRoot, tempJukeboxDetailsRoot, movie);
                        // Add all the movie files to the exclusion list
                        FileTools.addMovieToJukeboxFilenames(movie);

                        return null;
                    }
                });
            }
            tasks.waitFor();

            // Clear the cache if we've used it
            CacheMemory.clear();
            JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.MASTERS_END,
                    System.currentTimeMillis());
            SystemTools.showMemory();

            // Issue 1886: Html indexes recreated every time
            StringBuilder indexFilename;
            for (Movie setMovie : library.getMoviesList()) {
                if (setMovie.isSetMaster()) {
                    indexFilename = new StringBuilder(jukebox.getJukeboxRootLocationDetails());
                    indexFilename.append(File.separator).append(setMovie.getBaseName()).append(EXT_DOT_XML);
                    File xmlFile = FileTools.fileCache.getFile(indexFilename.toString());
                    if (xmlFile.exists()) {
                        xmlReader.parseSetXML(xmlFile, setMovie, library.getMoviesList());
                    }
                }
            }

            // Issue 1882: Separate index files for each category
            List<String> categoriesList = Arrays.asList(
                    getProperty("mjb.categories.indexList", "Other,Genres,Title,Certification,Year,Library,Set")
                            .split(","));

            if (!skipIndexGeneration) {
                LOG.info("Writing Indexes XML...");
                xmlWriter.writeIndexXML(jukebox, library, tasks);

                // Issue 2235: Update artworks after masterSet changed
                ToolSet tools = threadTools.get();
                StringBuilder idxName;
                boolean createPosters = PropertiesUtil.getBooleanProperty("mjb.sets.createPosters", Boolean.FALSE);

                for (IndexInfo idx : library.getGeneratedIndexes()) {
                    if (!idx.canSkip && idx.categoryName.equals(Library.INDEX_SET)) {
                        idxName = new StringBuilder(idx.categoryName);
                        idxName.append("_").append(FileTools.makeSafeFilename(idx.key)).append("_1");

                        for (Movie movie : indexMasters) {
                            if (!movie.getBaseName().equals(idxName.toString())) {
                                continue;
                            }

                            if (createPosters) {
                                // Create/update a detail poster for setMaster
                                LOG.debug("Create/update detail poster for set: {}", movie.getBaseName());
                                createPoster(tools.imagePlugin, jukebox, SkinProperties.getSkinHome(), movie,
                                        Boolean.TRUE);
                            }

                            // Create/update a thumbnail for setMaster
                            LOG.debug("Create/update thumbnail for set: {}, isTV: {}, isHD: {}",
                                    movie.getBaseName(), movie.isTVShow(), movie.isHD());
                            createThumbnail(tools.imagePlugin, jukebox, SkinProperties.getSkinHome(), movie,
                                    Boolean.TRUE);

                            for (int inx = 0; inx < footerCount; inx++) {
                                if (FOOTER_ENABLE.get(inx)) {
                                    LOG.debug("Create/update footer for set: {}, footerName: {}",
                                            movie.getBaseName(), FOOTER_NAME.get(inx));
                                    updateFooter(jukebox, movie, tools.imagePlugin, inx, Boolean.TRUE);
                                }
                            }
                        }
                    }
                }

                LOG.info("Writing Category XML...");
                library.setDirty(library.isDirty() || forceIndexOverwrite);
                xmlWriter.writeCategoryXML(jukebox, library, "Categories", library.isDirty());

                // Issue 1882: Separate index files for each category
                if (separateCategories) {
                    for (String categoryName : categoriesList) {
                        xmlWriter.writeCategoryXML(jukebox, library, categoryName, library.isDirty());
                    }
                }
            }

            SystemTools.showMemory();

            LOG.info("Writing Library data...");
            // Multi-thread: Parallel Executor
            tasks.restart();

            int totalCount = library.values().size();
            int currentCount = 1;

            for (final Movie movie : library.values()) {
                System.out.print("\r    Processing library #" + currentCount++ + "/" + totalCount);

                // Issue 997: Skip the processing of extras if not required
                if (movie.isExtra() && !processExtras) {
                    continue;
                }

                if (movie.isSkipped()) {
                    continue;
                }

                // Multi-tread: Start Parallel Processing
                tasks.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws FileNotFoundException, XMLStreamException {
                        ToolSet tools = threadTools.get();
                        // Update movie XML files with computed index information
                        LOG.debug("Writing index data to movie: {}", movie.getBaseName());
                        xmlWriter.writeMovieXML(jukebox, movie, library);

                        // Create a detail poster for each movie
                        LOG.debug("Creating detail poster for movie: {}", movie.getBaseName());
                        createPoster(tools.imagePlugin, jukebox, SkinProperties.getSkinHome(), movie,
                                forcePosterOverwrite);

                        // Create a thumbnail for each movie
                        LOG.debug("Creating thumbnails for movie: {}", movie.getBaseName());
                        createThumbnail(tools.imagePlugin, jukebox, SkinProperties.getSkinHome(), movie,
                                forceThumbnailOverwrite);

                        if (!skipIndexGeneration && !skipHtmlGeneration) {
                            // write the movie details HTML
                            LOG.debug("Writing detail HTML to movie: {}", movie.getBaseName());
                            htmlWriter.generateMovieDetailsHTML(jukebox, movie);

                            // write the playlist for the movie if needed
                            if (!skipPlaylistGeneration) {
                                FileTools.addJukeboxFiles(htmlWriter.generatePlaylist(jukebox, movie));
                            }
                        }
                        // Add all the movie files to the exclusion list
                        FileTools.addMovieToJukeboxFilenames(movie);

                        return null;
                    }
                });
            }
            tasks.waitFor();
            System.out.print("\n");

            SystemTools.showMemory();
            JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.WRITE_INDEX_END,
                    System.currentTimeMillis());

            if (peopleScan) {
                LOG.info("Writing people data...");
                // Multi-thread: Parallel Executor
                tasks.restart();

                totalCount = library.getPeople().size();
                currentCount = 1;
                for (final Person person : library.getPeople()) {
                    // Multi-tread: Start Parallel Processing
                    System.out.print("\r    Processing person #" + currentCount++ + "/" + totalCount);
                    tasks.submit(new Callable<Void>() {
                        @Override
                        public Void call() throws FileNotFoundException, XMLStreamException {
                            // ToolSet tools = threadTools.get();
                            // Update person XML files with computed index information
                            LOG.debug("Writing index data to person: {}", person.getName());
                            xmlWriter.writePersonXML(jukebox, person);

                            if (!skipIndexGeneration && !skipHtmlGeneration) {
                                // write the person details HTML
                                htmlWriter.generatePersonDetailsHTML(jukebox, person);
                            }

                            return null;
                        }
                    });
                }
                tasks.waitFor();
                System.out.print("\n");

                SystemTools.showMemory();
                JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.WRITE_PEOPLE_END,
                        System.currentTimeMillis());
            }

            if (!skipIndexGeneration) {
                if (!skipHtmlGeneration) {
                    LOG.info("Writing Indexes HTML...");
                    LOG.info("  Video indexes...");
                    htmlWriter.generateMoviesIndexHTML(jukebox, library, tasks);
                    LOG.info("  Category indexes...");
                    htmlWriter.generateMoviesCategoryHTML(jukebox, "Categories", "categories.xsl",
                            library.isDirty());

                    // Issue 1882: Separate index files for each category
                    if (separateCategories) {
                        LOG.info("  Separate category indexes...");
                        for (String categoryName : categoriesList) {
                            htmlWriter.generateMoviesCategoryHTML(jukebox, categoryName, "category.xsl",
                                    library.isDirty());
                        }
                    }
                }

                /*
                 * Generate the index file.
                 *
                 * Do not skip this part as it's the index that starts the jukebox
                 */
                htmlWriter.generateMainIndexHTML(jukebox, library);
                JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.WRITE_HTML_END,
                        System.currentTimeMillis());

                /*
                 Generate extra pages if required
                 */
                String pageList = PropertiesUtil.getProperty("mjb.customPages", "");
                if (StringUtils.isNotBlank(pageList)) {
                    List<String> newPages = new ArrayList<>(Arrays.asList(pageList.split(",")));
                    for (String page : newPages) {
                        LOG.info("Transforming skin custom page '{}'", page);
                        htmlWriter.transformXmlFile(jukebox, page);
                    }
                }
            }

            if (enableCompleteMovies) {
                CompleteMoviesWriter.generate(library, jukebox);
            }

            /**
             * ******************************************************************************
             *
             * PART 4 : Copy files to target directory
             *
             */
            SystemTools.showMemory();

            LOG.info("Copying new files to Jukebox directory...");
            String index = getProperty("mjb.indexFile", "index.htm");

            FileTools.copyDir(jukebox.getJukeboxTempLocationDetails(), jukebox.getJukeboxRootLocationDetails(),
                    Boolean.TRUE);
            FileTools.copyFile(new File(jukebox.getJukeboxTempLocation() + File.separator + index),
                    new File(jukebox.getJukeboxRootLocation() + File.separator + index));

            String skinDate = jukebox.getJukeboxRootLocationDetails() + File.separator + "pictures" + File.separator
                    + "skin.date";
            File skinFile = new File(skinDate);
            File propFile = new File(userPropertiesName);

            // Only check the property file date if the jukebox properties are not being monitored.
            boolean copySkin = JukeboxProperties.isMonitor() ? Boolean.FALSE
                    : FileTools.isNewer(propFile, skinFile);

            // If forceSkinOverwrite is set, the skin file doesn't exist, the user properties file doesn't exist or is newer than the skin.date file
            if (forceSkinOverwrite || !skinFile.exists() || !propFile.exists()
                    || (SkinProperties.getFileDate() > skinFile.lastModified()) || copySkin) {
                if (forceSkinOverwrite) {
                    LOG.info("Copying skin files to Jukebox directory (forceSkinOverwrite)...");
                } else if (SkinProperties.getFileDate() > skinFile.lastModified()) {
                    LOG.info("Copying skin files to Jukebox directory (Skin is newer)...");
                } else if (!propFile.exists()) {
                    LOG.info("Copying skin files to Jukebox directory (No property file)...");
                } else if (FileTools.isNewer(propFile, skinFile)) {
                    LOG.info("Copying skin files to Jukebox directory ({} is newer)...", propFile.getName());
                } else {
                    LOG.info("Copying skin files to Jukebox directory...");
                }

                StringTokenizer st = new StringTokenizer(PropertiesUtil.getProperty("mjb.skin.copyDirs", "html"),
                        " ,;|");

                while (st.hasMoreTokens()) {
                    String skinDirName = st.nextToken();
                    String skinDirFull = StringTools.appendToPath(SkinProperties.getSkinHome(), skinDirName);

                    if ((new File(skinDirFull).exists())) {
                        LOG.info("Copying the {} directory...", skinDirName);
                        FileTools.copyDir(skinDirFull, jukebox.getJukeboxRootLocationDetails(), Boolean.TRUE);
                    }
                }

                if (skinFile.exists()) {
                    skinFile.setLastModified(JukeboxStatistics.getTime(JukeboxStatistics.JukeboxTimes.START));
                } else {
                    FileTools.makeDirsForFile(skinFile);
                    skinFile.createNewFile();
                }
            } else {
                LOG.info("Skin copying skipped.");
                LOG.debug("Use mjb.forceSkinOverwrite=true to force the overwitting of the skin files");
            }

            FileTools.fileCache.saveFileList("filecache.txt");
            JukeboxStatistics.setJukeboxTime(JukeboxStatistics.JukeboxTimes.COPYING_END,
                    System.currentTimeMillis());

            /**
             * ******************************************************************************
             *
             * PART 5: Clean-up the jukebox directory
             *
             */
            SystemTools.showMemory();

            // Clean the jukebox folder of unneeded files
            cleanJukeboxFolder();

            if (moviejukeboxListing) {
                LOG.info("Generating listing output...");
                listingPlugin.generate(jukebox, library);
            }

            LOG.info("Clean up temporary files");
            File rootIndex = new File(appendToPath(jukebox.getJukeboxTempLocation(), index));
            rootIndex.delete();

            FileTools.deleteDir(jukebox.getJukeboxTempLocation());

            // clean up extracted attachments
            AttachmentScanner.cleanUp();
        }

        // Set the end time
        JukeboxStatistics.setTimeEnd(System.currentTimeMillis());

        // Write the jukebox details file at the END of the run (Issue 1830)
        JukeboxProperties.writeFile(jukebox, library, mediaLibraryPaths);

        // Output the statistics
        JukeboxStatistics.writeFile(jukebox, library, mediaLibraryPaths);

        LOG.info("");
        LOG.info("MovieJukebox process completed at {}", new Date());
        LOG.info("Processing took {}", JukeboxStatistics.getProcessingTime());
    }

    private static boolean comparePersonId(Filmography aPerson, Filmography bPerson) {
        String aValue, bValue;
        for (Map.Entry<String, String> e : aPerson.getIdMap().entrySet()) {
            aValue = e.getValue();
            if (StringTools.isNotValidString(aValue)) {
                continue;
            }
            bValue = bPerson.getId(e.getKey());
            if (StringTools.isValidString(bValue) && aValue.equals(bValue)) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    /**
     * Clean up the jukebox folder of any extra files that are not needed.
     *
     * If the jukeboxClean parameter is not set, just report on the files that would be cleaned.
     */
    private static void cleanJukeboxFolder() {
        boolean cleanReport = PropertiesUtil.getBooleanProperty("mjb.jukeboxCleanReport", Boolean.FALSE);

        if (jukeboxClean) {
            if (ScanningLimit.isLimitReached()) {
                LOG.info("Jukebox cleaning skipped as movie limit was reached");
                return;
            }
            LOG.info("Cleaning up the jukebox directory...");
        } else if (cleanReport) {
            LOG.info("Jukebox cleaning skipped, the following files are orphaned (not used anymore):");
        } else {
            LOG.info("Jukebox cleaning skipped.");
            return;
        }

        Collection<String> generatedFileNames = FileTools.getJukeboxFiles();

        File[] cleanList = jukebox.getJukeboxRootLocationDetailsFile().listFiles();
        int cleanDeletedTotal = 0;
        boolean skip;

        String skipPattStr = getProperty("mjb.clean.skip");
        Pattern skipPatt;

        if (StringTools.isValidString(skipPattStr)) {
            // Try and convert the string into a pattern
            try {
                skipPatt = Pattern.compile(skipPattStr, Pattern.CASE_INSENSITIVE);
            } catch (PatternSyntaxException ex) {
                LOG.warn("Error converting mjb.clean.skip '{}'", skipPattStr);
                LOG.warn(ex.getMessage());
                skipPatt = null;
            }
        } else {
            skipPatt = null;
        }
        for (File cleanList1 : cleanList) {
            // Scan each file in here
            if (cleanList1.isFile() && !generatedFileNames.contains(cleanList1.getName())) {
                skip = Boolean.FALSE;
                // If the file is in the skin's exclusion regex, skip it
                if (skipPatt != null) {
                    skip = skipPatt.matcher(cleanList1.getName()).matches();
                }
                // If the file isn't skipped and it's not part of the library, delete it
                if (!skip) {
                    if (jukeboxClean) {
                        LOG.debug("Deleted: {} from library", cleanList1.getName());
                        cleanList1.delete();
                    } else {
                        LOG.debug("Unused: {}", cleanList1.getName());
                    }
                    cleanDeletedTotal++;
                }
            }
        }

        LOG.info("{} files in the jukebox directory", cleanList.length);
        if (cleanDeletedTotal > 0) {
            if (jukeboxClean) {
                LOG.info("Deleted {} unused file{} from the jukebox directory", cleanDeletedTotal,
                        cleanDeletedTotal == 1 ? "" : "s");
            } else {
                LOG.info("There {} {} orphaned file{} in the jukebox directory",
                        cleanDeletedTotal == 1 ? "is" : "are", cleanDeletedTotal,
                        cleanDeletedTotal == 1 ? "" : "s");
            }
        }
    }

    /**
     * Generates a movie XML file which contains data in the <tt>Movie</tt> bean.
     *
     * When an XML file exists for the specified movie file, it is loaded into the specified <tt>Movie</tt> object.
     *
     * When no XML file exist, scanners are called in turn, in order to add information to the specified <tt>movie</tt> object. Once
     * scanned, the <tt>movie</tt> object is persisted.
     *
     * @param xmlReader
     * @param miScanner
     * @param movie
     * @param jukebox
     * @param library
     * @return
     */
    public boolean updateMovieData(MovieJukeboxXMLReader xmlReader, MediaInfoScanner miScanner, Jukebox jukebox,
            Movie movie, Library library) {
        boolean forceXMLOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceXMLOverwrite", Boolean.FALSE);
        boolean checkNewer = PropertiesUtil.getBooleanProperty("filename.nfo.checknewer", Boolean.TRUE);

        /*
         * For each video in the library, if an XML file for this video already
         * exists, then there is no need to search for the video file
         * information, just parse the XML data.
         */
        String safeBaseName = movie.getBaseName();
        File xmlFile = FileTools.fileCache
                .getFile(jukebox.getJukeboxRootLocationDetails() + File.separator + safeBaseName + EXT_DOT_XML);

        // See if we can find the NFO associated with this video file.
        List<File> nfoFiles = MovieNFOScanner.locateNFOs(movie);

        // Only check the NFO files if the XML exists and the CheckNewer parameter is set
        if (checkNewer && xmlFile.exists()) {
            for (File nfoFile : nfoFiles) {
                // Only re-scan the nfo files if one of them is newer
                if (FileTools.isNewer(nfoFile, xmlFile)) {
                    LOG.info("NFO for {} ({}) has changed, will rescan file.", movie.getOriginalTitle(),
                            nfoFile.getAbsolutePath());
                    movie.setDirty(DirtyFlag.NFO, Boolean.TRUE);
                    movie.setDirty(DirtyFlag.INFO, Boolean.TRUE);
                    movie.setDirty(DirtyFlag.POSTER, Boolean.TRUE);
                    movie.setDirty(DirtyFlag.FANART, Boolean.TRUE);
                    movie.setDirty(DirtyFlag.BANNER, Boolean.TRUE);
                    forceXMLOverwrite = Boolean.TRUE;
                    break; // one is enough
                }
            }
        }

        Collection<MovieFile> scannedFiles = Collections.emptyList();
        // Only parse the XML file if we mean to update the XML file.
        if (xmlFile.exists() && !forceXMLOverwrite) {
            // Parse the XML file
            LOG.debug("XML file found for {}", movie.getBaseName());
            // Copy scanned files BEFORE parsing the existing XML
            scannedFiles = new ArrayList<>(movie.getMovieFiles());

            xmlReader.parseMovieXML(xmlFile, movie);

            // Issue 1886: HTML indexes recreated every time
            // after remove NFO set data restoring from XML - compare NFO and XML sets
            Movie movieNFO = new Movie();
            for (String set : movie.getSetsKeys()) {
                movieNFO.addSet(set);
            }

            MovieNFOScanner.scan(movieNFO, nfoFiles);
            if (!Arrays.equals(movieNFO.getSetsKeys().toArray(), movie.getSetsKeys().toArray())) {
                movie.setSets(movieNFO.getSets());
                movie.setDirty(DirtyFlag.NFO, Boolean.TRUE);
            }

            // If we are overwiting the indexes, we need to check for an update to the library description
            if (forceIndexOverwrite) {
                for (MediaLibraryPath mlp : mediaLibraryPaths) {
                    // Check to see if the paths match and then update the description and quit
                    String mlpPath = mlp.getPath().concat(File.separator);
                    if (movie.getFile().getAbsolutePath().startsWith(mlpPath)
                            && !movie.getLibraryDescription().equals(mlp.getDescription())) {
                        LOG.debug("Changing libray description for video '{}' from '{}' to '{}'", movie.getTitle(),
                                movie.getLibraryDescription(), mlp.getDescription());
                        library.addDirtyLibrary(movie.getLibraryDescription());
                        movie.setLibraryDescription(mlp.getDescription());
                        movie.setDirty(DirtyFlag.INFO, Boolean.TRUE);
                        break;
                    }
                }
            }

            // Check to see if the video file needs a recheck
            if (RecheckScanner.scan(movie)) {
                LOG.info("Recheck of {} required", movie.getBaseName());
                forceXMLOverwrite = Boolean.TRUE;
                // Don't think we need the DIRTY_INFO with the RECHECK, so long as it is checked for specifically
                //movie.setDirty(DirtyFlag.INFO, Boolean.TRUE);
                movie.setDirty(DirtyFlag.RECHECK, Boolean.TRUE);
            }

            if (AttachmentScanner.rescan(movie, xmlFile)) {
                forceXMLOverwrite = Boolean.TRUE;
                // TODO Need for new dirty flag ATTACHMENT?
            }

            if (peopleScan && movie.getPeople().isEmpty()
                    && (movie.getCast().size() + movie.getWriters().size() + movie.getDirectors().size()) > 0) {
                forceXMLOverwrite = Boolean.TRUE;
                movie.clearWriters();
                movie.clearDirectors();
                movie.clearCast();
            }
        }

        // ForceBannerOverwrite is set here to force the re-load of TV Show data including the banners
        if (xmlFile.exists() && !forceXMLOverwrite && !(movie.isTVShow() && forceBannerOverwrite)) {
            FileLocationChange.process(movie, library, jukeboxPreserve, scannedFiles, mediaLibraryPaths);

            // update mediainfo values
            miScanner.update(movie);

            // update new episodes titles if new MovieFiles were added
            DatabasePluginController.scanTVShowTitles(movie);

            // Update thumbnails format if needed
            movie.setThumbnailFilename(movie.getBaseName() + thumbnailToken + "." + thumbnailExtension);

            // Update poster format if needed
            movie.setDetailPosterFilename(movie.getBaseName() + posterToken + "." + posterExtension);

            // Check for local CoverArt
            PosterScanner.scan(jukebox, movie);

            // If we don't have a local poster, look online
            // And even though we do "recheck" for a poster URL we should always try and get one
            if (isNotValidString(movie.getPosterURL())) {
                PosterScanner.scan(movie);
            }

        } else {
            // No XML file for this movie.
            // We've got to find movie information where we can (filename, IMDb, NFO, etc...) Add here extra scanners if needed.
            if (forceXMLOverwrite) {
                LOG.debug("Rescanning internet for information on {}", movie.getBaseName());
            } else {
                movie.setDirty(DirtyFlag.NEW); // Set a dirty flag so that caller knows we spent time processing the movie
                LOG.debug("Jukebox XML file not found: {}", xmlFile.getAbsolutePath());
                LOG.debug("Scanning for information on {}", movie.getBaseName());
            }

            // Changing call order, first MediaInfo then NFO. NFO will overwrite any information found by the MediaInfo Scanner.
            miScanner.scan(movie);

            // scan for attachments
            AttachmentScanner.scan(movie);
            // extract attached NFO and add to list of NFO files
            AttachmentScanner.addAttachedNfo(movie, nfoFiles);

            // scan NFO files
            MovieNFOScanner.scan(movie, nfoFiles);

            if (StringTools.isNotValidString(movie.getVideoSource())) {
                movie.setVideoSource(defaultSource, Movie.UNKNOWN);
            }

            // Added forceXMLOverwrite for issue 366
            if (!isValidString(movie.getPosterURL()) || movie.isDirty(DirtyFlag.POSTER)) {
                PosterScanner.scan(jukebox, movie);
            }

            DatabasePluginController.scan(movie);
            // Issue 1323:      Posters not picked up from NFO file
            // Only search for poster if we didn't have already
            if (!isValidString(movie.getPosterURL())) {
                PosterScanner.scan(movie);
            }

            movie.setCertification(Library.getIndexingCertification(movie.getCertification()),
                    movie.getOverrideSource(OverrideFlag.CERTIFICATION));
        }

        boolean photoFound = Boolean.FALSE;
        for (Filmography person : movie.getPeople()) {
            if (isValidString(person.getPhotoFilename())) {
                continue;
            }

            if (FileTools.findFilenameInCache(person.getName(), PHOTO_EXTENSIONS, jukebox, Boolean.TRUE,
                    peopleFolder) != null) {
                if (StringTools.isNotValidString(person.getFilename())) {
                    person.setFilename();
                }
                person.setPhotoFilename();
                photoFound = Boolean.TRUE;
            }
        }
        if (photoFound) {
            movie.setDirty(DirtyFlag.INFO, Boolean.TRUE);
        }

        // Update footer format if needed
        for (int i = 0; i < footerCount; i++) {
            if (FOOTER_ENABLE.get(i)) {
                StringBuilder sb = new StringBuilder(movie.getBaseFilename());
                if (FOOTER_NAME.get(i).contains("[")) {
                    sb.append(footerToken).append(" ").append(i);
                } else {
                    sb.append(".").append(FOOTER_NAME.get(i));
                }
                sb.append(".").append(FOOTER_EXTENSION.get(i));
                movie.setFooterFilename(sb.toString(), i);
            }
        }

        return movie.isDirty(DirtyFlag.INFO) || movie.isDirty(DirtyFlag.NFO);
    }

    public void updatePersonData(MovieJukeboxXMLReader xmlReader, Jukebox jukebox, Person person,
            MovieImagePlugin imagePlugin) {
        boolean forceXMLOverwrite = PropertiesUtil.getBooleanProperty("mjb.forceXMLOverwrite", Boolean.FALSE);
        person.setFilename();
        File xmlFile = FileTools.fileCache.getFile(jukebox.getJukeboxRootLocationDetails() + File.separator
                + peopleFolder + person.getFilename() + EXT_DOT_XML);

        // Change the output message depending on the existance of the XML file
        if (xmlFile.exists()) {
            LOG.info("Checking existing person: {}", person.getName());
        } else {
            LOG.info("Processing new person: {}", person.getName());
        }

        if (xmlFile.exists() && !forceXMLOverwrite) {
            LOG.debug("XML file found for {}", person.getName());
            xmlReader.parsePersonXML(xmlFile, person);
        } else {
            if (forceXMLOverwrite) {
                LOG.debug("Rescanning internet for information on {}", person.getName());
            } else {
                LOG.debug("Jukebox XML file not found: {}", xmlFile.getAbsolutePath());
                LOG.debug("Scanning for information on {}", person.getName());
            }
            DatabasePluginController.scan(person);
        }

        if (photoDownload) {
            PhotoScanner.scan(imagePlugin, jukebox, person);
        }

        if (backdropDownload) {
            BackdropScanner.scan(jukebox, person);
        }
    }

    /**
     * Update the movie poster for the specified movie.
     * <p>
     * When an existing thumbnail is found for the movie, it is not overwritten, unless the mjb.forceThumbnailOverwrite is set to
     * true in the property file.
     * <p>
     * When the specified movie does not contain a valid URL for the poster, a dummy image is used instead.
     *
     * @param jukebox
     * @param movie
     */
    public void updateMoviePoster(Jukebox jukebox, Movie movie) {
        String posterFilename = movie.getPosterFilename();
        String skinHome = SkinProperties.getSkinHome();
        File dummyFile = FileUtils.getFile(skinHome, LIT_RESOURCES, DUMMY_JPG);
        File posterFile = new File(FilenameUtils.concat(jukebox.getJukeboxRootLocationDetails(), posterFilename));
        File tmpDestFile = new File(FilenameUtils.concat(jukebox.getJukeboxTempLocationDetails(), posterFilename));

        FileTools.makeDirsForFile(posterFile);
        FileTools.makeDirsForFile(tmpDestFile);

        // Check to see if there is a local poster.
        // Check to see if there are posters in the jukebox directories (target and temp)
        // Check to see if the local poster is newer than either of the jukebox posters
        // Download poster
        // Do not overwrite existing posters, unless there is a new poster URL in the nfo file.
        if ((!tmpDestFile.exists() && !posterFile.exists()) || movie.isDirty(DirtyFlag.POSTER)
                || forcePosterOverwrite) {
            FileTools.makeDirsForFile(posterFile);

            if (!isValidString(movie.getPosterURL())) {
                LOG.debug("Dummy image used for {}", movie.getBaseName());
                FileTools.copyFile(dummyFile, tmpDestFile);
            } else {
                try {
                    // Issue 201 : we now download to local temp dir
                    LOG.debug("Downloading poster for {} to '{}'", movie.getBaseName(), tmpDestFile.getName());
                    FileTools.downloadImage(tmpDestFile, movie.getPosterURL());
                    LOG.debug("Downloaded poster for {}", movie.getBaseName());
                } catch (IOException error) {
                    LOG.debug("Failed downloading movie poster: {} - Error: {}", movie.getPosterURL(),
                            error.getMessage());
                    FileTools.copyFile(dummyFile, tmpDestFile);
                }
            }
        }
    }

    /**
     * Update the banner for the specified TV Show.
     *
     * When an existing banner is found for the movie, it is not overwritten, unless the mjb.forcePOSTEROverwrite is set to true in
     * the property file.
     *
     * When the specified movie does not contain a valid URL for the banner, a dummy image is used instead.
     *
     * @param jukebox
     * @param movie
     * @param imagePlugin
     */
    public void updateTvBanner(Jukebox jukebox, Movie movie, MovieImagePlugin imagePlugin) {
        String skinHome = SkinProperties.getSkinHome();
        String bannerFilename = movie.getBannerFilename();
        File bannerFile = FileTools.fileCache
                .getFile(jukebox.getJukeboxRootLocationDetails() + File.separator + bannerFilename);
        String tmpDestFilename = jukebox.getJukeboxTempLocationDetails() + File.separator + bannerFilename;
        File tmpDestFile = new File(tmpDestFilename);
        String origDestFilename = jukebox.getJukeboxTempLocationDetails() + File.separator
                + movie.getWideBannerFilename();
        File origDestFile = new File(origDestFilename);

        // Check to see if there is a local banner.
        // Check to see if there are banners in the jukebox directories (target and temp)
        // Check to see if the local banner is newer than either of the jukebox banners
        // Download banner
        // Do not overwrite existing banners, unless there is a new poster URL in the nfo file.
        if ((!tmpDestFile.exists() && !bannerFile.exists()) || movie.isDirty(DirtyFlag.BANNER)
                || forceBannerOverwrite) {
            FileTools.makeDirsForFile(tmpDestFile);

            if (isNotValidString(movie.getBannerURL())) {
                LOG.debug("Dummy banner used for {}", movie.getBaseName());
                FileTools.copyFile(
                        new File(skinHome + File.separator + LIT_RESOURCES + File.separator + "dummy_banner.jpg"),
                        origDestFile);
            } else {
                try {
                    LOG.debug("Downloading banner for '{}' to '{}'", movie.getBaseName(), origDestFile.getName());
                    FileTools.downloadImage(origDestFile, movie.getBannerURL());
                } catch (IOException error) {
                    LOG.debug("Failed downloading banner: {} - Error: {}", movie.getBannerURL(),
                            error.getMessage());
                    FileTools.copyFile(new File(
                            skinHome + File.separator + LIT_RESOURCES + File.separator + "dummy_banner.jpg"),
                            origDestFile);
                }
            }

            try {
                BufferedImage bannerImage = GraphicTools.loadJPEGImage(origDestFile);
                if (bannerImage != null) {
                    bannerImage = imagePlugin.generate(movie, bannerImage, "banners", null);
                    GraphicTools.saveImageToDisk(bannerImage, tmpDestFilename);
                }
            } catch (IOException ex) {
                LOG.debug("MovieJukebox: Failed generate banner: {} - Error: {}", tmpDestFilename, ex.getMessage());
            }
        }
    }

    public void updateFooter(Jukebox jukebox, Movie movie, MovieImagePlugin imagePlugin, Integer inx,
            boolean forceFooterOverwrite) {
        if (movie.getFooterFilename() == null || movie.getFooterFilename().isEmpty()) {
            LOG.debug("MovieJukebox: Footer update not required for {}", movie.getBaseName());
            return;
        }

        String footerFilename = movie.getFooterFilename().get(inx);
        File footerFile = FileTools.fileCache
                .getFile(jukebox.getJukeboxRootLocationDetails() + File.separator + footerFilename);
        String tmpDestFilename = jukebox.getJukeboxTempLocationDetails() + File.separator + footerFilename;
        File tmpDestFile = new File(tmpDestFilename);

        if (forceFooterOverwrite || (!tmpDestFile.exists() && !footerFile.exists())) {
            FileTools.makeDirsForFile(footerFile);

            BufferedImage footerImage = GraphicTools.createBlankImage(FOOTER_WIDTH.get(inx),
                    FOOTER_HEIGHT.get(inx));
            if (footerImage != null) {
                footerImage = imagePlugin.generate(movie, footerImage, "footer" + FOOTER_NAME.get(inx), null);
                GraphicTools.saveImageToDisk(footerImage, tmpDestFilename);
            }
        }
    }

    public static synchronized MovieImagePlugin getImagePlugin(String className) {
        try {
            Thread t = Thread.currentThread();
            ClassLoader cl = t.getContextClassLoader();
            Class<? extends MovieImagePlugin> pluginClass = cl.loadClass(className)
                    .asSubclass(MovieImagePlugin.class);
            return pluginClass.newInstance();
        } catch (InstantiationException ex) {
            LOG.error("Failed instanciating ImagePlugin: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (IllegalAccessException ex) {
            LOG.error("Failed accessing ImagePlugin: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (ClassNotFoundException ex) {
            LOG.error("ImagePlugin class not found: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        }
        LOG.error("Default poster plugin will be used instead.");
        return new DefaultImagePlugin();
    }

    public static MovieImagePlugin getBackgroundPlugin(String className) {
        try {
            Thread t = Thread.currentThread();
            ClassLoader cl = t.getContextClassLoader();
            Class<? extends MovieImagePlugin> pluginClass = cl.loadClass(className)
                    .asSubclass(MovieImagePlugin.class);
            return pluginClass.newInstance();
        } catch (InstantiationException ex) {
            LOG.error("Failed instantiating BackgroundPlugin: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (IllegalAccessException ex) {
            LOG.error("Failed accessing BackgroundPlugin: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (ClassNotFoundException ex) {
            LOG.error("BackgroundPlugin class not found: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        }

        LOG.error("Default background plugin will be used instead.");
        return new DefaultBackgroundPlugin();
    }

    public static MovieListingPlugin getListingPlugin(String className) {
        try {
            Thread t = Thread.currentThread();
            ClassLoader cl = t.getContextClassLoader();
            Class<? extends MovieListingPlugin> pluginClass = cl.loadClass(className)
                    .asSubclass(MovieListingPlugin.class);
            return pluginClass.newInstance();
        } catch (InstantiationException ex) {
            LOG.error("Failed instanciating ListingPlugin: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (IllegalAccessException ex) {
            LOG.error("Failed accessing ListingPlugin: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (ClassNotFoundException ex) {
            LOG.error("ListingPlugin class not found: {} - Error: {}", className, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
        }

        LOG.error("No listing plugin will be used.");
        return new MovieListingPluginBase();
    } // getListingPlugin()

    /**
     * Create a thumbnail from the original poster file.
     *
     * @param imagePlugin
     * @param skinHome
     * @param jukebox
     * @param movie
     * @param forceThumbnailOverwrite
     */
    public static void createThumbnail(MovieImagePlugin imagePlugin, Jukebox jukebox, String skinHome, Movie movie,
            boolean forceThumbnailOverwrite) {

        // TODO Move all temp directory code to FileTools for a cleaner method
        // Issue 201 : we now download to local temp directory
        String safePosterFilename = movie.getPosterFilename();
        String safeThumbnailFilename = movie.getThumbnailFilename();

        File tmpPosterFile = new File(appendToPath(jukebox.getJukeboxTempLocationDetails(), safePosterFilename));
        File jkbPosterFile = FileTools.fileCache
                .getFile(appendToPath(jukebox.getJukeboxRootLocationDetails(), safePosterFilename));
        String tmpThumbnailFile = appendToPath(jukebox.getJukeboxTempLocationDetails(), safeThumbnailFilename);
        String jkbThumbnailFile = appendToPath(jukebox.getJukeboxRootLocationDetails(), safeThumbnailFilename);
        File destinationFile;

        if (movie.isDirty(DirtyFlag.POSTER) || forceThumbnailOverwrite
                || !FileTools.fileCache.fileExists(jkbThumbnailFile) || tmpPosterFile.exists()) {
            // Issue 228: If the PNG files are deleted before running the jukebox this fails.
            // Therefore check to see if they exist in the original directory
            if (tmpPosterFile.exists()) {
                // logger.debug("Use new file: " + tmpPosterFile.getAbsolutePath());
                destinationFile = tmpPosterFile;
            } else {
                // logger.debug("Use jukebox file: " + jkbPosterFile.getAbsolutePath());
                destinationFile = jkbPosterFile;
            }

            BufferedImage bi = null;
            try {
                bi = GraphicTools.loadJPEGImage(destinationFile);
            } catch (IOException ex) {
                LOG.warn("Error reading the thumbnail file: {} - Error: {}", destinationFile.getAbsolutePath(),
                        ex.getMessage());
            }

            if (bi == null) {
                LOG.info("Using dummy thumbnail image for {}", movie.getBaseName());
                // There was an error with the URL, assume it's a bad URL and clear it so we try again
                movie.setPosterURL(Movie.UNKNOWN);
                FileTools.copyFile(new File(skinHome + File.separator + LIT_RESOURCES + File.separator + DUMMY_JPG),
                        tmpPosterFile);
                try {
                    bi = GraphicTools.loadJPEGImage(tmpPosterFile);
                } catch (IOException ex) {
                    LOG.warn("Error reading the dummy file: {} - Error: {}", tmpPosterFile.getAbsolutePath(),
                            ex.getMessage());
                }
            }

            // Perspective code.
            String perspectiveDirection = getProperty("thumbnails.perspectiveDirection", RIGHT);

            // Generate and save both images
            if (BOTH.equalsIgnoreCase(perspectiveDirection)) {
                // Calculate mirror thumbnail name.
                String dstMirror = tmpThumbnailFile.substring(0, tmpThumbnailFile.lastIndexOf('.')) + "_mirror"
                        + tmpThumbnailFile.substring(tmpThumbnailFile.lastIndexOf('.'));

                // Generate left & save as copy
                LOG.debug("Generating mirror thumbnail from {} to {}", tmpPosterFile, dstMirror);
                BufferedImage biMirror = imagePlugin.generate(movie, bi, THUMBNAILS, LEFT);
                GraphicTools.saveImageToDisk(biMirror, dstMirror);

                // Generate right as per normal
                LOG.debug("Generating right thumbnail from {} to {}", tmpPosterFile, tmpThumbnailFile);
                bi = imagePlugin.generate(movie, bi, THUMBNAILS, RIGHT);
                GraphicTools.saveImageToDisk(bi, tmpThumbnailFile);
            }

            // Only generate the right image
            if (RIGHT.equalsIgnoreCase(perspectiveDirection)) {
                bi = imagePlugin.generate(movie, bi, THUMBNAILS, RIGHT);

                // Save the right perspective image.
                GraphicTools.saveImageToDisk(bi, tmpThumbnailFile);
                LOG.debug("Generating right thumbnail from {} to {}", tmpPosterFile, tmpThumbnailFile);
            }

            // Only generate the left image
            if (LEFT.equalsIgnoreCase(perspectiveDirection)) {
                bi = imagePlugin.generate(movie, bi, THUMBNAILS, LEFT);

                // Save the right perspective image.
                GraphicTools.saveImageToDisk(bi, tmpThumbnailFile);
                LOG.debug("Generating left thumbnail from {} to {}", tmpPosterFile, tmpThumbnailFile);
            }
        }
    }

    /**
     * Create a detailed poster file from the original poster file
     *
     * @param posterManager
     * @param jukebox
     * @param skinHome
     * @param movie
     * @param forcePosterOverwrite
     */
    public static void createPoster(MovieImagePlugin posterManager, Jukebox jukebox, String skinHome, Movie movie,
            boolean forcePosterOverwrite) {

        // Issue 201 : we now download to local temporary directory
        String safePosterFilename = movie.getPosterFilename();
        String safeDetailPosterFilename = movie.getDetailPosterFilename();
        File tmpPosterFile = new File(appendToPath(jukebox.getJukeboxTempLocationDetails(), safePosterFilename));
        File jkbPosterFile = FileTools.fileCache
                .getFile(appendToPath(jukebox.getJukeboxRootLocationDetails(), safePosterFilename));
        String tmpThumbnailFile = appendToPath(jukebox.getJukeboxTempLocationDetails(), safeDetailPosterFilename);
        String jkbThumbnailFile = appendToPath(jukebox.getJukeboxRootLocationDetails(), safeDetailPosterFilename);
        File destinationFile;

        if (movie.isDirty(DirtyFlag.POSTER) || forcePosterOverwrite
                || !FileTools.fileCache.fileExists(jkbThumbnailFile) || tmpPosterFile.exists()) {
            // Issue 228: If the PNG files are deleted before running the jukebox this fails. Therefore check to see if they exist in the original directory
            if (tmpPosterFile.exists()) {
                LOG.debug("CreatePoster: New file exists ({})", tmpPosterFile);
                destinationFile = tmpPosterFile;
            } else {
                LOG.debug("CreatePoster: Using old file ({})", jkbPosterFile);
                destinationFile = jkbPosterFile;
            }

            BufferedImage bi = null;
            try {
                bi = GraphicTools.loadJPEGImage(destinationFile);
            } catch (IOException ex) {
                LOG.warn("Error processing the poster file: {}", destinationFile.getAbsolutePath());
                LOG.error(SystemTools.getStackTrace(ex));
            }

            if (bi == null) {
                // There was an error with the URL, assume it's a bad URL and clear it so we try again
                movie.setPosterURL(Movie.UNKNOWN);
                FileTools.copyFile(new File(skinHome + File.separator + LIT_RESOURCES + File.separator + DUMMY_JPG),
                        jkbPosterFile);
                try {
                    bi = GraphicTools.loadJPEGImage(tmpPosterFile);
                    LOG.info("Using dummy poster image for {}", movie.getOriginalTitle());
                } catch (IOException ex) {
                    LOG.warn("Error processing the dummy poster file: {}", tmpPosterFile.getAbsolutePath());
                    LOG.error(SystemTools.getStackTrace(ex));
                }
            }
            LOG.debug("Generating poster from {} to {}", tmpPosterFile, tmpThumbnailFile);

            // Perspective code.
            String perspectiveDirection = getProperty("posters.perspectiveDirection", RIGHT);

            // Generate and save both images
            if (BOTH.equalsIgnoreCase(perspectiveDirection)) {
                // Calculate mirror poster name.
                String dstMirror = FilenameUtils.removeExtension(tmpThumbnailFile) + "_mirror."
                        + FilenameUtils.getExtension(tmpThumbnailFile);

                // Generate left & save as copy
                LOG.debug("Generating mirror poster from {} to {}", tmpPosterFile, dstMirror);
                BufferedImage biMirror = posterManager.generate(movie, bi, POSTERS, LEFT);
                GraphicTools.saveImageToDisk(biMirror, dstMirror);

                // Generate right as per normal
                LOG.debug("Generating right poster from {} to {}", tmpPosterFile, tmpThumbnailFile);
                bi = posterManager.generate(movie, bi, POSTERS, RIGHT);
                GraphicTools.saveImageToDisk(bi, tmpThumbnailFile);
            }

            // Only generate the right image
            if (RIGHT.equalsIgnoreCase(perspectiveDirection)) {
                bi = posterManager.generate(movie, bi, POSTERS, RIGHT);

                // Save the right perspective image.
                GraphicTools.saveImageToDisk(bi, tmpThumbnailFile);
                LOG.debug("Generating right poster from {} to {}", tmpPosterFile, tmpThumbnailFile);
            }

            // Only generate the left image
            if (LEFT.equalsIgnoreCase(perspectiveDirection)) {
                bi = posterManager.generate(movie, bi, POSTERS, LEFT);

                // Save the right perspective image.
                GraphicTools.saveImageToDisk(bi, tmpThumbnailFile);
                LOG.debug("Generating left poster from {} to {}", tmpPosterFile, tmpThumbnailFile);
            }
        }
    }

    public static boolean isJukeboxPreserve() {
        return jukeboxPreserve;
    }

    public static void setJukeboxPreserve(boolean bJukeboxPreserve) {
        jukeboxPreserve = bJukeboxPreserve;
        if (bJukeboxPreserve) {
            LOG.info("Existing jukebox video information will be preserved.");
        }
    }

    @XmlRootElement(name = "jukebox")
    public static class JukeboxXml {

        @XmlElement
        public List<Movie> movies;
    }
}