com.moviejukebox.tools.FileTools.java Source code

Java tutorial

Introduction

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

import static com.moviejukebox.tools.PropertiesUtil.getProperty;
import static org.apache.commons.lang3.StringUtils.substringAfter;
import static org.apache.commons.lang3.StringUtils.substringBefore;
import static org.apache.commons.lang3.StringUtils.trimToNull;

import com.moviejukebox.model.Jukebox;
import com.moviejukebox.model.Movie;
import com.moviejukebox.model.MovieFile;
import com.moviejukebox.scanner.IArchiveScanner;
import java.io.*;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class FileTools {

    private static final Logger LOG = LoggerFactory.getLogger(FileTools.class);
    private static final int BUFF_SIZE = 16 * 1024;
    private static final Collection<String> SUBTITLE_EXTENSIONS = new ArrayList<>();
    private static final Collection<ReplaceEntry> UNSAFE_CHARS = new ArrayList<>();
    private static final Collection<String> GENERATED_FILENAMES = Collections
            .synchronizedCollection(new ArrayList<String>());
    private static boolean videoimageDownload = PropertiesUtil.getBooleanProperty("mjb.includeVideoImages",
            Boolean.FALSE);
    private static int footerImageEnabled = PropertiesUtil.getIntProperty("mjb.footer.count", 0);
    private static String indexFilesPrefix = getProperty("mjb.indexFilesPrefix", "");
    // Literals
    private static final String BDMV_STREAM = File.separator + "BDMV" + File.separator + "STREAM";
    // File Cache
    public static ScannedFilesCache fileCache = new ScannedFilesCache();
    // Lock for mkdirs
    private static Lock fsLock = new ReentrantLock();
    private static final String DEFAULT_CHARSET = "UTF-8";
    private static final int MAX_TRIES = 5;

    /**
     * Gabriel Corneanu: One buffer for each thread to allow threaded copies
     */
    private static final ThreadLocal<byte[]> THREAD_BUFFER = new ThreadLocal<byte[]>() {
        @Override
        protected byte[] initialValue() {
            return new byte[BUFF_SIZE];
        }
    };

    private FileTools() {
        throw new UnsupportedOperationException("Class cannot be instantiated");
    }

    public static void initSubtitleExtensions() {
        if (SUBTITLE_EXTENSIONS.isEmpty()) {
            SUBTITLE_EXTENSIONS.addAll(Arrays.asList(
                    PropertiesUtil.getProperty("filename.scanner.subtitle", "SRT,SUB,SSA,SMI,PGS").split(",")));
        }
    }

    private static class ReplaceEntry {

        private String oldText, newText;
        private int oldLength;

        public ReplaceEntry(String oldtext, String newtext) {
            this.oldText = oldtext;
            this.newText = newtext;
            oldLength = oldtext.length();
        }

        public String check(String filename) {
            String newFilename = filename;
            int pos = newFilename.indexOf(oldText, 0);
            while (pos >= 0) {
                newFilename = newFilename.substring(0, pos) + newText + newFilename.substring(pos + oldLength);
                pos = newFilename.indexOf(oldText, pos + oldLength);
            }
            return newFilename;
        }
    }

    public static void initUnsafeChars() {
        if (!UNSAFE_CHARS.isEmpty()) {
            return;
        }

        // What to do if the user specifies a blank encodeEscapeChar? I guess disable encoding.
        String encodeEscapeCharString = PropertiesUtil.getProperty("mjb.charset.filenameEncodingEscapeChar", "$");
        if (encodeEscapeCharString.length() > 0) {
            // What to do if the user specifies a >1 character long string? I guess just use the first char.
            final Character ENCODE_ESCAPE_CHAR = encodeEscapeCharString.charAt(0);

            String repChars = PropertiesUtil.getProperty("mjb.charset.unsafeFilenameChars", "<>:\"/\\|?*");
            for (String repChar : repChars.split("")) {
                if (repChar.length() > 0) {
                    char ch = repChar.charAt(0);
                    // Don't encode characters that are hex digits
                    // Also, don't encode the escape char -- it is safe by definition!
                    if (!Character.isDigit(ch) && -1 == "AaBbCcDdEeFf".indexOf(ch)
                            && !ENCODE_ESCAPE_CHAR.equals(ch)) {
                        String hex = Integer.toHexString(ch).toUpperCase();
                        UNSAFE_CHARS.add(new ReplaceEntry(repChar, ENCODE_ESCAPE_CHAR + hex));
                    }
                }
            }
        }

        // Parse transliteration map: (source_character [-] transliteration_sequence [,])+
        StringTokenizer st = new StringTokenizer(PropertiesUtil.getProperty("mjb.charset.filename.translate", ""),
                ",");
        while (st.hasMoreElements()) {
            final String token = st.nextToken();
            String beforeStr = substringBefore(token, "-");
            final String character = beforeStr.length() == 1 && ("\t".equals(beforeStr) || " ".equals(beforeStr))
                    ? beforeStr
                    : trimToNull(beforeStr);
            if (character == null) {
                // TODO Error message?
                continue;
            }
            String afterStr = substringAfter(token, "-");
            final String translation = afterStr.length() == 1 && ("\t".equals(afterStr) || " ".equals(afterStr))
                    ? afterStr
                    : trimToNull(afterStr);
            if (translation == null) {
                // TODO Error message?
                // TODO Allow empty transliteration?
                continue;
            }
            UNSAFE_CHARS.add(new ReplaceEntry(character.toUpperCase(), translation.toUpperCase()));
            UNSAFE_CHARS.add(new ReplaceEntry(character.toLowerCase(), translation.toLowerCase()));
        }
    }

    public static int copy(InputStream is, OutputStream os) throws IOException {
        int bytesCopied = 0;
        byte[] buffer = THREAD_BUFFER.get();
        while (Boolean.TRUE) {
            int amountRead = is.read(buffer);
            if (amountRead == -1) {
                break;
            }
            bytesCopied += amountRead;
            os.write(buffer, 0, amountRead);
        }
        return bytesCopied;
    }

    /**
     * Copy the source file to the destination
     *
     * @param src
     * @param dst
     * @return
     */
    public static boolean copyFile(String src, String dst) {
        File srcFile = new File(src);
        File dstFile = new File(dst);
        return copyFile(srcFile, dstFile);
    }

    /**
     * Copy the source file to the destination
     *
     * @param src
     * @param dst
     * @return
     */
    public static boolean copyFile(File src, File dst) {
        boolean returnValue = Boolean.FALSE;

        if (!src.exists()) {
            LOG.error("The file '{}' does not exist!", src);
            return returnValue;
        }

        if (dst.isDirectory()) {
            makeDirs(dst);
            returnValue = copyFile(src, new File(dst + File.separator + src.getName()));
        } else {

            try (FileInputStream inSource = new FileInputStream(src);
                    FileOutputStream outSource = new FileOutputStream(dst);
                    FileChannel inChannel = inSource.getChannel();
                    FileChannel outChannel = outSource.getChannel()) {

                long p = 0, s = inChannel.size();
                while (p < s) {
                    p += inChannel.transferTo(p, 1024 * 1024, outChannel);
                }
                return Boolean.TRUE;
            } catch (IOException error) {
                LOG.error("Failed copying file '{}' to '{}'", src, dst);
                LOG.error(SystemTools.getStackTrace(error));
                returnValue = Boolean.FALSE;
            }
        }

        return returnValue;
    }

    /**
     * Copy files from one directory to another
     *
     * @param srcPathName The source directory to copy from
     * @param dstPathName The target directory to copy to
     * @param updateDisplay Display an update to the console
     */
    public static void copyDir(String srcPathName, String dstPathName, boolean updateDisplay) {
        copyDir(srcPathName, dstPathName, updateDisplay, null);
    }

    /**
     * Copy files from one directory to another
     *
     * @param srcPathName The source directory to copy from
     * @param dstPathName The target directory to copy to
     * @param updateDisplay Display an update to the console
     * @param pathRoot The root of the srcPath for display purposes
     */
    public static void copyDir(String srcPathName, String dstPathName, boolean updateDisplay, String pathRoot) {
        String displayRoot;
        if (StringTools.isNotValidString(pathRoot)) {
            int pos = srcPathName.lastIndexOf(File.separator);
            if (pos > 0) {
                try {
                    displayRoot = srcPathName.substring(pos + 1);
                } catch (Exception error) {
                    displayRoot = srcPathName;
                }
            } else {
                displayRoot = srcPathName;
            }
        } else {
            displayRoot = pathRoot;
        }

        try {
            File srcDir = new File(srcPathName);
            if (!srcDir.exists()) {
                LOG.error("Source directory {} does not exist!", srcPathName);
                return;
            }

            File dstDir = new File(dstPathName);
            makeDirs(dstDir);

            if (!dstDir.exists()) {
                LOG.error("Target directory {} does not exist!", dstPathName);
                return;
            }

            if (srcDir.isFile()) {
                copyFile(srcDir, dstDir);
            } else {

                File[] contentList = srcDir.listFiles();
                if (contentList != null) {
                    List<File> files = Arrays.asList(contentList);
                    Collections.sort(files);

                    int totalSize = files.size();
                    int currentFile = 0;

                    String displayPath = srcDir.getCanonicalPath();

                    int pos = displayPath.lastIndexOf(displayRoot);
                    if (pos > 0) {
                        displayPath = displayPath.substring(pos);
                    }

                    for (File file : files) {
                        currentFile++;
                        if (!".svn".equals(file.getName())) {
                            if (file.isDirectory()) {
                                copyDir(file.getAbsolutePath(), dstPathName + File.separator + file.getName(),
                                        updateDisplay, pathRoot);
                            } else {
                                if (updateDisplay) {
                                    System.out.print("\r    Copying directory " + displayPath + " (" + currentFile
                                            + "/" + totalSize + ")");
                                    if (LOG.isTraceEnabled()) {
                                        LOG.trace("Copying: {}", file.getName());
                                    }
                                }
                                copyFile(file, dstDir);
                            }
                        }
                    }

                    if (updateDisplay && !files.isEmpty()) {
                        System.out.print("\n");
                    }

                    LOG.debug("Copied {} files from {}", totalSize, srcDir.getCanonicalPath());
                }
            }
        } catch (IOException error) {
            LOG.error("Failed to copy '{}' to '{}'", srcPathName, dstPathName);
            LOG.error(SystemTools.getStackTrace(error));
        }
    }

    /**
     * Read a file and return it as a string using default encoding
     *
     * @param file
     * @return
     */
    public static String readFileToString(File file) {
        return readFileToString(file, DEFAULT_CHARSET);
    }

    /**
     * Read a file and return it as a string
     *
     * @param file
     * @param encoding
     * @return
     */
    public static String readFileToString(File file, String encoding) {
        String data = "";
        if (file == null) {
            LOG.error("Failed reading file, file is null");
        } else {
            try {
                data = FileUtils.readFileToString(file, encoding);
            } catch (IOException ex) {
                LOG.error("Failed reading file {} - Error: {}", file.getName(), ex.getMessage());
            }
        }
        return data;
    }

    /**
     * Write string to a file (Used for debugging)
     *
     * @param filename Filename to write the output to
     * @param outputString The string to save
     */
    public static void writeStringToFile(String filename, String outputString) {
        File outFile = new File(filename);
        try {
            LOG.debug("Writing string to '{}'", outFile.getAbsolutePath());
            FileUtils.writeStringToFile(outFile, outputString);
        } catch (IOException ex) {
            LOG.warn("Failed to write to file '{}': {}", outFile.getAbsolutePath(), ex.getMessage(), ex);
        }
    }

    /**
     * *
     * @author Stuart Boston
     * @param file1 - first file to compare
     * @param file2 - second file to compare
     * @return true if the files exist and file2 is older, false otherwise.
     *
     * Note that file1 will be checked to see if it's newer than file2
     */
    public static boolean isNewer(File file1, File file2) {
        // TODO: Update this routine to use fileCache
        // If file1 exists and file2 doesn't then return true
        if (file1.exists()) {
            // If file2 doesn't exist then file1 is newer
            if (!file2.exists()) {
                return Boolean.TRUE;
            }
        } else {
            // File1 doesn't exist so return false
            return Boolean.FALSE;
        }

        // Compare the file dates. This is only true if the first file is newer than the second, as the second file is the file2 file
        if (file1.lastModified() <= file2.lastModified()) {
            // file1 is older than the file2.
            return Boolean.FALSE;
        }
        // file1 is newer than file2
        return Boolean.TRUE;
    }

    public static String createCategoryKey(String key2) {
        return key2;
    }

    public static String createPrefix(String category, String key) {
        StringBuilder prefix = new StringBuilder(indexFilesPrefix);
        prefix.append(category).append('_').append(key).append('_');
        return prefix.toString();
    }

    public static OutputStream createFileOutputStream(File f, int size) throws FileNotFoundException {
        // return new FileOutputStream(f);
        return new BufferedOutputStream(new FileOutputStream(f), size);
    }

    public static OutputStream createFileOutputStream(File f) throws FileNotFoundException {
        return createFileOutputStream(f, 10 * 1024);
    }

    public static OutputStream createFileOutputStream(String f) throws FileNotFoundException {
        return createFileOutputStream(new File(f));
    }

    public static OutputStream createFileOutputStream(String f, int size) throws FileNotFoundException {
        return createFileOutputStream(new File(f), size);
    }

    public static InputStream createFileInputStream(File f) throws FileNotFoundException {
        // return new FileInputStream(f);
        return new BufferedInputStream(new FileInputStream(f), 10 * 1024);
    }

    public static InputStream createFileInputStream(String f) throws FileNotFoundException {
        return createFileInputStream(new File(f));
    }

    public static String makeSafeFilename(String filename) {
        String newFilename = filename;

        for (ReplaceEntry rep : UNSAFE_CHARS) {
            newFilename = rep.check(newFilename);
        }

        if (!newFilename.equals(filename)) {
            LOG.debug("Encoded filename string '{}' to '{}'", filename, newFilename);
        }

        return newFilename;
    }

    /**
     * Returns the given path in canonical form
     *
     * i.e. no duplicated separators, no ".", ".."..., and ending without trailing separator the only exception is a root! the
     * canonical form for a root INCLUDES the separator
     *
     * @param path
     * @return
     */
    public static String getCanonicalPath(String path) {
        try {
            return new File(path).getCanonicalPath();
        } catch (IOException e) {
            return path;
        }
    }

    /**
     * when concatenating paths and the source MIGHT be a root, use this function to safely add the separator
     *
     * @param path
     * @return
     */
    public static String getDirPathWithSeparator(String path) {
        return path.endsWith(File.separator) ? path : path + File.separator;
    }

    /**
     * Get the extension of a file.
     *
     * @param filename
     * @return
     */
    public static String getFileExtension(String filename) {
        return FilenameUtils.getExtension(filename);
    }

    /**
     * Returns the parent folder name only; used when searching for artwork...
     *
     * @param file
     * @return
     */
    public static String getParentFolderName(File file) {
        if (file == null) {
            return "";
        }
        String path = file.getParent();
        return path.substring(path.lastIndexOf(File.separator) + 1);
    }

    /**
     *
     * Pass in the filename and a list of extensions.
     *
     * This function will scan for the filename plus extensions and return the File
     *
     * @param fullBaseFilename
     * @param fileExtensions
     * @return always a File, to be tested with exists() for valid file
     */
    public static File findFileFromExtensions(String fullBaseFilename, Collection<String> fileExtensions) {
        File localFile = null;

        for (String extension : fileExtensions) {
            localFile = fileCache.getFile(fullBaseFilename + "." + extension);
            if (localFile.exists()) {
                if (LOG.isTraceEnabled()) {
                    LOG.trace("Found {} in the file cache", localFile);
                }
                return localFile;
            }
        }

        return (localFile == null ? new File(Movie.UNKNOWN) : localFile); //just in case
    }

    /**
     * Search for the filename in the cache and look for each with the extensions
     *
     * @param searchFilename
     * @param fileExtensions
     * @param jukebox
     * @return
     */
    public static File findFilenameInCache(String searchFilename, Collection<String> fileExtensions,
            Jukebox jukebox) {
        return findFilenameInCache(searchFilename, fileExtensions, jukebox, Boolean.FALSE, Movie.UNKNOWN);
    }

    /**
     * Search for the filename in the cache and look for each with the extensions
     *
     * @param searchFilename
     * @param fileExtensions
     * @param jukebox
     * @param includeJukebox
     * @return
     */
    public static File findFilenameInCache(String searchFilename, Collection<String> fileExtensions,
            Jukebox jukebox, boolean includeJukebox) {
        return findFilenameInCache(searchFilename, fileExtensions, jukebox, includeJukebox, Movie.UNKNOWN);
    }

    /**
     * Search for the filename in the cache and look for each with the extensions
     *
     * @param searchFilename
     * @param fileExtensions
     * @param jukebox
     * @param includeJukebox
     * @param subFolder
     * @return
     */
    public static File findFilenameInCache(String searchFilename, Collection<String> fileExtensions,
            Jukebox jukebox, boolean includeJukebox, String subFolder) {
        File searchFile = null;
        String safeFilename = makeSafeFilename(searchFilename);

        safeFilename = File.separator + safeFilename;

        StringBuilder jukeboxSubFolder = new StringBuilder(jukebox.getJukeboxRootLocationDetails().toLowerCase());
        if (StringTools.isValidString(subFolder)) {
            jukeboxSubFolder.append(File.separator).append((subFolder.toLowerCase()));
        }

        Collection<File> files = FileTools.fileCache.searchFilename(safeFilename, Boolean.TRUE);

        if (!files.isEmpty()) {
            // Copy the synchronized list to avoid ConcurrentModificationException
            Iterator<File> iter = new ArrayList<>(FileTools.fileCache.searchFilename(safeFilename, Boolean.TRUE))
                    .iterator();

            while (iter.hasNext() && (searchFile == null)) {
                File file = iter.next();
                String abPath = file.getAbsolutePath().toLowerCase();

                if (!includeJukebox && abPath.startsWith(jukeboxSubFolder.toString())) {
                    // Skip any files found in the jukebox
                    continue;
                }

                // Loop round the filename+extension to see if any exist and add them to the array
                String checkFilename;
                for (String extension : fileExtensions) {
                    checkFilename = safeFilename + "." + extension;
                    if (abPath.endsWith((checkFilename).toLowerCase())) {
                        searchFile = file;
                        // We've found the file, so we should quit the loop
                        break;
                    }
                }
            }

            if (searchFile != null) {
                LOG.debug("Using first one found: {}", searchFile.getAbsolutePath());
            } else {
                LOG.debug("No matching files {} found for {}", fileExtensions, safeFilename);
            }
        } else {
            LOG.debug("No scanned files found for {}", searchFilename);
        }

        return searchFile;
    }

    /**
     * Download the image for the specified URL into the specified file. Utilises the WebBrowser downloadImage function to allow for
     * proxy connections.
     *
     * @param imageFile
     * @param imageURL
     * @return
     * @throws IOException
     */
    public static boolean downloadImage(File imageFile, String imageURL) throws IOException {
        URL url;
        if (imageURL.contains(" ")) {
            url = new URL(imageURL.replaceAll(" ", "%20"));
        } else {
            url = new URL(imageURL);
        }

        if ("file".equals(url.getProtocol())) {
            LOG.debug("Copy from url: '{}'", url);
            try (InputStream in = url.openStream(); OutputStream out = new FileOutputStream(imageFile)) {
                copy(in, out);
            }
            return true;
        }

        // download image
        return YamjHttpClientBuilder.getHttpClient().downloadImage(imageFile, url);
    }

    /**
     * Find the parent directory of the movie file.
     *
     * @param movieFile
     * @return Parent folder
     * @author Stuart Boston
     */
    public static String getParentFolder(File movieFile) {
        String parentFolder;

        if (movieFile.isDirectory()) { // for VIDEO_TS
            parentFolder = movieFile.getPath();
        } else {
            parentFolder = movieFile.getParent();
        }

        // Issue 1070, /BDMV/STREAM is being appended to the parent path
        if (parentFolder.toUpperCase().endsWith(BDMV_STREAM)) {
            parentFolder = parentFolder.substring(0, parentFolder.length() - 12);
        }

        return parentFolder;
    }

    /**
     * Recursively delete a directory
     *
     * @param dir
     * @return
     */
    public static boolean deleteDir(String dir) {
        return deleteDir(new File(dir));
    }

    /**
     * Recursively delete a directory
     *
     * @param dir
     * @return
     */
    public static boolean deleteDir(File dir) {
        if (dir.isDirectory()) {
            String[] children = dir.list();
            for (String children1 : children) {
                boolean success = deleteDir(new File(dir, children1));
                if (!success) {
                    // System.out.println("Failed");
                    return Boolean.FALSE;
                }
            }
        }
        // The directory is now empty so delete it
        // System.out.println("Deleting: " + dir.getAbsolutePath());
        return dir.delete();
    }

    /**
     * Add a list of files to the jukebox filenames
     *
     * @param filenames
     */
    public static void addJukeboxFiles(Collection<String> filenames) {
        GENERATED_FILENAMES.addAll(filenames);
    }

    /**
     * Add an individual filename to the jukebox cleaning exclusion list
     *
     * @param filename
     */
    public static void addJukeboxFile(String filename) {
        if (StringTools.isValidString(filename)) {
            GENERATED_FILENAMES.add(filename);

            if (LOG.isTraceEnabled()) {
                LOG.trace("Adding {} to safe jukebox files", filename);
            }
        }
    }

    /**
     * Process the movie and add all the files to the jukebox cleaning exclusion list
     *
     * @param movie
     */
    public static void addMovieToJukeboxFilenames(Movie movie) {
        addJukeboxFile(movie.getPosterFilename());
        addJukeboxFile(movie.getDetailPosterFilename());
        addJukeboxFile(movie.getThumbnailFilename());
        addJukeboxFile(movie.getBannerFilename());
        addJukeboxFile(movie.getFanartFilename());

        if (videoimageDownload) {
            for (MovieFile mf : movie.getFiles()) {
                for (int part = mf.getFirstPart(); part <= mf.getLastPart(); part++) {
                    addJukeboxFile(mf.getVideoImageFilename(part));
                }
            }
        }

        addJukeboxFile(movie.getClearArtFilename());
        addJukeboxFile(movie.getClearLogoFilename());
        addJukeboxFile(movie.getSeasonThumbFilename());
        addJukeboxFile(movie.getTvThumbFilename());
        addJukeboxFile(movie.getMovieDiscFilename());

        // Are footer images enabled?
        if (footerImageEnabled > 0) {
            addJukeboxFiles(movie.getFooterFilename());
        }
    }

    public static Collection<String> getJukeboxFiles() {
        return GENERATED_FILENAMES;
    }

    /**
     * Special File with "cached" attributes used to minimize file system access which slows down everything
     *
     * @author Gabriel Corneanu
     */
    public static class FileEx extends File {

        private static final long serialVersionUID = 1L;
        private volatile Boolean isDir = null;
        private volatile Boolean fileExists = null;
        private volatile Boolean isfile = null;
        private volatile Long fileLen = null;
        private volatile Long fileLastModified = null;
        private IArchiveScanner[] archiveScanners;
        private volatile File[] listFiles;

        //Standard constructors
        public FileEx(String parent, String child) {
            super(parent, child);
        }

        public FileEx(String pathname) {
            super(pathname);
        }

        public FileEx(File parent, String child) {
            super(parent, child);
        }

        private FileEx(String pathname, boolean exists) {
            this(pathname);
            fileExists = exists;
        }

        // archive scanner supporting constructors
        public FileEx(String pathname, IArchiveScanner[] archiveScanners) {
            super(pathname);
            if (archiveScanners == null) {
                this.archiveScanners = null;
            } else {
                this.archiveScanners = archiveScanners.clone();
            }
        }

        public FileEx(File parent, String child, IArchiveScanner[] archiveScanners) {
            this(parent, child);
            if (archiveScanners == null) {
                this.archiveScanners = null;
            } else {
                this.archiveScanners = archiveScanners.clone();
            }
        }

        @Override
        public boolean isDirectory() {
            if (isDir == null) {
                synchronized (this) {
                    if (isDir == null) {
                        isDir = super.isDirectory();
                    }
                }
            }
            return isDir;
        }

        @Override
        public boolean exists() {
            if (fileExists == null) {
                synchronized (this) {
                    if (fileExists == null) {
                        fileExists = super.exists();
                    }
                }
            }
            return fileExists;
        }

        @Override
        public boolean isFile() {
            if (isfile == null) {
                synchronized (this) {
                    if (isfile == null) {
                        isfile = super.isFile();
                    }
                }
            }
            return isfile;
        }

        @Override
        public long length() {
            if (fileLen == null) {
                synchronized (this) {
                    if (fileLen == null) {
                        fileLen = super.length();
                    }
                }
            }
            return fileLen;
        }

        @Override
        public long lastModified() {
            if (fileLastModified == null) {
                synchronized (this) {
                    if (fileLastModified == null) {
                        fileLastModified = super.lastModified();
                    }
                }
            }
            return fileLastModified;
        }

        @Override
        public File getParentFile() {
            String p = this.getParent();
            if (p == null) {
                return null;
            }
            return new FileEx(p, archiveScanners);
        }

        @Override
        public File[] listFiles() {
            synchronized (this) {
                if (listFiles != null) {
                    return listFiles;
                }

                String[] nameStrings = list();
                if (nameStrings == null) {
                    return null;
                }

                List<String> mutableNames = new ArrayList<>(Arrays.asList(nameStrings));
                List<File> files = new ArrayList<>();
                if (archiveScanners != null) {
                    for (IArchiveScanner as : archiveScanners) {
                        files.addAll(as.getArchiveFiles(this, mutableNames));
                    }
                }

                for (String name : mutableNames) {
                    FileEx fe = new FileEx(this, name, archiveScanners);
                    fe.fileExists = Boolean.TRUE;
                    files.add(fe);
                }

                listFiles = files.toArray(new File[files.size()]);
            }
            return listFiles;
        }

        @Override
        public File[] listFiles(FilenameFilter filter) {
            File[] src = listFiles();

            if (src == null) {
                return null;
            }

            if (filter == null) {
                return Arrays.copyOf(src, src.length);
            }

            List<File> l = new ArrayList<>();
            for (File f : src) {
                if (filter.accept(this, f.getName())) {
                    l.add(f);
                }
            }
            return l.toArray(new File[l.size()]);
        }
    }

    /**
     * cached File instances the key is always absolute path in upper-case, so it will NOT work for case only differences
     *
     * @author Gabriel Corneanu
     */
    public static class ScannedFilesCache {
        //cache for ALL files found during initial scan

        private final Map<String, File> cachedFiles = new ConcurrentHashMap<>(1000);

        /**
         * Check whether the file exists
         *
         * @param absPath
         * @return
         */
        public boolean fileExists(String absPath) {
            return cachedFiles.containsKey(absPath.toUpperCase());
        }

        /**
         * Check whether the file exists
         *
         * @param file
         * @return
         */
        public boolean fileExists(File file) {
            return cachedFiles.containsKey(file.getAbsolutePath().toUpperCase());
        }

        /**
         * Add a file instance to cache
         *
         * @param file
         */
        public void fileAdd(File file) {
            cachedFiles.put(file.getAbsolutePath().toUpperCase(), file);
        }

        /**
         * Retrieve a file from cache
         *
         * If it is NOT found, construct one instance and mark it as non-existing.
         *
         * The exist() test is used very often throughout the library to search for specific files.
         *
         * The path MUST be canonical (i.e. carefully constructed)
         *
         * We do NOT want here to make it canonical because it goes to the file system and it's slow.
         *
         * @param path
         * @return
         */
        public File getFile(String path) {
            File f = cachedFiles.get(path.toUpperCase());
            return (f == null ? new FileEx(path, Boolean.FALSE) : f);
        }

        /**
         * Add a full directory listing; used for existing jukebox
         *
         * @param dir
         * @param depth
         */
        public void addDir(File dir, int depth) {
            File[] files = dir.listFiles();
            if (files == null) {
                return;
            }

            if (files.length == 0) {
                return;
            }

            addFiles(files);
            if (depth <= 0) {
                return;
            }

            for (File f : files) {
                if (f.isDirectory()) {
                    addDir(f, depth - 1);
                }
            }
        }

        public void addFiles(File[] files) {
            if (files.length == 0) {
                return;
            }
            Map<String, File> map = new HashMap<>(files.length);
            for (File f : files) {
                map.put(f.getAbsolutePath().toUpperCase(), f);
            }
            cachedFiles.putAll(map);
        }

        public long size() {
            return cachedFiles.size();
        }

        public Collection<File> searchFilename(String searchName, boolean findAll) {
            ArrayList<File> files = new ArrayList<>();

            String upperName = searchName.toUpperCase();

            for (String listName : cachedFiles.keySet()) {
                if (listName.contains(upperName)) {
                    files.add(cachedFiles.get(listName));
                    if (!findAll) {
                        // We only look for the first
                        break;
                    }
                }
            }

            return files;
        }

        public void saveFileList(String filename) throws FileNotFoundException, UnsupportedEncodingException {
            try (PrintWriter p = new PrintWriter(
                    new OutputStreamWriter(new FileOutputStream(filename, Boolean.TRUE), DEFAULT_CHARSET))) {
                Set<String> names = cachedFiles.keySet();
                String[] sortednames = names.toArray(new String[names.size()]);
                Arrays.sort(sortednames);
                for (String f : sortednames) {
                    p.println(f);
                }
            }
        }
    }

    /**
     * Look for any subtitle files for a file
     *
     * @param fileToScan
     * @return
     */
    public static File findSubtitles(File fileToScan) {
        String basename = FilenameUtils.removeExtension(fileToScan.getAbsolutePath().toUpperCase());
        return findFileFromExtensions(basename, SUBTITLE_EXTENSIONS);
    }

    /**
     * Create a directory hash from the filename
     *
     * @param filename
     * @return
     */
    public static String createDirHash(final String filename) {
        // Skip if the filename is invalid OR has already been hashed
        if (StringTools.isNotValidString(filename) || filename.contains(File.separator)) {
            return filename;
        }

        // Remove all the non-word characters from the filename, replacing with an underscore
        String cleanFilename = filename.replaceAll("[^\\p{L}\\p{N}]", "_").toLowerCase().trim();

        StringBuilder dirHash = new StringBuilder();
        dirHash.append(cleanFilename.substring(0, 1)).append(File.separator);
        dirHash.append(cleanFilename.substring(0, cleanFilename.length() > 1 ? 2 : 1)).append(File.separator);
        dirHash.append(filename);

        return dirHash.toString();
    }

    /**
     * Create a directory has from the filename of a file
     *
     * @param file
     * @return
     */
    public static String createDirHash(final File file) {
        return createDirHash(file.getName());
    }

    /**
     * Create all directories up to the level of the file passed
     *
     * @param file Source directory or file to create the directories directories
     * @return
     */
    public static Boolean makeDirsForFile(final File file) {
        if (file.isDirectory()) {
            // no need to alter the file
            return makeDirs(file);
        }
        return makeDirs(file.getParentFile());
    }

    /**
     * Create all directories up to the level of the directory passed
     *
     * @param sourceDirectory Source directory or file to create the directories
     * @return
     */
    public static Boolean makeDirs(final File sourceDirectory) {
        if (sourceDirectory.exists()) {
            return Boolean.TRUE;
        }

        LOG.trace("Creating directories for {}", sourceDirectory.getAbsolutePath());

        fsLock.lock();
        try {
            boolean status = sourceDirectory.mkdirs();
            int looper = 1;
            while (!status && looper++ <= MAX_TRIES) {
                status = sourceDirectory.mkdirs();
            }
            if (status && looper > 10) {
                LOG.error("Failed creating the directory ({}). Ensure this directory is read/write!",
                        sourceDirectory.getAbsolutePath());
                return Boolean.FALSE;
            }
            return Boolean.TRUE;
        } finally {
            fsLock.unlock();
        }
    }
}