diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index cbeb3255..0401755f 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -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 movieFiles = filter(files, VIDEO_FILES); - List nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo")); + // ignore sample files + List fileset = filter(files, NON_CLUTTER_FILES); - List orphanedFiles = new ArrayList(filter(files, FILES)); + // handle movie files + List movieFiles = filter(fileset, VIDEO_FILES); + List nfoFiles = filter(fileset, MediaTypes.getDefaultFilter("application/nfo")); + + List orphanedFiles = new ArrayList(filter(fileset, FILES)); orphanedFiles.removeAll(movieFiles); orphanedFiles.removeAll(nfoFiles); @@ -366,7 +368,7 @@ public class CmdlineOperations implements CmdlineInterface { List movieMatchFiles = new ArrayList(); 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) diff --git a/source/net/sourceforge/filebot/media/MediaDetection.java b/source/net/sourceforge/filebot/media/MediaDetection.java index 3e6bfc9e..39537f93 100644 --- a/source/net/sourceforge/filebot/media/MediaDetection.java +++ b/source/net/sourceforge/filebot/media/MediaDetection.java @@ -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 files = new ArrayList(); - files.add(getName(movieFile)); - files.add(getName(movieFile.getParentFile())); + List terms = new ArrayList(); - List 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 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 optionsByRelevance = new ArrayList(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 matchMovieName(final List files, final Locale locale, final boolean strict) throws Exception { // cross-reference file / folder name with movie list final HighPerformanceMatcher nameMatcher = new HighPerformanceMatcher(3); diff --git a/source/net/sourceforge/filebot/media/ReleaseInfo.java b/source/net/sourceforge/filebot/media/ReleaseInfo.java index 17fde5ab..9c658b7a 100644 --- a/source/net/sourceforge/filebot/media/ReleaseInfo.java +++ b/source/net/sourceforge/filebot/media/ReleaseInfo.java @@ -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 releaseGroupResource = new PatternResource(getBundle(getClass().getName()).getString("url.release-groups")); protected final CachedResource 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 quoteAll(Collection strings) { List patterns = new ArrayList(strings.size()); for (String it : strings) { diff --git a/source/net/sourceforge/filebot/media/ReleaseInfo.properties b/source/net/sourceforge/filebot/media/ReleaseInfo.properties index 6845c149..aab682b6 100644 --- a/source/net/sourceforge/filebot/media/ReleaseInfo.properties +++ b/source/net/sourceforge/filebot/media/ReleaseInfo.properties @@ -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: (?> match(final List files, final SortOrder sortOrder, final Locale locale, final boolean autodetect, final Component parent) throws Exception { - // handle movie files - List movieFiles = filter(files, VIDEO_FILES); - List nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo")); + // ignore sample files + List fileset = filter(files, NON_CLUTTER_FILES); - List orphanedFiles = new ArrayList(filter(files, FILES)); + // handle movie files + List movieFiles = filter(fileset, VIDEO_FILES); + List nfoFiles = filter(fileset, MediaTypes.getDefaultFilter("application/nfo")); + + List orphanedFiles = new ArrayList(filter(fileset, FILES)); orphanedFiles.removeAll(movieFiles); orphanedFiles.removeAll(nfoFiles); @@ -122,12 +126,16 @@ class MovieHashMatcher implements AutoCompleteMatcher { List movieMatchFiles = new ArrayList(); 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>> grabMovieJobs = new ArrayList>>(); + // remember user decisions and only bother user once + final Map selectionMemory = new TreeMap(CommonSequenceMatcher.getLenientCollator(Locale.ROOT)); + final Map inputMemory = new TreeMap(CommonSequenceMatcher.getLenientCollator(Locale.ROOT)); + // map all files by movie for (final File file : movieMatchFiles) { grabMovieJobs.add(new Callable>() { @@ -136,7 +144,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { public Entry 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 selectionMemory, Map inputMemory, Component parent, Movie... suggestions) throws Exception { Set options = new LinkedHashSet(); // 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 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 options, final Map 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 selectDialog = new SelectDialog(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(); } } diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index 129f438c..9278a5f8 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -326,6 +326,11 @@ public final class FileUtilities { } + public static FileFilter not(FileFilter filter) { + return new NotFileFilter(filter); + } + + public static List flatten(Iterable roots, int maxDepth, boolean listHiddenFiles) { List files = new ArrayList(); @@ -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. */