com.moviejukebox.scanner.MovieDirectoryScanner.java Source code

Java tutorial

Introduction

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

import com.moviejukebox.model.Library;
import com.moviejukebox.model.MediaLibraryPath;
import com.moviejukebox.model.Movie;
import com.moviejukebox.model.MovieFile;
import com.moviejukebox.model.MovieFileNameDTO;
import com.moviejukebox.scanner.BDRipScanner.BDFilePropertiesMovie;
import com.moviejukebox.tools.DateTimeTools;
import com.moviejukebox.tools.FileTools;
import com.moviejukebox.tools.HTMLTools;
import com.moviejukebox.tools.OverrideTools;
import com.moviejukebox.tools.PropertiesUtil;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DirectoryScanner
 *
 * @author jjulien
 * @author gaelead
 * @author jriihi
 */
public class MovieDirectoryScanner {

    private static final String SOURCE_FILENAME = "filename";
    private static final Logger LOG = LoggerFactory.getLogger(MovieDirectoryScanner.class);
    private static int dirCount = 1;
    private static int fileCount = 0;
    private static final Pattern PATTERN_RAR_PART = Pattern.compile("\\.part(\\d+)\\.rar");

    private int mediaLibraryRootPathIndex; // always includes path delimiter
    private final Set<String> supportedExtensions = new HashSet<>();
    private final String thumbnailsFormat;
    private final String postersFormat;
    private final String posterToken;
    private final String thumbnailToken;
    private final String bannersFormat;
    private final String bannerToken;
    private final String wideBannerToken;
    private final String opensubtitles;
    private final int hashpathdepth;
    private final Boolean excludeFilesWithoutExternalSubtitles;
    private final Boolean excludeMultiPartBluRay;
    private final Boolean playFullBluRayDisk;
    private final Boolean nmjCompliant;

    // BD rip infos Scanner
    private final BDRipScanner localBDRipScanner;

    // Archived virtual directories (f.ex. rar, zip, tar.gz etc.)
    private IArchiveScanner[] archiveScanners;

    public MovieDirectoryScanner() {
        supportedExtensions.addAll(Arrays.asList(
                PropertiesUtil.getProperty("mjb.extensions", "AVI DIVX MKV WMV M2TS TS RM QT ISO VOB MPG MOV")
                        .toUpperCase().split(" ")));
        thumbnailsFormat = PropertiesUtil.getProperty("thumbnails.format", "png");
        postersFormat = PropertiesUtil.getProperty("posters.format", "png");
        bannersFormat = PropertiesUtil.getProperty("banners.format", "png");
        thumbnailToken = PropertiesUtil.getProperty("mjb.scanner.thumbnailToken", "_small");
        posterToken = PropertiesUtil.getProperty("mjb.scanner.posterToken", "_large");
        bannerToken = PropertiesUtil.getProperty("mjb.scanner.bannerToken", ".banner");
        wideBannerToken = PropertiesUtil.getProperty("mjb.scanner.wideBannerToken", ".wide");
        excludeFilesWithoutExternalSubtitles = PropertiesUtil
                .getBooleanProperty("mjb.subtitles.ExcludeFilesWithoutExternal", Boolean.FALSE);
        excludeMultiPartBluRay = PropertiesUtil.getBooleanProperty("mjb.excludeMultiPartBluRay", Boolean.FALSE);
        opensubtitles = PropertiesUtil.getProperty("opensubtitles.language", ""); // We want to check this isn't set for the exclusion
        hashpathdepth = PropertiesUtil.getIntProperty("mjb.scanner.hashpathdepth", 0);
        playFullBluRayDisk = PropertiesUtil.getBooleanProperty("mjb.playFullBluRayDisk", Boolean.FALSE);
        nmjCompliant = PropertiesUtil.getBooleanProperty("mjb.nmjCompliant", Boolean.FALSE);

        localBDRipScanner = new BDRipScanner();
    }

    /**
     * Scan the specified directory for video files.
     *
     * @param srcPath
     * @param library
     * @return a new library
     */
    public Library scan(MediaLibraryPath srcPath, Library library) {

        File directory = new FileTools.FileEx(srcPath.getPath(), archiveScanners);

        String mediaLibraryRoot;
        if (directory.isFile()) {
            mediaLibraryRoot = directory.getParentFile().getAbsolutePath();
        } else {
            mediaLibraryRoot = directory.getAbsolutePath();
        }
        // including path delimiter
        mediaLibraryRootPathIndex = FileTools.getDirPathWithSeparator(mediaLibraryRoot).length();

        this.scanDirectory(srcPath, directory, library);
        return library;
    }

