* improved handling of clutter files like samples/trailers/etc

* improved movie detection
This commit is contained in:
Reinhard Pointner 2012-06-15 10:45:35 +00:00
parent 0c9bc8a742
commit c67b0d0d47
6 changed files with 134 additions and 37 deletions

View File

@ -52,7 +52,6 @@ import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.hash.VerificationFileReader; import net.sourceforge.filebot.hash.VerificationFileReader;
import net.sourceforge.filebot.hash.VerificationFileWriter; import net.sourceforge.filebot.hash.VerificationFileWriter;
import net.sourceforge.filebot.media.MediaDetection; import net.sourceforge.filebot.media.MediaDetection;
import net.sourceforge.filebot.media.ReleaseInfo;
import net.sourceforge.filebot.similarity.EpisodeMatcher; import net.sourceforge.filebot.similarity.EpisodeMatcher;
import net.sourceforge.filebot.similarity.EpisodeMetrics; import net.sourceforge.filebot.similarity.EpisodeMetrics;
import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Match;
@ -298,11 +297,14 @@ public class CmdlineOperations implements CmdlineInterface {
throws Exception { throws Exception {
CLILogger.config(format("Rename movies using [%s]", service.getName())); CLILogger.config(format("Rename movies using [%s]", service.getName()));
// handle movie files // ignore sample files
List<File> movieFiles = filter(files, VIDEO_FILES); List<File> fileset = filter(files, NON_CLUTTER_FILES);
List<File> nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo"));
List<File> orphanedFiles = new ArrayList<File>(filter(files, FILES)); // handle movie files
List<File> movieFiles = filter(fileset, VIDEO_FILES);
List<File> nfoFiles = filter(fileset, MediaTypes.getDefaultFilter("application/nfo"));
List<File> orphanedFiles = new ArrayList<File>(filter(fileset, FILES));
orphanedFiles.removeAll(movieFiles); orphanedFiles.removeAll(movieFiles);
orphanedFiles.removeAll(nfoFiles); orphanedFiles.removeAll(nfoFiles);
@ -366,7 +368,7 @@ public class CmdlineOperations implements CmdlineInterface {
List<File> movieMatchFiles = new ArrayList<File>(); List<File> movieMatchFiles = new ArrayList<File>();
movieMatchFiles.addAll(movieFiles); movieMatchFiles.addAll(movieFiles);
movieMatchFiles.addAll(nfoFiles); movieMatchFiles.addAll(nfoFiles);
movieMatchFiles.addAll(filter(files, new ReleaseInfo().getDiskFolderFilter())); movieMatchFiles.addAll(filter(files, DISK_FOLDERS));
movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files
// map movies to (possibly multiple) files (in natural order) // map movies to (possibly multiple) files (in natural order)

View File

@ -8,6 +8,7 @@ import static net.sourceforge.filebot.similarity.Normalization.*;
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*;
import java.io.File; import java.io.File;
import java.io.FileFilter;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@ -53,9 +54,17 @@ public class MediaDetection {
private static final ReleaseInfo releaseInfo = new ReleaseInfo(); private static final ReleaseInfo releaseInfo = new ReleaseInfo();
public static final FileFilter DISK_FOLDERS = releaseInfo.getDiskFolderFilter();
public static final FileFilter NON_CLUTTER_FILES = not(releaseInfo.getClutterFileFilter());
public static boolean isDiskFolder(File folder) { public static boolean isDiskFolder(File folder) {
return releaseInfo.getDiskFolderFilter().accept(folder); return DISK_FOLDERS.accept(folder);
}
public static boolean isNonClutter(File file) {
return NON_CLUTTER_FILES.accept(file);
} }
@ -290,11 +299,18 @@ public class MediaDetection {
} }
// search by file name or folder name // search by file name or folder name
List<String> files = new ArrayList<String>(); List<String> terms = new ArrayList<String>();
files.add(getName(movieFile));
files.add(getName(movieFile.getParentFile()));
List<Movie> movieNameMatches = matchMovieName(files, locale, strict); // 1. term: file
terms.add(getName(movieFile));
// 2. term: first meaningful parent folder
File movieFolder = guessMovieFolder(movieFile);
if (movieFolder != null) {
terms.add(getName(movieFolder));
}
List<Movie> movieNameMatches = matchMovieName(terms, locale, strict);
// skip further queries if collected matches are already sufficient // skip further queries if collected matches are already sufficient
if (options.size() > 0 && movieNameMatches.size() > 0) { if (options.size() > 0 && movieNameMatches.size() > 0) {
@ -304,12 +320,12 @@ public class MediaDetection {
// if matching name+year failed, try matching only by name // if matching name+year failed, try matching only by name
if (movieNameMatches.isEmpty() && strict) { if (movieNameMatches.isEmpty() && strict) {
movieNameMatches = matchMovieName(files, locale, false); movieNameMatches = matchMovieName(terms, locale, false);
} }
// query by file / folder name // query by file / folder name
if (queryLookupService != null) { if (queryLookupService != null) {
options.addAll(queryMovieByFileName(files, queryLookupService, locale)); options.addAll(queryMovieByFileName(terms, queryLookupService, locale));
} }
// add local matching after online search // add local matching after online search
@ -317,11 +333,23 @@ public class MediaDetection {
// sort by relevance // sort by relevance
List<Movie> optionsByRelevance = new ArrayList<Movie>(options); List<Movie> optionsByRelevance = new ArrayList<Movie>(options);
sort(optionsByRelevance, new SimilarityComparator(stripReleaseInfo(getName(movieFile)), stripReleaseInfo(getName(movieFile.getParentFile())))); sort(optionsByRelevance, new SimilarityComparator(new NameSimilarityMetric(), stripReleaseInfo(terms, true).toArray()));
return optionsByRelevance; return optionsByRelevance;
} }
public static File guessMovieFolder(File movieFile) throws IOException {
// first meaningful parent folder
for (File f = movieFile.getParentFile(); f != null; f = f.getParentFile()) {
String term = stripReleaseInfo(f.getName());
if (term.length() > 0) {
return f;
}
}
return null;
}
private static List<Movie> matchMovieName(final List<String> files, final Locale locale, final boolean strict) throws Exception { private static List<Movie> matchMovieName(final List<String> files, final Locale locale, final boolean strict) throws Exception {
// cross-reference file / folder name with movie list // cross-reference file / folder name with movie list
final HighPerformanceMatcher nameMatcher = new HighPerformanceMatcher(3); final HighPerformanceMatcher nameMatcher = new HighPerformanceMatcher(3);

View File

@ -200,6 +200,11 @@ public class ReleaseInfo {
} }
public FileFilter getClutterFileFilter() {
return new FileFolderNameFilter(compile(getBundle(getClass().getName()).getString("pattern.file.ignore")));
}
// fetch release group names online and try to update the data every other day // fetch release group names online and try to update the data every other day
protected final CachedResource<String[]> releaseGroupResource = new PatternResource(getBundle(getClass().getName()).getString("url.release-groups")); protected final CachedResource<String[]> releaseGroupResource = new PatternResource(getBundle(getClass().getName()).getString("url.release-groups"));
protected final CachedResource<String[]> queryBlacklistResource = new PatternResource(getBundle(getClass().getName()).getString("url.query-blacklist")); protected final CachedResource<String[]> queryBlacklistResource = new PatternResource(getBundle(getClass().getName()).getString("url.query-blacklist"));
@ -283,6 +288,23 @@ public class ReleaseInfo {
} }
public static class FileFolderNameFilter implements FileFilter {
private final Pattern namePattern;
public FileFolderNameFilter(Pattern namePattern) {
this.namePattern = namePattern;
}
@Override
public boolean accept(File file) {
return (namePattern.matcher(file.getName()).find() || (file.isFile() && namePattern.matcher(file.getParentFile().getName()).find()));
}
}
private Collection<String> quoteAll(Collection<String> strings) { private Collection<String> quoteAll(Collection<String> strings) {
List<String> patterns = new ArrayList<String>(strings.size()); List<String> patterns = new ArrayList<String>(strings.size());
for (String it : strings) { for (String it : strings) {

View File

@ -16,3 +16,4 @@ url.series-list: http://filebot.sourceforge.net/data/series.list.gz
# disk folder matcher # disk folder matcher
pattern.diskfolder.entry: ^BDMV$|^HVDVD_TS$|^VIDEO_TS$|^AUDIO_TS$|^VCD$ pattern.diskfolder.entry: ^BDMV$|^HVDVD_TS$|^VIDEO_TS$|^AUDIO_TS$|^VCD$
pattern.file.ignore: (?<!\\p{Alnum})(?i:sample|trailer|extras|deleted.scenes)(?!\\p{Alnum})

View File

@ -25,6 +25,7 @@ import java.util.Map.Entry;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -40,7 +41,7 @@ import javax.swing.SwingUtilities;
import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.Analytics;
import net.sourceforge.filebot.MediaTypes; import net.sourceforge.filebot.MediaTypes;
import net.sourceforge.filebot.media.ReleaseInfo; import net.sourceforge.filebot.similarity.CommonSequenceMatcher;
import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.similarity.NameSimilarityMetric; import net.sourceforge.filebot.similarity.NameSimilarityMetric;
import net.sourceforge.filebot.similarity.SimilarityMetric; import net.sourceforge.filebot.similarity.SimilarityMetric;
@ -64,11 +65,14 @@ class MovieHashMatcher implements AutoCompleteMatcher {
@Override @Override
public List<Match<File, ?>> match(final List<File> files, final SortOrder sortOrder, final Locale locale, final boolean autodetect, final Component parent) throws Exception { public List<Match<File, ?>> match(final List<File> files, final SortOrder sortOrder, final Locale locale, final boolean autodetect, final Component parent) throws Exception {
// handle movie files // ignore sample files
List<File> movieFiles = filter(files, VIDEO_FILES); List<File> fileset = filter(files, NON_CLUTTER_FILES);
List<File> nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo"));
List<File> orphanedFiles = new ArrayList<File>(filter(files, FILES)); // handle movie files
List<File> movieFiles = filter(fileset, VIDEO_FILES);
List<File> nfoFiles = filter(fileset, MediaTypes.getDefaultFilter("application/nfo"));
List<File> orphanedFiles = new ArrayList<File>(filter(fileset, FILES));
orphanedFiles.removeAll(movieFiles); orphanedFiles.removeAll(movieFiles);
orphanedFiles.removeAll(nfoFiles); orphanedFiles.removeAll(nfoFiles);
@ -122,12 +126,16 @@ class MovieHashMatcher implements AutoCompleteMatcher {
List<File> movieMatchFiles = new ArrayList<File>(); List<File> movieMatchFiles = new ArrayList<File>();
movieMatchFiles.addAll(movieFiles); movieMatchFiles.addAll(movieFiles);
movieMatchFiles.addAll(nfoFiles); movieMatchFiles.addAll(nfoFiles);
movieMatchFiles.addAll(filter(files, new ReleaseInfo().getDiskFolderFilter())); movieMatchFiles.addAll(filter(files, DISK_FOLDERS));
movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files
// match remaining movies file by file in parallel // match remaining movies file by file in parallel
List<Callable<Entry<File, Movie>>> grabMovieJobs = new ArrayList<Callable<Entry<File, Movie>>>(); List<Callable<Entry<File, Movie>>> grabMovieJobs = new ArrayList<Callable<Entry<File, Movie>>>();
// remember user decisions and only bother user once
final Map<String, Movie> selectionMemory = new TreeMap<String, Movie>(CommonSequenceMatcher.getLenientCollator(Locale.ROOT));
final Map<String, String> inputMemory = new TreeMap<String, String>(CommonSequenceMatcher.getLenientCollator(Locale.ROOT));
// map all files by movie // map all files by movie
for (final File file : movieMatchFiles) { for (final File file : movieMatchFiles) {
grabMovieJobs.add(new Callable<Entry<File, Movie>>() { grabMovieJobs.add(new Callable<Entry<File, Movie>>() {
@ -136,7 +144,7 @@ class MovieHashMatcher implements AutoCompleteMatcher {
public Entry<File, Movie> call() throws Exception { public Entry<File, Movie> call() throws Exception {
// unknown hash, try via imdb id from nfo file // unknown hash, try via imdb id from nfo file
if (!movieByFile.containsKey(file) || !autodetect) { if (!movieByFile.containsKey(file) || !autodetect) {
Movie result = grabMovieName(file, locale, autodetect, parent, movieByFile.get(file)); Movie result = grabMovieName(file, locale, autodetect, selectionMemory, inputMemory, parent, movieByFile.get(file));
if (result != null) { if (result != null) {
Analytics.trackEvent(service.getName(), "SearchMovie", result.toString(), 1); Analytics.trackEvent(service.getName(), "SearchMovie", result.toString(), 1);
} }
@ -209,7 +217,7 @@ class MovieHashMatcher implements AutoCompleteMatcher {
} }
protected Movie grabMovieName(File movieFile, Locale locale, boolean autodetect, Component parent, Movie... suggestions) throws Exception { protected Movie grabMovieName(File movieFile, Locale locale, boolean autodetect, Map<String, Movie> selectionMemory, Map<String, String> inputMemory, Component parent, Movie... suggestions) throws Exception {
Set<Movie> options = new LinkedHashSet<Movie>(); Set<Movie> options = new LinkedHashSet<Movie>();
// add default value if any // add default value if any
@ -227,8 +235,12 @@ class MovieHashMatcher implements AutoCompleteMatcher {
String suggestion = options.isEmpty() ? stripReleaseInfo(getName(movieFile)) : options.iterator().next().getName(); String suggestion = options.isEmpty() ? stripReleaseInfo(getName(movieFile)) : options.iterator().next().getName();
String input = null; String input = null;
synchronized (this) { synchronized (inputMemory) {
input = showInputDialog("Enter movie name:", suggestion, String.format("%s / %s", movieFile.getParentFile().getName(), movieFile.getName()), parent); input = inputMemory.get(suggestion);
if (input == null || suggestion == null || suggestion.isEmpty()) {
input = showInputDialog("Enter movie name:", suggestion, String.format("%s/%s", movieFile.getParentFile().getName(), movieFile.getName()), parent);
inputMemory.put(suggestion, input);
}
} }
// we only care about results from manual input from here on out // we only care about results from manual input from here on out
@ -239,17 +251,20 @@ class MovieHashMatcher implements AutoCompleteMatcher {
} }
} }
return options.isEmpty() ? null : selectMovie(movieFile, options, parent); return options.isEmpty() ? null : selectMovie(movieFile, options, selectionMemory, parent);
} }
protected Movie selectMovie(final File movieFile, final Collection<Movie> options, final Component parent) throws Exception { protected Movie selectMovie(final File movieFile, final Collection<Movie> options, final Map<String, Movie> selectionMemory, final Component parent) throws Exception {
// clean file / folder names // 1. movie by filename
final String fileQuery = stripReleaseInfo(getName(movieFile)).toLowerCase(); final String fileQuery = stripReleaseInfo(getName(movieFile));
final String folderQuery = stripReleaseInfo(getName(movieFile.getParentFile())).toLowerCase();
// 2. movie by directory
final File movieFolder = guessMovieFolder(movieFile);
final String folderQuery = (movieFolder == null) ? "" : stripReleaseInfo(movieFolder.getName());
// auto-ignore invalid files // auto-ignore invalid files
if (fileQuery.length() < 2) { if (fileQuery.length() < 2 && folderQuery.length() < 2) {
return null; return null;
} }
@ -260,7 +275,7 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// auto-select perfect match // auto-select perfect match
for (Movie movie : options) { for (Movie movie : options) {
String movieIdentifier = normalizePunctuation(movie.toString()).toLowerCase(); String movieIdentifier = normalizePunctuation(movie.toString()).toLowerCase();
if (fileQuery.startsWith(movieIdentifier) || folderQuery.startsWith(movieIdentifier)) { if (fileQuery.toLowerCase().startsWith(movieIdentifier) || folderQuery.toLowerCase().startsWith(movieIdentifier)) {
return movie; return movie;
} }
} }
@ -290,8 +305,8 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// multiple results have been found, user must select one // multiple results have been found, user must select one
SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(parent, options); SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(parent, options);
selectDialog.setTitle(String.format("%s / %s", movieFile.getParentFile().getName(), movieFile.getName())); selectDialog.setTitle(String.format("%s / %s", folderQuery, fileQuery));
selectDialog.getHeaderLabel().setText(String.format("Movies matching '%s':", fileQuery)); selectDialog.getHeaderLabel().setText(String.format("Movies matching '%s':", fileQuery.length() >= 2 || folderQuery.length() <= 2 ? fileQuery : folderQuery));
selectDialog.getCancelAction().putValue(Action.NAME, "Ignore"); selectDialog.getCancelAction().putValue(Action.NAME, "Ignore");
selectDialog.pack(); selectDialog.pack();
@ -306,10 +321,17 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// allow only one select dialog at a time // allow only one select dialog at a time
synchronized (this) { synchronized (this) {
SwingUtilities.invokeAndWait(showSelectDialog); synchronized (selectionMemory) {
} if (selectionMemory.containsKey(fileQuery)) {
return selectionMemory.get(fileQuery);
}
// selected value or null SwingUtilities.invokeAndWait(showSelectDialog);
return showSelectDialog.get();
// cache selected value
selectionMemory.put(fileQuery, showSelectDialog.get());
return showSelectDialog.get();
}
}
} }
} }

View File

@ -326,6 +326,11 @@ public final class FileUtilities {
} }
public static FileFilter not(FileFilter filter) {
return new NotFileFilter(filter);
}
public static List<File> flatten(Iterable<File> roots, int maxDepth, boolean listHiddenFiles) { public static List<File> flatten(Iterable<File> roots, int maxDepth, boolean listHiddenFiles) {
List<File> files = new ArrayList<File>(); List<File> files = new ArrayList<File>();
@ -620,6 +625,23 @@ public final class FileUtilities {
} }
public static class NotFileFilter implements FileFilter {
public FileFilter filter;
public NotFileFilter(FileFilter filter) {
this.filter = filter;
}
@Override
public boolean accept(File file) {
return !filter.accept(file);
}
}
/** /**
* Dummy constructor to prevent instantiation. * Dummy constructor to prevent instantiation.
*/ */