com.moviejukebox.scanner.artwork.ArtworkScanner.java Source code

Java tutorial

Introduction

Here is the source code for com.moviejukebox.scanner.artwork.ArtworkScanner.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.scanner.artwork;

import com.moviejukebox.model.IImage;
import com.moviejukebox.model.Jukebox;
import com.moviejukebox.model.Movie;
import com.moviejukebox.model.artwork.ArtworkPriority;
import com.moviejukebox.model.artwork.ArtworkType;
import com.moviejukebox.model.enumerations.DirtyFlag;
import com.moviejukebox.plugin.DefaultBackgroundPlugin;
import com.moviejukebox.plugin.DefaultImagePlugin;
import com.moviejukebox.plugin.MovieImagePlugin;
import com.moviejukebox.tools.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Scanner for artwork
 *
 * @author Stuart
 */
public abstract class ArtworkScanner implements IArtworkScanner {

    //These are the properties used:
    //  {artworkType}.scanner.artworkSearchLocal - true/false
    //  {artworkType}.scanner.artworkExtensions - The extensions to search for the artwork
    //  {artworkType}.scanner.artworkTokenOriginal - A token that delimits the artwork, such as ".fanart" or ".banner"
    //  {artworkType}.scanner.imageName - List of fixed artwork names
    //  {artworkType}.scanner.Validate -
    //  {artworkType}.scanner.ValidateMatch -
    //  {artworkType}.scanner.ValidateAspect -
    //  {artworkType}.scanner.artworkDirectory -
    //  {artworkType}.scanner.artworkPriority - mjb.skin.dir - Skin directory
    //
    // From skin.properties
    //  ???.width
    //  ???.height
    private static final Logger LOG = LoggerFactory.getLogger(ArtworkScanner.class);
    private static final String SPLITTER = ",;|";
    protected final WebBrowser webBrowser = new WebBrowser();
    protected MovieImagePlugin artworkImagePlugin;
    // Location of the skin files used to get the dummy images from for missing artwork
    protected String skinHome = SkinProperties.getSkinHome();
    // *** Scanner settings
    // The type of the artwork. Will be used to load the other properties. When using for properties, must be lowercase
    protected ArtworkType artworkType;
    // The artwork type name to use in properties (all lowercase)
    protected String artworkTypeName;
    protected boolean artworkOverwrite = Boolean.FALSE;
    // *** Search settings
    // Should we search for local artwork
    protected boolean artworkSearchLocal;
    // Whether or not to download artwork for a Movie
    protected boolean artworkDownloadMovie;
    // Whether or not to download artwork for a TV Show
    protected boolean artworkDownloadTv;
    // *** Local search settings
    protected Collection<String> artworkExtensions = new ArrayList<>();
    // List of fixed artwork names
    protected Collection<String> artworkImageName = new ArrayList<>();
    // The name of the artwork directory to search from from either the video directory or the root of the library
    private final String artworkDirectory;
    // The order of the searches performed for the local artwork.
    private final List<ArtworkPriority> artworkPriority = new ArrayList<>();
    // *** Artwork attributes
    // Format of the artwork to save, e.g. JPG, PNG, etc.
    protected String artworkFormat;
    // The suffix of the artwork filename to search for.
    protected String artworkTokenOriginal;
    // The suffix of the artwork filename to search for.
    protected String artworkTokenJukebox;
    // The width of the image from the skin.properties for use in the validation routine
    protected int artworkWidth;
    // The height of the image from the skin.properties for use in the validation routine
    protected int artworkHeight;
    // *** Artwork validation
    // Should the artwork be validated or not.
    protected boolean artworkValidate;
    // Should the artwork be validated for it's aspect
    protected boolean artworkValidateAspect;
    // How close the image should be to the expected dimensions (artworkWidth & artworkHeight)
    protected int artworkValidateMatch;
    // *** Artwork dummy settings
    protected static boolean useDummy;
    // *** Artwork original file settings
    protected static boolean saveOriginal;

    /**
     * Construct the
     *
     * @param conArtworkType
     */
    public ArtworkScanner(ArtworkType conArtworkType) {
        setArtworkType(conArtworkType);

        StringTokenizer st = new StringTokenizer(
                PropertiesUtil.getProperty(conArtworkType + ".scanner.imageName", ""), SPLITTER);
        while (st.hasMoreTokens()) {
            artworkImageName.add(st.nextToken());
        }

        // Get artwork scanner behaviour
        artworkSearchLocal = PropertiesUtil
                .getBooleanProperty(artworkTypeName + ".scanner.searchForExistingArtwork", Boolean.FALSE);
        artworkDownloadMovie = PropertiesUtil.getBooleanProperty(artworkTypeName + ".movie.download",
                Boolean.FALSE);
        artworkDownloadTv = PropertiesUtil.getBooleanProperty(artworkTypeName + ".tv.download", Boolean.FALSE);

        setArtworkExtensions(
                PropertiesUtil.getProperty(artworkTypeName + ".scanner.artworkExtensions", "jpg,png,gif"));
        artworkTokenOriginal = PropertiesUtil.getProperty(artworkTypeName + ".scanner.artworkToken", "");
        if (StringUtils.isBlank(artworkTokenOriginal) && artworkType != ArtworkType.POSTER) {
            // If the token is empty, create a default from the name
            artworkTokenOriginal = "." + artworkTypeName + "_orig";
        }

        artworkTokenJukebox = PropertiesUtil.getProperty(artworkTypeName + ".scanner.artworkToken", "");
        if (StringUtils.isBlank(artworkTokenJukebox) && artworkType != ArtworkType.POSTER) {
            artworkTokenJukebox = "." + artworkTypeName + "_jb";
        }

        artworkFormat = PropertiesUtil.getProperty(artworkTypeName + ".format", "jpg");
        setArtworkImageName(PropertiesUtil.getProperty(artworkTypeName + ".scanner.imageName", ""));

        artworkValidate = PropertiesUtil.getBooleanProperty(artworkTypeName + ".scanner.Validate", Boolean.TRUE);
        artworkValidateMatch = PropertiesUtil.getIntProperty(artworkTypeName + ".scanner.ValidateMatch", 75);
        artworkValidateAspect = PropertiesUtil.getBooleanProperty(artworkTypeName + ".scanner.ValidateAspect",
                Boolean.TRUE);

        artworkDirectory = PropertiesUtil.getProperty(artworkTypeName + ".scanner.artworkDirectory", "");

        // Get the priority (order) that the artwork is searched for
        setArtworkPriority(PropertiesUtil.getProperty(artworkTypeName + ".scanner.artworkPriority",
                "video,folder,fixed,series,directory"));

        // Should dummy artwork be created
        useDummy = PropertiesUtil.getBooleanProperty(artworkTypeName + ".scanner.useDummy", Boolean.TRUE);

        // Should the original artwork be saved
        saveOriginal = PropertiesUtil.getBooleanProperty(artworkTypeName + ".scanner.saveOriginal", Boolean.TRUE);

        // Get & set the default artwork dimensions
        setArtworkDimensions();

        // Set the image plugin
        setArtworkImagePlugin();
    }

