com.flicklib.folderscanner.AdvancedFolderScanner.java Source code

Java tutorial

Introduction

Here is the source code for com.flicklib.folderscanner.AdvancedFolderScanner.java

Source

/*
 * This file is part of Flicklib.
 *
 * Copyright (C) Francis De Brabandere
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.flicklib.folderscanner;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.flicklib.tools.LevenshteinDistance;

/**
 * Scans a folder for movies
 * 
 * @author francisdb
 */
@Singleton
public class AdvancedFolderScanner implements Scanner {

    private static final Logger LOGGER = LoggerFactory.getLogger(AdvancedFolderScanner.class);

    /**
     * If a folder contains no other than these it is a movie folder
     * TODO make regex?
     */
    private static final String[] MOVIE_SUB_DIRS = new String[] { "subs", "subtitles", "cd1", "cd2", "cd3", "cd4",
            "sample", "covers", "cover", "approved", "info" };

    private final MovieNameExtractor movieNameExtractor;

    @Inject
    public AdvancedFolderScanner(final MovieNameExtractor movieNameExtractor) {
        this.movieNameExtractor = movieNameExtractor;
    }

    public AdvancedFolderScanner() {
        this.movieNameExtractor = new MovieNameExtractor();
    }

    private List<FileGroup> movies;
    private String currentLabel;

    /**
     * Scans the folders
     * 
     * @param folders
     * @return a List of MovieInfo
     *
     * TODO get rid of the synchronized and create a factory or pass all state data
     */
    @Override
    public synchronized List<FileGroup> scan(final Set<FileObject> folders, AsyncMonitor monitor) {
        movies = new ArrayList<FileGroup>();

        if (monitor != null) {
            monitor.start();
        }
        for (FileObject folder : folders) {
            try {
                URL url = folder.getURL();
                if (folder.exists()) {
                    currentLabel = folder.getName().getBaseName();
                    LOGGER.info("scanning " + url);
                    try {
                        browse(folder, monitor);
                    } catch (InterruptedException ie) {
                        LOGGER.info("task is cancelled!" + ie.getMessage());
                        return null;
                    }
                } else {
                    LOGGER.warn("folder " + folder.getURL() + " does not exist!");
                }
            } catch (FileSystemException e) {
                LOGGER.error("error during checking  " + folder + ", " + e.getMessage(), e);
            }
        }
        if (monitor != null) {
            monitor.finish();
        }
        return movies;
    }