    /**
     * Recursively scan the directory for video files
     *
     * @param srcPath
     * @param directory
     * @param collection
     */
    protected void scanDirectory(MediaLibraryPath srcPath, File directory, Library collection) {
        FileTools.fileCache.fileAdd(directory);
        if (directory.isFile()) {
            scanFile(srcPath, directory, collection);
        } else {

            // skip this directory if it is the nmj_database
            if (nmjCompliant && "nmj_database".equalsIgnoreCase(directory.getName())) {
                LOG.debug("Scanning of directory {} skipped due nmj database", directory.getAbsolutePath());
                return;
            }

            File[] files = directory.listFiles();

            if (files != null) {
                fileCount += files.length;
            }

            System.out.print("\r    Scanning directory #" + dirCount++ + ", " + fileCount + " files scanned");

            if (files != null && files.length > 0) {
                List<File> fileList = Arrays.asList(files);
                Collections.sort(fileList);

                // Prescan files list. Ignore directory if file with predefined name is found.
                // TODO May be read the file and exclude files by mask (similar to .cvsignore)
                for (File file : files) {
                    if (".mjbignore".equalsIgnoreCase(file.getName())) {
                        LOG.debug("Scanning of directory {} skipped due to override file",
                                directory.getAbsolutePath());
                        return;
                    }

                    if (nmjCompliant) {
                        // also check for .no_all.nmj and .no_video.nmj and the
                        if (".no_all.nmj".equalsIgnoreCase(file.getName())) {
                            LOG.debug("Scanning of directory {} skipped due to nmj all override file",
                                    directory.getAbsolutePath());
                            return;
                        }
                        if (".no_video.nmj".equalsIgnoreCase(file.getName())) {
                            LOG.debug("Scanning of directory {} skipped due to nmj video override file",
                                    directory.getAbsolutePath());
                            return;
                        }
                    }
                }

                // add all files to the global cache, after ignore check but before the actual scan
                FileTools.fileCache.addFiles(files);

                for (File file : fileList) {
                    if (!isFiltered(srcPath, file)) {
                        if (file.isDirectory() && "VIDEO_TS".equalsIgnoreCase(file.getName())) {
                            scanFile(srcPath, file.getParentFile(), collection);
                        } else if (file.isDirectory() && "BDMV".equalsIgnoreCase(file.getName())) {
                            scanFile(srcPath, file.getParentFile(), collection);
                        } else if (file.isDirectory()) {
                            scanDirectory(srcPath, file, collection);
                        } else {
                            scanFile(srcPath, file, collection);
                        }
                    }
                }
            }
        }
    }

    /**
     * Checks the file or directory passed to determine if it should be excluded
     * from the scan
     *
     * @param srcPath
     * @param file
     * @return boolean flag, true if the file should be excluded, false
     * otherwise
     */
    protected boolean isFiltered(MediaLibraryPath srcPath, File file) {
        boolean isDirectory = file.isDirectory();
        String filename = file.getName();

        // Skip these parts if the file is a directory
        if (!isDirectory) {
            int index = filename.lastIndexOf('.');
            if (index < 0) {
                return true;
            }

            String extension = file.getName().substring(index + 1).toUpperCase();
            if (!supportedExtensions.contains(extension)) {
                return true;
            }

            // Exclude files without external subtitles
            if (StringUtils.isBlank(opensubtitles)) {
                // We are not downloading subtitles, so exclude those that don't have any.
                if (excludeFilesWithoutExternalSubtitles && !hasSubtitles(file)) {
                    LOG.info("File {} excluded. (no external subtitles)", filename);
                    return true;
                }
            }
        }

        // Compute the relative filename
        String relativeFilename = file.getAbsolutePath().substring(mediaLibraryRootPathIndex);

        String relativeFileNameLower = relativeFilename.toLowerCase();
        String jukeboxName = PropertiesUtil.getProperty("mjb.detailsDirName", "Jukebox");

        for (String excluded : srcPath.getExcludes()) {
            if (excluded.length() > 0) {
                try {
                    Pattern excludePatt = Pattern.compile(excluded, Pattern.CASE_INSENSITIVE);
                    if (excludePatt.matcher(relativeFileNameLower).find()) {
                        LOG.debug("{} '{}' excluded.", isDirectory ? "Directory" : "File", relativeFilename);
                        return true;
                    }
                } catch (Exception error) {
                    LOG.info("Error processing exclusion pattern: {}, {}", excluded, error.getMessage());
                }

                excluded = excluded.replace("/", File.separator);
                excluded = excluded.replace("\\", File.separator);
                if (relativeFileNameLower.contains(excluded.toLowerCase())) {
                    // Don't print a message for the exclusion of Jukebox files
                    if (!relativeFileNameLower.contains(jukeboxName)) {
                        LOG.debug("{} '{}' excluded.", isDirectory ? "Directory" : "File", relativeFilename);
                    }
                    return true;
                }
            }
        }

        // Handle special case of RARs. If ".rar", and it is ".partXXX.rar"
        // exclude all NON-"part001.rar" files.
        Matcher m = PATTERN_RAR_PART.matcher(relativeFileNameLower);

        if (m.find() && (m.groupCount() == 1)) {
            if (Integer.parseInt(m.group(1)) != 1) {
                LOG.debug("Excluding file '{}' as it is a non-first part RAR archive ({})", relativeFilename,
                        m.group(1));
                return true;
            }
        }

        return false;
    }