    /**
     * A catch all routine to scan local artwork and then online artwork.
     *
     * @param jukebox
     * @param movie
     * @return
     */
    @Override
    public final String scan(Jukebox jukebox, Movie movie) {
        // If we are not required, leave
        if (!isSearchRequired()) {
            LOG.debug("{} {} not required", movie.getBaseFilename(), artworkTypeName);
            return getArtworkUrl(movie);
        }

        // If forceOverwrite is set, clear the Url so we will search again
        if (isOverwrite()) {
            LOG.debug("forceOverwite set, clearing URL before search"); // XXX DEBUG
            setArtworkUrl(movie, Movie.UNKNOWN);
            setOriginalFilename(movie, Movie.UNKNOWN);
        } else {
            // Check to see if we have a valid URL and it's not dirty
            if (StringTools.isValidString(getArtworkUrl(movie))
                    && !(isDirtyArtwork(movie) || movie.isDirty(DirtyFlag.INFO))) {
                // Valid URL, so exit with that
                LOG.debug("URL for {} looks valid, skipping online search: {}", movie.getBaseName(),
                        getArtworkUrl(movie));
                if (StringTools.isNotValidString(getOriginalFilename(movie))) {
                    setOriginalFilename(movie, makeSafeOriginalFilename(movie, Boolean.TRUE));
                }
                return getArtworkUrl(movie);
            }

            // Not a valid URL, check to see if the artwork is dirty or the movie is dirty
            if (!(isDirtyArtwork(movie) || movie.isDirty(DirtyFlag.INFO) || movie.isDirty(DirtyFlag.RECHECK))) {
                // Artwork and movie is not dirty, so don't process
                LOG.debug("URL update not required (not overwrite, dirty or recheck)");
                return getArtworkUrl(movie);
            }
        }

        String artworkUrl;
        if (isSearchLocal()) {
            LOG.debug("Scanning for local artwork for {}", movie.getBaseName());
            artworkUrl = scanLocalArtwork(jukebox, movie);
            LOG.debug("ScanLocalArtwork returned: {}", artworkUrl);
            if (StringTools.isValidString(artworkUrl)) {
                // Update the movie artwork URL
                setArtworkUrl(movie, artworkUrl);

                // Only set the filename if we have an artwork URL
                setOriginalFilename(movie, makeSafeOriginalFilename(movie, Boolean.TRUE));

                // Save the artwork to the jukebox
                copyLocalArtwork(jukebox, movie);
                return artworkUrl;
            }
        } else {
            artworkUrl = Movie.UNKNOWN;
        }

        if (StringTools.isNotValidString(artworkUrl) && isSearchOnline(movie)) {
            LOG.debug("Scanning for online artwork for {}", movie.getBaseName());
            artworkUrl = scanOnlineArtwork(movie);
            LOG.debug("ScanOnlineArtwork returned: {}", artworkUrl);

            if (StringTools.isValidString(artworkUrl)) {
                // Update the movie artwork URL
                setArtworkUrl(movie, artworkUrl);

                // Only set the filename if we have an artwork URL
                setOriginalFilename(movie, makeSafeOriginalFilename(movie, Boolean.TRUE));

                // Save the artwork to the jukebox
                saveArtworkToJukebox(jukebox, movie);
            } else {
                LOG.debug("No online artwork found for {}", movie.getBaseName());
            }
        }

        return artworkUrl;
    }

    /**
     * Save the artwork to the jukebox
     *
     * @param jukebox
     * @param movie
     * @return
     */
    @Override
    public boolean saveArtworkToJukebox(Jukebox jukebox, Movie movie) {
        boolean returnValue = Boolean.TRUE;
        /*
         * 2) Determing the type of the artwork - either local or online
         * 2a) Download online artwork and save to temp jukebox
         * 2b) Copy local artwork
         * 2c) Use dummy artwork if required
         * 3) Copy original artwork to jukebox if required
         */

        String artworkFilename = getOriginalFilename(movie);
        File artworkFile = FileTools.fileCache
                .getFile(StringTools.appendToPath(jukebox.getJukeboxRootLocationDetails(), artworkFilename));

        // Does the artwork need to be overwritten?
        if (!artworkOverwrite && artworkFile.exists()) {
            LOG.debug("{}: Artwork does not need to be saved", movie.getBaseFilename());
            return returnValue;
        }

        String artworkUrl = getArtworkUrl(movie);

        // Determine the type of artwork, either local or online and act accordingly
        if (artworkUrl.startsWith("http")) {
            // Looks like a URL so download it
            returnValue = downloadArtwork(jukebox, movie);
        } else {
            // Looks like a file, so copy it
            returnValue = copyLocalArtwork(jukebox, movie);
        }

        // If we Successfully copied or downloaded the artwork, we need to process it
        if (returnValue) {
            returnValue = processArtwork(jukebox, movie);
        }

        return returnValue;
    }

    /**
     * Save the artwork to the jukebox folder
     *
     * This will be the temp folder by default.
     *
     * It will not post process the image.
     *
     * TODO: Parameter to control if the original artwork is saved in the jukebox or not. We should save this in an
     * "originalArtwork" folder or something
     *
     * @param jukebox
     * @param movie
     * @return the status of the save. True if saved correctly, false otherwise.
     */
    public boolean downloadArtwork(Jukebox jukebox, Movie movie) {
        String artworkUrl = getArtworkUrl(movie);
        String artworkFilename = getOriginalFilename(movie);

        if (StringTools.isNotValidString(artworkUrl)) {
            LOG.debug("Invalid artwork URL, artwork not copied.");
            return Boolean.FALSE;
        }

        if (StringTools.isNotValidString(artworkFilename)) {
            // Create the filename if not found
            setOriginalFilename(movie, makeSafeOriginalFilename(movie, Boolean.TRUE));
        }

        String artworkPath = StringTools.appendToPath(jukebox.getJukeboxRootLocationDetails(), artworkFilename);
        if (!artworkOverwrite && FileTools.fileCache.fileExists(artworkPath)) {
            LOG.debug("{} exists for {}", artworkTypeName, movie.getBaseName());
            return Boolean.TRUE;
        }

        artworkPath = StringTools.appendToPath(jukebox.getJukeboxTempLocationDetails(), artworkFilename);
        File artworkFile = new File(artworkPath); // This is the temp folder, don't use filecache
        LOG.debug("Saving {} for {} to '{}'", artworkTypeName, movie.getBaseName(), artworkPath);

        boolean returnValue;
        try {
            returnValue = FileTools.downloadImage(artworkFile, artworkUrl);
        } catch (IOException ex) {
            LOG.debug("Failed to download {}: {}, Error: {}", artworkType, artworkUrl, ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
            returnValue = Boolean.FALSE;
        }

        return returnValue;

    }

    /**
     * Copy the local artwork file to the jukebox.
     *
     * If there's no URL do not create dummy artwork
     *
     * @param jukebox
     * @param movie
     * @return
     */
    protected boolean copyLocalArtwork(Jukebox jukebox, Movie movie) {
        String artworkUrl = getArtworkUrl(movie);
        boolean returnValue;

        if (artworkUrl.equalsIgnoreCase(Movie.UNKNOWN)) {
            LOG.debug("No local {} found for {}", artworkType, movie.getBaseName());
            returnValue = Boolean.FALSE;
        } else {
            LOG.debug("{}: '{}' found", movie.getBaseName(), artworkUrl);
            if (overwriteArtwork(jukebox, movie)) {
                String destFilename = getOriginalFilename(movie);
                String destFullPath = StringTools.appendToPath(jukebox.getJukeboxTempLocationDetails(),
                        destFilename);

                returnValue = FileTools.copyFile(artworkUrl, destFullPath);
                if (returnValue) {
                    LOG.debug("'{}' has been copied to '{}'", artworkUrl, destFilename);
                } else {
                    LOG.debug("Failed to copy '{}' to '{}'", artworkUrl, destFilename);
                }
            } else {
                // Don't copy the file
                LOG.debug("'{}' does not need to be copied", artworkUrl);
                returnValue = Boolean.TRUE;
            }
        }
        return returnValue;
    }

    /**
     * Process the artwork to apply the graphic manipulations required for display in the skin
     *
     * Will leave the original file in place (if required) and output the changes as a new file
     *
     * @param jukebox
     * @param movie
     * @return
     */
    public boolean processArtwork(Jukebox jukebox, Movie movie) {
        boolean returnValue;

        String originalFilename = StringTools.appendToPath(jukebox.getJukeboxTempLocationDetails(),
                getOriginalFilename(movie));
        File originalFile = new File(originalFilename);
        if (!originalFile.exists()) {
            LOG.debug("File not found: {}", originalFilename);
            boolean saveValue = saveArtworkToJukebox(jukebox, movie);
            if (!saveValue) {
                LOG.debug("Failed to save file to jukebox: {}", originalFilename);
                return Boolean.FALSE;
            }
        }

        String processedFilename = StringTools.appendToPath(jukebox.getJukeboxTempLocationDetails(),
                getOriginalFilename(movie));
        File processedFile = new File(processedFilename);

        try {
            BufferedImage artworkImage = GraphicTools.loadJPEGImage(originalFile);

            if (artworkImage != null) {
                artworkImage = artworkImagePlugin.generate(movie, artworkImage, getPropertyName(), null);
                GraphicTools.saveImageToDisk(artworkImage, processedFile.getAbsolutePath());
                returnValue = Boolean.TRUE;
            } else {
                setOriginalFilename(movie, Movie.UNKNOWN);
                setArtworkUrl(movie, Movie.UNKNOWN);
                returnValue = Boolean.FALSE;
            }
        } catch (IOException ex) {
            LOG.debug("Failed to process: {}, Error: {}", originalFile.getAbsolutePath(), ex.getMessage());
            LOG.error(SystemTools.getStackTrace(ex));
            returnValue = Boolean.FALSE;
        }

        if (returnValue) {
            LOG.debug("Processed '{}'", originalFile.getAbsolutePath());
        } else {
            LOG.debug("Failed to process artwork '{}'", originalFile.getAbsolutePath());
        }

        return returnValue;
    }

    /**
     * Process the artwork for a Set
     *
     * @param jukebox
     * @param movie
     * @return
     */
    public boolean processSetArtwork(Jukebox jukebox, Movie movie) {
        if (isSearchRequired()) {
            // Store the current artwork file and URL incase nothing is found
            String originalFilename = getOriginalFilename(movie);
            String originalUrl = getArtworkUrl(movie);

            // Set a default banner filename in case it's not found during the scan
            String safeOriginalFilename = makeSafeOriginalFilename(movie, Boolean.TRUE);
            setOriginalFilename(movie, safeOriginalFilename);
            setJukeboxFilename(movie, makeSafeJukeboxFilename(movie, Boolean.TRUE));

            String bannerUrl = scanLocalArtwork(jukebox, movie);
            if (StringTools.isValidString(bannerUrl)) {
                LOG.debug("Local set banner found: {}", bannerUrl);
            } else {
                // updateTvBanner(jukebox, movie, tools.imagePlugin);
                LOG.debug("Local set banner ({}) not found, using '{}'", safeOriginalFilename, originalFilename);
                setOriginalFilename(movie, originalFilename);
                setArtworkUrl(movie, originalUrl);
            }
            saveArtworkToJukebox(jukebox, movie);
            processArtwork(jukebox, movie);
        } else {
            LOG.debug("Artwork not required for {}", movie.getBaseName());
        }

        return false;
    }

    /**
     * Get the original artwork filename from the movie object based on the artwork type
     *
     * @param movie
     * @return the Artwork Filename
     */
    @Override
    public abstract String getOriginalFilename(Movie movie);

    /**
     * Get the artwork filename for the jukebox from the movie object based on the artwork type
     *
     * @param movie
     * @return the Artwork filename
     */
    @Override
    public abstract String getJukeboxFilename(Movie movie);

    /**
     * Get the artwork URL from the movie object based on the artwork type
     *
     * @param movie
     * @return the Artwork URL
     */
    @Override
    public abstract String getArtworkUrl(Movie movie);

    @Override
    public abstract boolean isDirtyArtwork(Movie movie);

    @Override
    public final boolean isSearchRequired() {
        // Assume the artwork is required
        return isSearchLocal() || isSearchOnline();
    }

    @Override
    public final boolean isSearchLocal() {
        return artworkSearchLocal;
    }

    @Override
    public final boolean isSearchOnline() {
        return artworkDownloadTv || artworkDownloadMovie;
    }

    @Override
    public final boolean isSearchOnline(Movie movie) {
        if (!movie.isScrapeLibrary()) {
            return Boolean.FALSE;
        }

        if (!isSearchOnline()) {
            return Boolean.FALSE;
        }

        if (movie.isTVShow() && !artworkDownloadTv) {
            return Boolean.FALSE;
        } else if (!movie.isTVShow() && !artworkDownloadMovie) {
            return Boolean.FALSE;
        }

        for (Entry<String, String> e : movie.getIdMap().entrySet()) {
            if (("0".equals(e.getValue())) || ("-1".equals(e.getValue()))) {
                // Stop and return
                return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    /**
     * Check to see if the artwork type is required or not
     *
     * @param artworkType
     * @return
     */
    public static boolean isRequired(ArtworkType artworkType) {
        return isRequired(artworkType.toString().toLowerCase());
    }

    /**
     * Determine if the artwork type is required or not
     *
     * @param artworkTypeString
     * @return
     */
    public static boolean isRequired(String artworkTypeString) {
        boolean sArtworkLocalSearch = PropertiesUtil
                .getBooleanProperty(artworkTypeString + ".scanner.searchForExistingArtwork", Boolean.FALSE);
        boolean sArtworkMovieDownload = PropertiesUtil.getBooleanProperty(artworkTypeString + ".movie.download",
                Boolean.FALSE);
        boolean sArtworkTvDownload = PropertiesUtil.getBooleanProperty(artworkTypeString + ".tv.download",
                Boolean.FALSE);

        return (sArtworkLocalSearch || sArtworkMovieDownload || sArtworkTvDownload);
    }

    /**
     * Return a list of the required artwork types
     *
     * @return
     */
    public static Set<ArtworkType> getRequiredArtworkTypes() {
        Set<ArtworkType> artworkTypeRequired = EnumSet.noneOf(ArtworkType.class);
        for (ArtworkType artworkType : EnumSet.allOf(ArtworkType.class)) {
            if (isRequired(artworkType)) {
                artworkTypeRequired.add(artworkType);
            }
        }
        return artworkTypeRequired;
    }

    /**
     * Scan for any local artwork and return the path to it.
     *
     * This should only be called by the scanLocalArtwork method in the derived classes.
     *
     * Note: This will update the movie information for this artwork
     *
     * @param jukebox
     * @param movie
     * @param artworkImagePlugin
     * @return The URL (path) to the first found artwork in the priority list
     */
    protected String scanLocalArtwork(Jukebox jukebox, Movie movie, MovieImagePlugin artworkImagePlugin) {
        // Check to see if we are required
        if (!isSearchLocal()) {
            // return the current URL
            return getArtworkUrl(movie);
        }

        String artworkUrl = Movie.UNKNOWN;
        String artworkFilename = movie.getBaseFilename() + artworkTokenOriginal;

        LOG.debug("Searching for '{}'", artworkFilename);

        for (ArtworkPriority artworkSearch : artworkPriority) {
            if (StringTools.isValidString(artworkUrl)) {
                // We've got a valid URL, stop scanning
                break;
            }

            if (artworkSearch.equals(ArtworkPriority.VIDEO)) {
                artworkUrl = scanVideoArtwork(movie, artworkFilename);
                LOG.trace("{} scanVideoArtwork    : {}", movie.getBaseFilename(), artworkUrl);
                continue;
            }

            if (artworkSearch.equals(ArtworkPriority.FOLDER)) {
                artworkUrl = scanFolderArtwork(movie);
                LOG.trace("{} scanFolderArtwork    : {}", movie.getBaseFilename(), artworkUrl);
                continue;
            }

            if (artworkSearch.equals(ArtworkPriority.FIXED)) {
                artworkUrl = scanFixedArtwork(movie);
                LOG.trace("{} scanFixedArtwork    : {}", movie.getBaseFilename(), artworkUrl);
                continue;
            }

            if (artworkSearch.equals(ArtworkPriority.SERIES)) {
                // This is only for TV Sets as it searches the directory above the one the episode is in for fixed artwork
                if (movie.isTVShow() && movie.isSetMaster()) {
                    artworkUrl = scanTvSeriesArtwork(movie);
                    LOG.trace("{} scanTvSeriesArtwork    : {}", movie.getBaseFilename(), artworkUrl);
                }
                continue;
            }

            if (artworkSearch.equals(ArtworkPriority.DIRECTORY)) {
                artworkUrl = scanArtworkDirectory(movie, artworkFilename);
                LOG.trace("{} scanArtworkDirectory    : {}", movie.getBaseFilename(), artworkUrl);
            }

        }

        if (StringTools.isValidString(artworkUrl)) {
            LOG.debug("Found artwork for {}: {}", movie.getBaseName(), artworkUrl);
        } else {
            LOG.debug("No local artwork found for {}", movie.getBaseName());
        }
        setArtworkUrl(movie, artworkUrl);

        return artworkUrl;
    }

    @Override
    public abstract void setOriginalFilename(Movie movie, String artworkFilename);

    @Override
    public abstract void setArtworkUrl(Movie movie, String artworkUrl);

    /**
     * Updates the artwork by either copying the local file or downloading the artwork
     *
     * USE: saveArtworkToJukebox method instead
     *
     * @param jukebox
     * @param movie
     */
    @Deprecated
    public void updateArtwork(Jukebox jukebox, Movie movie) {
        String artworkFilename = getOriginalFilename(movie);
        String artworkUrl = getArtworkUrl(movie);
        String artworkDummy = getDummyFilename();

        File artworkFile = FileTools.fileCache
                .getFile(StringTools.appendToPath(jukebox.getJukeboxRootLocationDetails(), artworkFilename));
        File tmpDestFile = new File(
                StringTools.appendToPath(jukebox.getJukeboxTempLocationDetails(), artworkFilename));

        // Do not overwrite existing artwork, unless there is a new URL in the nfo file.
        if ((!tmpDestFile.exists() && !artworkFile.exists()) || isDirtyArtwork(movie) || isOverwrite()) {
            FileTools.makeDirsForFile(artworkFile);

            if (artworkUrl == null || artworkUrl.equals(Movie.UNKNOWN)) {
                LOG.debug("Dummy {} used for {}", artworkType, movie.getBaseName());
                FileTools.copyFile(
                        new File(skinHome + File.separator + "resources" + File.separator + artworkDummy),
                        tmpDestFile);
            } else {
                if (StringTools.isValidString(artworkDummy)) {
                    try {
                        LOG.debug("Saving {} for {} to {}", artworkType, movie.getBaseName(),
                                tmpDestFile.getName());
                        FileTools.downloadImage(tmpDestFile, artworkUrl);
                    } catch (IOException ex) {
                        LOG.debug("Failed downloading {}: {} - {}", artworkType, artworkUrl, ex.getMessage());
                        FileTools.copyFile(
                                new File(skinHome + File.separator + "resources" + File.separator + artworkDummy),
                                tmpDestFile);
                    }
                } else {
                    LOG.debug("No dummy artwork ('{}') for {}", artworkDummy, artworkType);
                }
            }
        } else {
            saveArtworkToJukebox(jukebox, movie);
        }
    }

    @Override
    public boolean validateArtwork(IImage artworkImage) {
        return validateArtwork(artworkImage, artworkWidth, artworkHeight, artworkValidateAspect);
    }

    /**
     * Get the size of the file at the end of the URL
     *
     * Taken from: http://forums.sun.com/thread.jspa?threadID=528155&messageID=2537096
     *
     * @param artworkImage Artwork image to check
     * @param artworkWidth The width to check
     * @param artworkHeight The height to check
     * @param checkAspect Should the aspect ratio be checked
     * @return True if the poster is good, false otherwise
     */
    @Override
    public boolean validateArtwork(IImage artworkImage, int artworkWidth, int artworkHeight, boolean checkAspect) {
        @SuppressWarnings("rawtypes")
        Iterator readers = ImageIO.getImageReadersBySuffix("jpeg");
        ImageReader reader = (ImageReader) readers.next();
        int urlWidth;
        int urlHeight;
        float urlAspect;

        if (!artworkValidate) {
            return Boolean.TRUE;
        }

        if (artworkImage.getUrl().equalsIgnoreCase(Movie.UNKNOWN)) {
            return Boolean.FALSE;
        }

        try (InputStream in = new URL(artworkImage.getUrl()).openStream();
                ImageInputStream iis = ImageIO.createImageInputStream(in)) {
            reader.setInput(iis, Boolean.TRUE);
            urlWidth = reader.getWidth(0);
            urlHeight = reader.getHeight(0);
        } catch (IOException ex) {
            LOG.debug("ValidateArtwork error: {}: can't open URL", ex.getMessage());
            return Boolean.FALSE; // Quit and return a Boolean.FALSE poster
        }

        urlAspect = (float) urlWidth / (float) urlHeight;

        if (checkAspect && urlAspect > 1.0) {
            LOG.debug("{} rejected: URL is wrong aspect (portrait/landscape)", artworkImage);
            return Boolean.FALSE;
        }

        // Adjust artwork width / height by the ValidateMatch figure
        int newArtworkWidth = artworkWidth * (artworkValidateMatch / 100);
        int newArtworkHeight = artworkHeight * (artworkValidateMatch / 100);

        if (urlWidth < newArtworkWidth) {
            LOG.debug("{} rejected: URL width ({}) is smaller than artwork width ({})", artworkImage, urlWidth,
                    newArtworkWidth);
            return Boolean.FALSE;
        }

        if (urlHeight < newArtworkHeight) {
            LOG.debug("{} rejected: URL height ({}) is smaller than artwork height ({})", artworkImage, urlHeight,
                    newArtworkHeight);
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    /**
     * returns the forceOverwite parameter for this artwork type.
     *
     * This is set in the constructor of each of the sub-scanners
     *
     * @return
     */
    protected boolean isOverwrite() {
        return artworkOverwrite;
    }

    /**
     * Determine if the artwork should be overwritten
     *
     * Checks to see if the artwork exists in the jukebox folders (temp and final)
     *
     * Checks the overwrite parameters Checks to see if the local artwork is newer
     *
     * @param jukebox
     * @param movie
     * @return
     */
    protected final boolean overwriteArtwork(Jukebox jukebox, Movie movie) {
        if (isOverwrite()) {
            setDirtyArtwork(movie, Boolean.TRUE);
            LOG.trace("{}: Artwork overwrite set, artwork will be overwritten", movie.getBaseFilename());
            return Boolean.TRUE;
        }

        if (isDirtyArtwork(movie)) {
            LOG.trace("{}: Dirty artwork set, artwork will be overwritten", movie.getBaseFilename());
            return Boolean.TRUE;
        }

        // This is the filename & path of the artwork that was found
        String artworkFilename = getOriginalFilename(movie);
        File artworkFile = new File(artworkFilename); // Note: We can't use the filecache because the file might not be in there

        // This is the filename & path of the jukebox artwork
        String jukeboxFilename = StringTools.appendToPath(jukebox.getJukeboxRootLocationDetails(), artworkFilename);
        File jukeboxFile = FileTools.fileCache.getFile(jukeboxFilename);

        // This is the filename & path for temporary storage jukebox
        String tempLocFilename = StringTools.appendToPath(jukebox.getJukeboxTempLocationDetails(), artworkFilename);
        File tempLocFile = new File(tempLocFilename);

        // Check to see if the found artwork newer than the temp jukebox
        if (FileTools.fileCache.fileExists(tempLocFile)) {
            if (FileTools.isNewer(artworkFile, tempLocFile)) {
                setDirtyArtwork(movie, Boolean.TRUE);
                LOG.trace("{}: Local artwork is newer, overwritting existing artwork", movie.getBaseName());
                return Boolean.TRUE;
            }
        } else if (FileTools.fileCache.fileExists(jukeboxFile)) {
            // Check to see if the found artwork newer than the existing jukebox
            if (FileTools.isNewer(artworkFile, jukeboxFile)) {
                setDirtyArtwork(movie, Boolean.TRUE);
                LOG.trace("{}: Local artwork is newer, overwritting existing jukebox artwork", movie.getBaseName());
                return Boolean.TRUE;
            }
            LOG.trace("{}: Local artwork is older, not copying", movie.getBaseName());
            return Boolean.FALSE;
        } else {
            LOG.trace("{}: No jukebox file found, file will be copied", movie.getBaseName());
            return Boolean.TRUE;
        }

        // All the tests passed so don't overwrite
        return Boolean.FALSE;
    }

    /**
     * Scan an absolute or relative path for the movie images.
     *
     * The relative path should include the directory of the movie as well as the library root
     *
     * @param movie
     * @param artworkFilename
     * @return UNKNOWN or Absolute Path
     */
    protected String scanArtworkDirectory(Movie movie, String artworkFilename) {
        String artworkPath = Movie.UNKNOWN;

        if (StringUtils.isNotBlank(artworkDirectory)) {
            String artworkLibraryPath = StringTools.appendToPath(movie.getLibraryPath(), artworkDirectory);
            artworkPath = scanVideoArtwork(movie, artworkFilename, artworkDirectory);
            if (artworkPath.equalsIgnoreCase(Movie.UNKNOWN)) {
                artworkPath = scanDirectoryForArtwork(artworkFilename, artworkLibraryPath);
            }
        }
        return artworkPath;
    }

    /**
     * Scan the passed directory for the artwork filename
     *
     * @param artworkFilename
     * @param artworkPath
     * @return UNKNOWN or Absolute Path
     */
    protected String scanDirectoryForArtwork(String artworkFilename, String artworkPath) {
        String fullFilename = StringTools.appendToPath(artworkPath, artworkFilename);
        File artworkFile = FileTools.findFileFromExtensions(fullFilename, artworkExtensions);

        if (artworkFile.exists()) {
            return artworkFile.getAbsolutePath();
        }
        return Movie.UNKNOWN;
    }

    /**
     * Scan for fixed name artwork, such as folder.jpg, backdrop.png, etc.
     *
     * @param movie
     * @return UNKNOWN or Absolute Path
     */
    protected String scanFixedArtwork(Movie movie) {
        String parentPath = FileTools.getParentFolder(movie.getFile());
        String fullFilename;
        File artworkFile;

        for (String imageFileName : artworkImageName) {
            fullFilename = StringTools.appendToPath(parentPath, imageFileName);
            artworkFile = FileTools.findFileFromExtensions(fullFilename, artworkExtensions);

            if (artworkFile.exists()) {
                return artworkFile.getAbsolutePath();
            }
        }
        return Movie.UNKNOWN;
    }

    /**
     * Scan artwork that is named the same as the folder that it is in
     *
     * @param movie
     * @return UNKNOWN or Absolute Path
     */
    protected String scanFolderArtwork(Movie movie) {
        String parentPath = FileTools.getParentFolder(movie.getFile());
        String fullFilename = StringTools.appendToPath(parentPath,
                FileTools.getParentFolderName(movie.getFile()) + artworkTokenOriginal);

        File artworkFile = FileTools.findFileFromExtensions(fullFilename, artworkExtensions);

        if (artworkFile.exists()) {
            return artworkFile.getAbsolutePath();
        }

        return Movie.UNKNOWN;
    }

    /**
     * Scan for TV show SERIES artwork.
     *
     * This usually exists in the directory ABOVE the one the files reside in
     *
     * @param movie
     * @return UNKNOWN or Absolute Path
     */
    protected String scanTvSeriesArtwork(Movie movie) {

        if (!movie.isTVShow()) {
            // Don't process if we are a movie.
            return Movie.UNKNOWN;
        }

        String parentPath = FileTools.getParentFolder(movie.getFile().getParentFile().getParentFile());
        String fullFilename;
        File artworkFile;

        for (String imageFileName : artworkImageName) {
            fullFilename = StringTools.appendToPath(parentPath, imageFileName);
            artworkFile = FileTools.findFileFromExtensions(fullFilename, artworkExtensions);
            if (artworkFile.exists()) {
                return artworkFile.getAbsolutePath();
            }
        }

        return Movie.UNKNOWN;
    }

    /**
     * Scan for artwork named like: {videoFileName}{artworkToken}.{artworkExtensions}
     *
     * @param movie
     * @param artworkFilename
     * @return UNKNOWN or Absolute Path
     */
    protected String scanVideoArtwork(Movie movie, String artworkFilename) {
        return scanVideoArtwork(movie, artworkFilename, "");
    }

    /**
     * Scan for artwork named like: {videoFileName}{artworkToken}.{artworkExtensions}
     *
     * @param movie
     * @param artworkFilename
     * @param additionalPath A sub-directory of the movie to scan
     * @return UNKNOWN or Absolute Path
     */
    protected String scanVideoArtwork(Movie movie, String artworkFilename, String additionalPath) {
        String parentPath = StringTools.appendToPath(FileTools.getParentFolder(movie.getFile()),
                StringUtils.trimToEmpty(additionalPath));
        return scanDirectoryForArtwork(artworkFilename, parentPath);
    }

    /**
     * Set the Image Plugin
     *
     * @param classNameString
     */
    protected void setImagePlugin(String classNameString) {
        String className;
        if (StringTools.isNotValidString(classNameString)) {
            // Use the default image plugin
            className = PropertiesUtil.getProperty("mjb.image.plugin",
                    "com.moviejukebox.plugin.DefaultImagePlugin");
        } else {
            className = classNameString;
        }

        try {
            Thread artThread = Thread.currentThread();
            ClassLoader cl = artThread.getContextClassLoader();
            Class<? extends MovieImagePlugin> pluginClass = cl.loadClass(className)
                    .asSubclass(MovieImagePlugin.class);
            artworkImagePlugin = pluginClass.newInstance();

            return;
        } catch (ClassNotFoundException ex) {
            LOG.error("Error instantiating imagePlugin: {} - class not found!", className);
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (InstantiationException ex) {
            LOG.error("Failed instantiating imagePlugin: {}", className);
            LOG.error(SystemTools.getStackTrace(ex));
        } catch (IllegalAccessException ex) {
            LOG.error("Unable instantiating imagePlugin: {}", className);
            LOG.error(SystemTools.getStackTrace(ex));
        }

        LOG.error("Default plugin will be used instead.");
        // Use the background plugin for fanart, the image plugin for all others
        if (artworkType == ArtworkType.FANART) {
            artworkImagePlugin = new DefaultBackgroundPlugin();
        } else {
            artworkImagePlugin = new DefaultImagePlugin();
        }
    }

    /**
     * Set the default artwork dimensions for use in the validate routine
     */
    private void setArtworkDimensions() {
        // This should return a value, but if it doesn't then we'll default to "posters"
        String artworkPropertyType = getPropertyName();

        artworkWidth = PropertiesUtil.getIntProperty(artworkPropertyType + ".width", 0);
        artworkHeight = PropertiesUtil.getIntProperty(artworkPropertyType + ".height", 0);

        if ((artworkWidth == 0) || (artworkHeight == 0)) {
            // Get the poster type for looking up the defaults
            artworkPropertyType = ArtworkScanner.getPropertyName(ArtworkType.POSTER);
            // There was an issue with the correct properties, so use poster as a default.
            artworkWidth = PropertiesUtil.getIntProperty(artworkPropertyType + ".width", 400);
            artworkHeight = PropertiesUtil.getIntProperty(artworkPropertyType + ".height", 600);
        }
    }

    /**
     * Set the list of artwork extensions to search for using a delimited string
     *
     * @param extensions
     */
    private void setArtworkExtensions(String extensions) {
        StringTokenizer st = new StringTokenizer(extensions, SPLITTER);
        while (st.hasMoreTokens()) {
            artworkExtensions.add(st.nextToken());
        }
    }

    /**
     * Set the priority (order) that the artwork is searched for
     *
     * @param priority
     */
    private void setArtworkPriority(String priority) {
        StringTokenizer st = new StringTokenizer(priority, SPLITTER);
        while (st.hasMoreTokens()) {
            String tempPriority = st.nextToken();
            try {
                artworkPriority.add(ArtworkPriority.fromString(tempPriority));
            } catch (IllegalArgumentException ex) {
                LOG.warn("Failed to set artwork priority '{}', error: {}", tempPriority, ex.getMessage(), ex);
            }
        }
    }

    /**
     * Set the list of folder artwork image names
     *
     * @param artworkImageNameString
     */
    private void setArtworkImageName(String artworkImageNameString) {
        StringTokenizer st = new StringTokenizer(artworkImageNameString, SPLITTER);

        while (st.hasMoreTokens()) {
            artworkImageName.add(st.nextToken());
        }
    }

    /**
     * Set the name for the artwork type to be used in logger messages
     *
     * @param setArtworkType The artwork type to instantiate the scanner for
     */
    private void setArtworkType(ArtworkType setArtworkType) {
        artworkType = setArtworkType;
        artworkTypeName = artworkType.toString().toLowerCase();
    }

    /**
     * Create a safe filename for the artwork
     *
     * @param movie
     * @param appendFormat Append the file format to the filename if required
     * @return
     */
    @Override
    public final String makeSafeOriginalFilename(Movie movie, boolean appendFormat) {
        StringBuilder filename = new StringBuilder();

        filename.append(FileTools.makeSafeFilename(movie.getBaseName()));
        if (StringTools.isValidString(artworkTokenOriginal)) {
            filename.append(artworkTokenOriginal);
        }

        if (appendFormat) {
            filename.append(".").append(artworkFormat);
        }

        LOG.debug("Safe original filename: {}", filename.toString());
        return filename.toString();
    }

    /**
     * Create a safe filename for the artwork
     *
     * @param movie
     * @param appendFormat Append the file format to the filename if required
     * @return
     */
    @Override
    public final String makeSafeJukeboxFilename(Movie movie, boolean appendFormat) {
        StringBuilder filename = new StringBuilder();

        filename.append(FileTools.makeSafeFilename(movie.getBaseName()));
        if (StringTools.isValidString(artworkTokenJukebox)) {
            filename.append(artworkTokenJukebox);
        }

        if (appendFormat) {
            filename.append(".").append(artworkFormat);
        }

        LOG.debug("Safe jukebox filename: {}", filename.toString());
        return filename.toString();
    }

    //<editor-fold defaultstate="collapsed" desc="Debug Methods">
    /**
     * Pretty print method for the debug property output
     *
     * @param propName
     * @param propValue
     * @param addTypeToOutput
     */
    protected final void debugProperty(String propName, Object propValue, boolean addTypeToOutput) {
        StringBuilder property = new StringBuilder("DEBUG - '");
        if (addTypeToOutput) {
            property.append(artworkTypeName);
        }
        property.append(
                StringUtils.rightPad(propName + "' ", (addTypeToOutput ? 40 : 40 + artworkTypeName.length()), "."))
                .append(" = ");
        property.append(propValue);
        LOG.debug(property.toString());
    }

    /**
     * Pretty print method for the debug property output
     *
     * @param propName
     * @param propValue
     */
    protected final void debugProperty(String propName, Object propValue) {
        debugProperty(propName, propValue, Boolean.TRUE);
    }

    /**
     * Output the properties used by this scanner
     */
    public final void debugOutput() {
        debugProperty(" Required?", isSearchRequired());
        debugProperty(" Local Required?", isSearchLocal());
        debugProperty(" TV Download?", artworkDownloadTv);
        debugProperty(" Movie Download?", artworkDownloadMovie);
        debugProperty(".scanner.imageName", artworkImageName);
        debugProperty(".scanner.searchForExistingArtwork", artworkSearchLocal);
        debugProperty(".scanner.artworkToken", artworkTokenOriginal);
        debugProperty(".format", artworkFormat);
        debugProperty(".scanner.Validate", artworkValidate);
        debugProperty(".scanner.ValidateMatch", artworkValidateMatch);
        debugProperty(".scanner.ValidateAspect", artworkValidateAspect);
        debugProperty(".scanner.artworkDirectory", artworkDirectory);
        debugProperty(".scanner.artworkPriority", artworkPriority);
        debugProperty(".movie.download", artworkDownloadMovie);
        debugProperty(".tv.download", artworkDownloadTv);
        debugProperty(".scanner.artworkExtensions", artworkExtensions);
        debugProperty(getPropertyName() + ".width", artworkWidth, Boolean.FALSE);
        debugProperty(getPropertyName() + ".height", artworkHeight, Boolean.FALSE);
    }
    //</editor-fold>

    /**
     * Return the name used in the properties file for this artwork type
     *
     * This is needed because of the disconnection between what was originally in the properties files and making it generic enough
     * for this scanner
     *
     * @return
     */
    protected final String getPropertyName() {
        return ArtworkScanner.getPropertyName(artworkType);
    }

    /**
     * Return the name used in the properties file for this artwork type
     *
     * This is needed because of the disconnection between what was originally in the properties files and making it generic enough
     * for this scanner
     *
     * @param artworkType
     * @return
     */
    public static String getPropertyName(ArtworkType artworkType) {
        if (artworkType == ArtworkType.POSTER) {
            return "posters";
        }

        if (artworkType == ArtworkType.FANART) {
            return "";
        }

        if (artworkType == ArtworkType.BANNER) {
            return "banners";
        }

        if (artworkType == ArtworkType.VIDEOIMAGE) {
            return "videoimages";
        }

        return artworkType.toString().toLowerCase();
    }

    /**
     * Return the filename to be used for the dummy artwork
     *
     * @return
     */
    protected final String getDummyFilename() {
        StringBuilder artworkDummy;
        if (artworkType == ArtworkType.POSTER) {
            artworkDummy = new StringBuilder("dummy.jpg");
        } else if (artworkType == ArtworkType.FANART) {
            // There is no dummy artwork for fanart
            artworkDummy = new StringBuilder("");
        } else {
            // guess a default dummy name
            artworkDummy = new StringBuilder("dummy_");
            artworkDummy.append(artworkTypeName);
            artworkDummy.append(".");
            artworkDummy.append(artworkFormat);
        }

        LOG.trace("Using dummy image name of '{}'", artworkDummy.toString());
        return artworkDummy.toString();
    }
}