* 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.VerificationFileWriter;
import net.sourceforge.filebot.media.MediaDetection;
import net.sourceforge.filebot.media.ReleaseInfo;
import net.sourceforge.filebot.similarity.EpisodeMatcher;
import net.sourceforge.filebot.similarity.EpisodeMetrics;
import net.sourceforge.filebot.similarity.Match;
@ -298,11 +297,14 @@ public class CmdlineOperations implements CmdlineInterface {
throws Exception {
CLILogger.config(format("Rename movies using [%s]", service.getName()));
// handle movie files
List<File> movieFiles = filter(files, VIDEO_FILES);
List<File> nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo"));
// ignore sample files
List<File> fileset = filter(files, NON_CLUTTER_FILES);
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(nfoFiles);
@ -366,7 +368,7 @@ public class CmdlineOperations implements CmdlineInterface {
List<File> movieMatchFiles = new ArrayList<File>();
movieMatchFiles.addAll(movieFiles);
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
// 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 java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
@ -53,9 +54,17 @@ public class MediaDetection {
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) {
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
List<String> files = new ArrayList<String>();
files.add(getName(movieFile));
files.add(getName(movieFile.getParentFile()));
List<String> terms = new ArrayList<String>();
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
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 (movieNameMatches.isEmpty() && strict) {
movieNameMatches = matchMovieName(files, locale, false);
movieNameMatches = matchMovieName(terms, locale, false);
}
// query by file / folder name
if (queryLookupService != null) {
options.addAll(queryMovieByFileName(files, queryLookupService, locale));
options.addAll(queryMovieByFileName(terms, queryLookupService, locale));
}
// add local matching after online search
@ -317,11 +333,23 @@ public class MediaDetection {
// sort by relevance
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;
}
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 {
// cross-reference file / folder name with movie list
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
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"));
@ -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) {
List<String> patterns = new ArrayList<String>(strings.size());
for (String it : strings) {

View File

@ -16,3 +16,4 @@ url.series-list: http://filebot.sourceforge.net/data/series.list.gz
# disk folder matcher
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.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
@ -40,7 +41,7 @@ import javax.swing.SwingUtilities;
import net.sourceforge.filebot.Analytics;
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.NameSimilarityMetric;
import net.sourceforge.filebot.similarity.SimilarityMetric;
@ -64,11 +65,14 @@ class MovieHashMatcher implements AutoCompleteMatcher {
@Override
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
List<File> movieFiles = filter(files, VIDEO_FILES);
List<File> nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo"));
// ignore sample files
List<File> fileset = filter(files, NON_CLUTTER_FILES);
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(nfoFiles);
@ -122,12 +126,16 @@ class MovieHashMatcher implements AutoCompleteMatcher {
List<File> movieMatchFiles = new ArrayList<File>();
movieMatchFiles.addAll(movieFiles);
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
// match remaining movies file by file in parallel
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
for (final File file : movieMatchFiles) {
grabMovieJobs.add(new Callable<Entry<File, Movie>>() {
@ -136,7 +144,7 @@ class MovieHashMatcher implements AutoCompleteMatcher {
public Entry<File, Movie> call() throws Exception {
// unknown hash, try via imdb id from nfo file
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) {
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>();
// 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 input = null;
synchronized (this) {
input = showInputDialog("Enter movie name:", suggestion, String.format("%s / %s", movieFile.getParentFile().getName(), movieFile.getName()), parent);
synchronized (inputMemory) {
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
@ -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 {
// clean file / folder names
final String fileQuery = stripReleaseInfo(getName(movieFile)).toLowerCase();
final String folderQuery = stripReleaseInfo(getName(movieFile.getParentFile())).toLowerCase();
protected Movie selectMovie(final File movieFile, final Collection<Movie> options, final Map<String, Movie> selectionMemory, final Component parent) throws Exception {
// 1. movie by filename
final String fileQuery = stripReleaseInfo(getName(movieFile));
// 2. movie by directory
final File movieFolder = guessMovieFolder(movieFile);
final String folderQuery = (movieFolder == null) ? "" : stripReleaseInfo(movieFolder.getName());
// auto-ignore invalid files
if (fileQuery.length() < 2) {
if (fileQuery.length() < 2 && folderQuery.length() < 2) {
return null;
}
@ -260,7 +275,7 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// auto-select perfect match
for (Movie movie : options) {
String movieIdentifier = normalizePunctuation(movie.toString()).toLowerCase();
if (fileQuery.startsWith(movieIdentifier) || folderQuery.startsWith(movieIdentifier)) {
if (fileQuery.toLowerCase().startsWith(movieIdentifier) || folderQuery.toLowerCase().startsWith(movieIdentifier)) {
return movie;
}
}
@ -290,8 +305,8 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// multiple results have been found, user must select one
SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(parent, options);
selectDialog.setTitle(String.format("%s / %s", movieFile.getParentFile().getName(), movieFile.getName()));
selectDialog.getHeaderLabel().setText(String.format("Movies matching '%s':", fileQuery));
selectDialog.setTitle(String.format("%s / %s", folderQuery, fileQuery));
selectDialog.getHeaderLabel().setText(String.format("Movies matching '%s':", fileQuery.length() >= 2 || folderQuery.length() <= 2 ? fileQuery : folderQuery));
selectDialog.getCancelAction().putValue(Action.NAME, "Ignore");
selectDialog.pack();
@ -306,10 +321,17 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// allow only one select dialog at a time
synchronized (this) {
SwingUtilities.invokeAndWait(showSelectDialog);
synchronized (selectionMemory) {
if (selectionMemory.containsKey(fileQuery)) {
return selectionMemory.get(fileQuery);
}
SwingUtilities.invokeAndWait(showSelectDialog);
// cache selected value
selectionMemory.put(fileQuery, showSelectDialog.get());
return showSelectDialog.get();
}
}
// selected value or null
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) {
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.
*/