    /**
     * 
     * @param folder
     * @param monitor 
     * @return true, if it contained movie file
     * @throws InterruptedException 
     * @throws FileSystemException 
     */
    private boolean browse(FileObject folder, AsyncMonitor monitor)
            throws InterruptedException, FileSystemException {
        URL url = folder.getURL();
        LOGGER.trace("entering " + url);
        FileObject[] files = folder.getChildren();
        if (monitor != null) {
            if (monitor.isCanceled()) {
                throw new InterruptedException("at " + url);
            }
            monitor.step("scanning " + url);
        }

        Set<String> plainFileNames = new HashSet<String>();
        int subDirectories = 0;
        int compressedFiles = 0;
        Set<String> directoryNames = new HashSet<String>();
        for (FileObject f : files) {
            if (isDirectory(f)) {
                subDirectories++;
                directoryNames.add(f.getName().getBaseName().toLowerCase());
            } else {
                String ext = getExtension(f);
                if (ext == null) {
                    LOGGER.trace("Ignoring file without extension: " + f.getURL());
                } else {
                    if (MovieFileType.getTypeByExtension(ext) == MovieFileType.COMPRESSED) {
                        compressedFiles++;
                    }
                    if (ext != null && MovieFileFilter.VIDEO_EXTENSIONS.contains(ext)) {
                        plainFileNames.add(getNameWithoutExt(f));
                    }
                }
            }
        }
        // check for multiple compressed files, the case of:
        // Title_of_the_film/abc.rar
        // Title_of_the_film/abc.r01
        // Title_of_the_film/abc.r02
        if (compressedFiles > 0) {
            FileGroup fg = initStorableMovie(folder);
            fg.getLocations().add(new FileLocation(currentLabel, folder.getURL()));
            addCompressedFiles(fg, files);
            add(fg);
            return true;
        }
        if (subDirectories >= 2 && subDirectories <= 5) {
            // the case of :
            // Title_of_the_film/cd1/...
            // Title_of_the_film/cd2/...
            // with an optional sample/subs directory
            // Title_of_the_film/sample/
            // Title_of_the_film/subs/
            // Title_of_the_film/subtitles/
            // or
            // Title_of_the_film/bla1.avi
            // Title_of_the_film/bla2.avi
            // Title_of_the_film/sample/
            // Title_of_the_film/subs/
            if (isMovieFolder(directoryNames)) {
                FileGroup fg = initStorableMovie(folder);
                fg.getLocations().add(new FileLocation(currentLabel, folder.getURL()));
                for (String cdFolder : getCdFolders(directoryNames)) {
                    addCompressedFiles(fg, files, cdFolder);
                }
                for (FileObject file : folder.getChildren()) {
                    if (!isDirectory(file)) {
                        String ext = getExtension(file);
                        if (MovieFileFilter.VIDEO_EXT_EXTENSIONS.contains(ext)) {
                            fg.getFiles().add(createFileMeta(file, MovieFileType.getTypeByExtension(ext)));
                        }
                    }
                }
                add(fg);
                return true;
            }
        }
        boolean subFolderContainMovie = false;
        for (FileObject f : files) {
            final String baseName = f.getName().getBaseName();
            if (isDirectory(f) && !baseName.equalsIgnoreCase("sample") && !baseName.startsWith(".")) {
                subFolderContainMovie |= browse(f, monitor);
            }
        }

        // We want to handle the following cases:
        // 1,
        // Title_of_the_film/abc.avi
        // Title_of_the_film/abc.srt
        // --> no subdirectory, one film -> the title should be name of the
        // directory
        //  
        // 2,
        // Title_of_the_film/abc-cd1.avi
        // Title_of_the_film/abc-cd1.srt
        // Title_of_the_film/abc-cd2.srt
        // Title_of_the_film/abc-cd2.srt
        //

        if (subDirectories > 0 && subFolderContainMovie) {
            return genericMovieFindProcess(files) || subFolderContainMovie;
        } else {

            int foundFiles = plainFileNames.size();
            switch (foundFiles) {
            case 0:
                return subFolderContainMovie;
            case 1: {
                FileGroup fg = initStorableMovie(folder);
                fg.getLocations().add(new FileLocation(currentLabel, folder.getURL()));
                addFiles(fg, files, plainFileNames.iterator().next());
                add(fg);
                return true;
            }
            case 2: {
                Iterator<String> it = plainFileNames.iterator();
                String name1 = it.next();
                String name2 = it.next();
                if (LevenshteinDistance.distance(name1, name2) < 3) {
                    // the difference is -cd1 / -cd2
                    FileGroup fg = initStorableMovie(folder);

                    fg.getLocations().add(new FileLocation(currentLabel, folder.getURL()));
                    addFiles(fg, files, name1);
                    add(fg);
                    return true;
                }
                // the difference is significant, we use the generic
                // solution
            }
            default: {
                return genericMovieFindProcess(files);
            }
            }
        }
    }

    private boolean isDirectory(FileObject f) throws FileSystemException {
        return f.getType().hasChildren();
    }

    /**
     * check that every sub folder name is some common movie related folder name, eg 'cd1', 'cd2', 'sample', etc...
     * 
     * @param subDirectoryNames
     * @return
     */
    private boolean isMovieFolder(Set<String> subDirectoryNames) {
        boolean movieFolder = true;
        List<String> valid = Arrays.asList(MOVIE_SUB_DIRS);
        Iterator<String> iter = subDirectoryNames.iterator();
        String next;
        while (iter.hasNext() && movieFolder) {
            next = iter.next();
            movieFolder = valid.contains(next);
            if (!movieFolder) {
                LOGGER.trace("not movie folder because: " + next);
            }
        }
        return movieFolder;
    }

    /**
     * Return a set of directory names, which starts with 'cd','disk' or 'part'.
     * 
     * @param subDirectoryNames
     * @return
     */
    private Set<String> getCdFolders(Set<String> subDirectoryNames) {
        Set<String> valid = new HashSet<String>();
        for (String folder : subDirectoryNames) {
            if (folder.startsWith("cd") || folder.startsWith("disk") || folder.startsWith("part")) {
                valid.add(folder);
            }
        }
        return valid;
    }

    /**
     * add the compressed files to the file group, which are in the specified directory.
     * @param sm
     * @param fg
     * @param fileList
     * @param folderName
     * @throws FileSystemException 
     */
    private void addCompressedFiles(FileGroup fg, FileObject[] fileList, String folderName)
            throws FileSystemException {
        for (FileObject f : fileList) {
            if (f.getType().hasChildren() && folderName.equals(f.getName().getBaseName().toLowerCase())) {
                addCompressedFiles(fg, f.getChildren());
            }
        }
    }

