/*
* 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;
}
}