    /**
     * Check to see if the file has subtitles or not
     *
     * @param fileToScan
     * @return
     */
    protected static boolean hasSubtitles(File fileToScan) {

        File found = FileTools.findSubtitles(fileToScan);
        return found.exists();
    }

    /**
     * Scan the file for the information to be added to the object
     *
     * @param srcPath
     * @param file
     * @param library
     */
    private void scanFile(MediaLibraryPath srcPath, File file, Library library) {
        File[] contentFiles;
        int bdDuration = 0;
        boolean isBluRay = false;

        contentFiles = new File[1];
        contentFiles[0] = file;

        if (file.isDirectory()) {
            // Scan BD Playlist files
            BDFilePropertiesMovie bdPropertiesMovie = localBDRipScanner.executeGetBDInfo(file);

            if (bdPropertiesMovie != null) {
                isBluRay = true;

                // Exclude multi part BluRay that include more than one file
                if (excludeMultiPartBluRay && bdPropertiesMovie.fileList.length > 1) {
                    LOG.info("File '{}' excluded. (multi part BluRay)", file.getName());
                    return;
                }

                bdDuration = bdPropertiesMovie.duration;

                contentFiles = bdPropertiesMovie.fileList;
            }
        }

        String hashstr = "";
        for (int i = 0; i < contentFiles.length; i++) {
            Movie movie = new Movie();

            // Hopefully this is a fix for issue #670 -- I can't duplicate it, since I don't have an BD rips
            if (contentFiles[i] == null) {
                continue;
            }

            // Compute the baseFilename: This is the filename without the extension
            String baseFileName = file.getName();

            if (!file.isDirectory()) {
                baseFileName = baseFileName.substring(0, file.getName().lastIndexOf("."));
                movie.setFormatType(Movie.TYPE_FILE);
            }

            // Compute the relative filename
            String relativeFilename = contentFiles[i].getAbsolutePath().substring(mediaLibraryRootPathIndex);

            MovieFile movieFile = new MovieFile();
            relativeFilename = relativeFilename.replace('\\', '/'); // make it unix!

            if (contentFiles[i].isDirectory()) {
                // For DVD images
                movieFile.setFilename(
                        srcPath.getPlayerRootPath() + HTMLTools.encodeUrlPath(relativeFilename) + "/VIDEO_TS");
                movie.setFormatType(Movie.TYPE_DVD);
            } else {
                if (isBluRay && playFullBluRayDisk) {
                    // A BluRay File and playFullBluRayDisk, so link to the directory and not the file
                    String tempFilename = srcPath.getPlayerRootPath() + HTMLTools.encodeUrlPath(relativeFilename);
                    tempFilename = tempFilename.substring(0, tempFilename.toUpperCase().lastIndexOf("BDMV"));
                    movieFile.setFilename(tempFilename);
                } else {
                    // Normal movie file so link to it
                    movieFile.setFilename(srcPath.getPlayerRootPath() + HTMLTools.encodeUrlPath(relativeFilename));
                    movie.setFormatType(Movie.TYPE_FILE);
                }
            }

            if (isBluRay) {
                // This is to overwrite the TYPE_FILE check above if the disk is bluray but not playFullBluRayDisk
                movie.setFormatType(Movie.TYPE_BLURAY);
            }

            // FIXME: part and file info are to be taken from filename scanner
            movieFile.setPart(i + 1);
            movieFile.setFile(contentFiles[i]);

            movie.setScrapeLibrary(srcPath.isScrapeLibrary());
            movie.addMovieFile(movieFile);
            movie.setFile(contentFiles[i]);
            movie.setContainerFile(file);
            movie.setBaseFilename(baseFileName);

            // Ensure that filename is unique. Prevent interference between files like "disk1.avi".
            // TODO: Actually it makes sense to use normalized movie name instead of first part name.
            if (hashpathdepth > 0) {
                int d, pos = relativeFilename.length();
                for (d = hashpathdepth + 1; d > 0 && pos > 0; d--) {
                    pos = relativeFilename.lastIndexOf("/", pos - 1);
                }
                hashstr = relativeFilename.substring(pos + 1);
                hashstr = Integer.toHexString(hashstr.hashCode());
            }
            movie.setBaseName(FileTools.makeSafeFilename(baseFileName) + hashstr);

            movie.setLibraryPath(srcPath.getPath());
            movie.setPosterFilename(movie.getBaseName() + ".jpg");
            movie.setThumbnailFilename(movie.getBaseName() + thumbnailToken + "." + thumbnailsFormat);
            movie.setDetailPosterFilename(movie.getBaseName() + posterToken + "." + postersFormat);
            movie.setBannerFilename(movie.getBaseName() + bannerToken + "." + bannersFormat);
            movie.setWideBannerFilename(movie.getBaseName() + wideBannerToken + "." + bannersFormat);
            movie.setSubtitles(hasSubtitles(movie.getFile()) ? "YES" : "NO");
            movie.setLibraryDescription(srcPath.getDescription());
            movie.setPrebuf(srcPath.getPrebuf());

            movie.addFileDate(new Date(file.lastModified()));

            // Issue 1241 - Take care of directory for size.
            movie.setFileSize(this.calculateFileSize(file));

            MovieFileNameDTO dto = MovieFilenameScanner.scan(file);

            movie.mergeFileNameDTO(dto);

            if (bdDuration == 0 || dto.getPart() > 0 || dto.isExtra()) {
                // Do not merge file information for Blu-Ray unless it's a multi-part disk or an extra
                movieFile.mergeFileNameDTO(dto);
            } else {
                if (playFullBluRayDisk && movie.isTVShow()) {
                    // This is needed for multi-part disks and TV shows
                    movieFile.mergeFileNameDTO(dto);
                }
                if (OverrideTools.checkOverwriteRuntime(movie, SOURCE_FILENAME)) {
                    // Set duration for BD disks using the data in the playlist + mark Bluray source and container
                    movie.setRuntime(DateTimeTools.formatDuration(bdDuration), SOURCE_FILENAME);
                }

                // Pretend that HDDVD is also BluRay
                if (!"HDDVD".equalsIgnoreCase(movie.getVideoSource())) {
                    movie.setFormatType(Movie.TYPE_BLURAY);
                    if (OverrideTools.checkOverwriteContainer(movie, SOURCE_FILENAME)) {
                        movie.setContainer("BluRay", SOURCE_FILENAME);
                    }
                    if (OverrideTools.checkOverwriteVideoSource(movie, SOURCE_FILENAME)) {
                        movie.setVideoSource("BluRay", SOURCE_FILENAME);
                    }
                }
            }

            // Issue 1679: Determine the file date of the movie by the VIDEO_TS or BDMV folders
            if (movie.isDVD()) {
                movie.setFileDate(new Date((new File(file, "/VIDEO_TS")).lastModified()));
            } else if (movie.isBluray()) {
                movie.setFileDate(new Date((new File(file, "/BDMV")).lastModified()));
            }

            library.addMovie(movie);

            // Stop after first file part if full BluRay Disk
            if (isBluRay && playFullBluRayDisk) {
                break;
            }
        }

    }

    /**
     * Return the file length, or if file is the directory, the sum of all files
     * in directory. Created for issue 1241
     *
     * @param file
     * @return
     */
    private long calculateFileSize(File file) {
        long total = 0;
        if (file.isDirectory()) {
            File[] listFiles = file.listFiles();
            // Check for empty directories
            if (listFiles != null && listFiles.length > 0) {
                for (File fileTmp : listFiles) {
                    total += calculateFileSize(fileTmp);
                }
            }
        } else {
            total += file.length();
        }
        return total;
    }
}