    /**
     * initialize a FileGroup 
     * @param folder
     * @param sm
     * @return
     * @throws FileSystemException 
     */
    private FileGroup initStorableMovie(FileObject folder) throws FileSystemException {
        FileGroup fg = new FileGroup();
        fg.setAudioLanguage(movieNameExtractor.getLanguageSuggestion(folder));
        fg.setTitle(movieNameExtractor.removeCrap(folder));
        return fg;
    }

    /**
     * Handle the one directory with several different movies case.
     * @param files
     * @return true, if a movie found
     * @throws FileSystemException 
     */
    private boolean genericMovieFindProcess(FileObject[] files) throws FileSystemException {
        Map<String, FileGroup> foundMovies = new HashMap<String, FileGroup>();
        for (FileObject f : files) {
            if (!f.getType().hasChildren()) {
                String extension = getExtension(f);
                if (MovieFileFilter.VIDEO_EXT_EXTENSIONS.contains(extension)) {
                    String baseName = movieNameExtractor.removeCrap(f);
                    FileGroup m = foundMovies.get(baseName);
                    if (m == null) {
                        m = new FileGroup();
                        m.setTitle(baseName);
                        m.setAudioLanguage(movieNameExtractor.getLanguageSuggestion(f));
                        m.getLocations().add(new FileLocation(currentLabel, f.getParent().getURL()));
                        foundMovies.put(baseName, m);
                    }
                    m.getFiles().add(createFileMeta(f, MovieFileType.getTypeByExtension(extension)));
                }
            }
        }
        boolean foundMovie = false;
        for (FileGroup m : foundMovies.values()) {
            if (m.isValid()) {
                add(m);
                foundMovie = true;
            }
        }
        return foundMovie;
    }

    /**
     * add the files, which has similar names, to the movie object
     * 
     * @param sm
     * @param files
     * @param next
     * @throws FileSystemException 
     */
    private void addFiles(FileGroup fg, FileObject[] files, String plainFileName) throws FileSystemException {
        for (FileObject f : files) {
            if (!f.getType().hasChildren()) {
                String baseName = getNameWithoutExt(f);
                String ext = getExtension(f);
                if (MovieFileFilter.VIDEO_EXT_EXTENSIONS.contains(ext)) {
                    if (LevenshteinDistance.distance(plainFileName, baseName) <= 3) {
                        fg.getFiles().add(createFileMeta(f, MovieFileType.getTypeByExtension(ext)));
                    }
                }
            }
        }
    }

    private void addCompressedFiles(FileGroup fg, FileObject[] files) throws FileSystemException {
        for (FileObject f : files) {
            if (!f.getType().hasChildren()) {
                String ext = getExtension(f);
                if (ext == null) {
                    LOGGER.trace("Ignoring file without extension: " + f.getURL());
                } else {
                    MovieFileType type = MovieFileType.getTypeByExtension(ext);
                    if (type == MovieFileType.COMPRESSED || type == MovieFileType.NFO
                            || type == MovieFileType.SUBTITLE) {
                        fg.getFiles().add(createFileMeta(f, type));
                    }
                }
            }
        }
    }

    protected FileMeta createFileMeta(FileObject f, MovieFileType type) throws FileSystemException {
        return new FileMeta(f.getName().getBaseName(), type, f.getContent().getSize());
    }

    private void add(FileGroup movie) {
        LOGGER.info(
                "film:" + movie.getTitle() + " found at: " + movie.getLocations() + " {" + movie.getFiles() + '}');
        movie.guessReleaseType();
        movies.add(movie);
    }

    private String getExtension(FileObject file) {
        return getExtension(file.getName().getBaseName());
    }

    private String getExtension(String fileName) {
        int lastDotPos = fileName.lastIndexOf('.');
        if (lastDotPos != -1 && lastDotPos != 0 && lastDotPos < fileName.length() - 1) {
            return fileName.substring(lastDotPos + 1).toLowerCase();
        }
        return null;
    }

    private String getNameWithoutExt(FileObject file) {
        String name = file.getName().getBaseName();
        int lastDotPos = name.lastIndexOf('.');
        if (lastDotPos != -1 && lastDotPos != 0 && lastDotPos < name.length() - 1) {
            return name.substring(0, lastDotPos);
        }
        return name;
    }

}