* improved handling of clutter files like samples/trailers/etc
* improved movie detection
This commit is contained in:
parent
0c9bc8a742
commit
c67b0d0d47
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue