From 097e00111133083fcd4c1d20afe9ec203c451f8a Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Wed, 13 Aug 2014 16:02:35 +0000 Subject: [PATCH] + support for adjustable match mode Opportunistic (default, like always) and new Strict mode (which is very restrictive, but will most likely get it right, if it gets anything at all) --- source/net/filebot/media/MediaDetection.java | 15 +++- .../ui/rename/AutoCompleteMatcher.java | 7 +- .../filebot/ui/rename/EpisodeListMatcher.java | 70 ++++++++++++------- .../filebot/ui/rename/MovieHashMatcher.java | 51 +++++++++----- source/net/filebot/ui/rename/RenamePanel.java | 41 +++++++---- website/data/query-blacklist.txt | 1 + 6 files changed, 123 insertions(+), 62 deletions(-) diff --git a/source/net/filebot/media/MediaDetection.java b/source/net/filebot/media/MediaDetection.java index 5b379943..295e9d31 100644 --- a/source/net/filebot/media/MediaDetection.java +++ b/source/net/filebot/media/MediaDetection.java @@ -620,7 +620,7 @@ public class MediaDetection { return sortBySimilarity(options, terms, getMovieMatchMetric(), true); } - // if matching name+year failed, try matching only by name + // if matching name+year failed, try matching only by name (in non-strict mode we would have checked these cases already by now) if (movieNameMatches.isEmpty() && strict) { movieNameMatches = matchMovieName(terms, false, 0); if (movieNameMatches.isEmpty()) { @@ -736,6 +736,19 @@ public class MediaDetection { return result; } + public static List parseMovieYear(String name) { + List years = new ArrayList(); + for (String it : name.split("\\D+")) { + if (it.length() == 4) { + int year = Integer.parseInt(it); + if (1920 < year && year < 2050) { + years.add(year); + } + } + } + return years; + } + public static String reduceMovieName(String name, boolean strict) throws IOException { Matcher matcher = compile(strict ? "^(.+)[\\[\\(]((?:19|20)\\d{2})[\\]\\)]" : "^(.+?)((?:19|20)\\d{2})").matcher(name); if (matcher.find()) { diff --git a/source/net/filebot/ui/rename/AutoCompleteMatcher.java b/source/net/filebot/ui/rename/AutoCompleteMatcher.java index 8f683d40..c73ca335 100644 --- a/source/net/filebot/ui/rename/AutoCompleteMatcher.java +++ b/source/net/filebot/ui/rename/AutoCompleteMatcher.java @@ -1,7 +1,5 @@ - package net.filebot.ui.rename; - import java.awt.Component; import java.io.File; import java.util.List; @@ -10,8 +8,7 @@ import java.util.Locale; import net.filebot.similarity.Match; import net.filebot.web.SortOrder; - interface AutoCompleteMatcher { - - List> match(List files, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception; + + List> match(List files, boolean strict, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception; } diff --git a/source/net/filebot/ui/rename/EpisodeListMatcher.java b/source/net/filebot/ui/rename/EpisodeListMatcher.java index d1dcfdc5..964f5454 100644 --- a/source/net/filebot/ui/rename/EpisodeListMatcher.java +++ b/source/net/filebot/ui/rename/EpisodeListMatcher.java @@ -48,7 +48,7 @@ import net.filebot.web.SortOrder; class EpisodeListMatcher implements AutoCompleteMatcher { - private final EpisodeListProvider provider; + private EpisodeListProvider provider; private boolean useAnimeIndex; private boolean useSeriesIndex; @@ -170,7 +170,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } @Override - public List> match(List files, final SortOrder sortOrder, final Locale locale, final boolean autodetection, final Component parent) throws Exception { + public List> match(List files, final boolean strict, final SortOrder sortOrder, final Locale locale, final boolean autodetection, final Component parent) throws Exception { if (files.isEmpty()) { return justFetchEpisodeList(sortOrder, locale, parent); } @@ -182,33 +182,49 @@ class EpisodeListMatcher implements AutoCompleteMatcher { final List mediaFiles = filter(fileset, VIDEO_FILES, SUBTITLE_FILES); // assume that many shows will be matched, do it folder by folder - List>>> taskPerFolder = new ArrayList>>>(); + List>>> tasks = 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)); // detect series names and create episode list fetch tasks - for (Entry, Set> sameSeriesGroup : mapSeriesNamesByFiles(mediaFiles, locale, useSeriesIndex, useAnimeIndex).entrySet()) { - final List> batchSets = new ArrayList>(); - final Collection queries = sameSeriesGroup.getValue(); + if (strict) { + // in strict mode simply process file-by-file (ignoring all files that don't contain clear SxE patterns) + for (final File file : mediaFiles) { + if (parseEpisodeNumber(file, true) != null || parseDate(file) != null) { + tasks.add(new Callable>>() { - if (queries != null && queries.size() > 0) { - // handle series name batch set all at once -> only 1 batch set - batchSets.add(new ArrayList(sameSeriesGroup.getKey())); - } else { - // these files don't seem to belong to any series -> handle folder per folder -> multiple batch sets - batchSets.addAll(mapByFolder(sameSeriesGroup.getKey()).values()); + @Override + public List> call() throws Exception { + return matchEpisodeSet(singletonList(file), detectSeriesNames(singleton(file), useSeriesIndex, useAnimeIndex, locale), sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent); + } + }); + } } + } else { + // in non-strict mode use the complicated (more powerful but also more error prone) match-batch-by-batch logic + for (Entry, Set> sameSeriesGroup : mapSeriesNamesByFiles(mediaFiles, locale, useSeriesIndex, useAnimeIndex).entrySet()) { + final List> batchSets = new ArrayList>(); + final Collection queries = sameSeriesGroup.getValue(); - for (final List batchSet : batchSets) { - taskPerFolder.add(new Callable>>() { + if (queries != null && queries.size() > 0) { + // handle series name batch set all at once -> only 1 batch set + batchSets.add(new ArrayList(sameSeriesGroup.getKey())); + } else { + // these files don't seem to belong to any series -> handle folder per folder -> multiple batch sets + batchSets.addAll(mapByFolder(sameSeriesGroup.getKey()).values()); + } - @Override - public List> call() throws Exception { - return matchEpisodeSet(batchSet, queries, sortOrder, locale, autodetection, selectionMemory, inputMemory, parent); - } - }); + for (final List batchSet : batchSets) { + tasks.add(new Callable>>() { + + @Override + public List> call() throws Exception { + return matchEpisodeSet(batchSet, queries, sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent); + } + }); + } } } @@ -218,7 +234,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { try { // merge all episodes List> matches = new ArrayList>(); - for (Future>> future : executor.invokeAll(taskPerFolder)) { + for (Future>> future : executor.invokeAll(tasks)) { // make sure each episode has unique object data for (Match it : future.get()) { matches.add(new Match(it.getValue(), ((Episode) it.getCandidate()).clone())); @@ -259,7 +275,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } } - public List> matchEpisodeSet(final List files, Collection queries, SortOrder sortOrder, Locale locale, boolean autodetection, Map selectionMemory, Map> inputMemory, Component parent) throws Exception { + public List> matchEpisodeSet(final List files, Collection queries, SortOrder sortOrder, boolean strict, Locale locale, boolean autodetection, Map selectionMemory, Map> inputMemory, Component parent) throws Exception { Set episodes = emptySet(); // detect series name and fetch episode list @@ -273,7 +289,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } // require user input if auto-detection has failed or has been disabled - if (episodes.isEmpty()) { + if (episodes.isEmpty() && !strict) { List detectedSeriesNames = detectSeriesNames(files, useSeriesIndex, useAnimeIndex, locale); String parentPathHint = normalizePathSeparators(getRelativePathTail(files.get(0).getParentFile(), 2).getPath()); String suggestion = detectedSeriesNames.size() > 0 ? join(detectedSeriesNames, "; ") : parentPathHint; @@ -299,10 +315,12 @@ class EpisodeListMatcher implements AutoCompleteMatcher { List> matches = new ArrayList>(); // group by subtitles first and then by files in general - for (List filesPerType : mapByMediaExtension(files).values()) { - EpisodeMatcher matcher = new EpisodeMatcher(filesPerType, episodes, false); - for (Match it : matcher.match()) { - matches.add(new Match(it.getValue(), ((Episode) it.getCandidate()).clone())); + if (episodes.size() > 0) { + for (List filesPerType : mapByMediaExtension(files).values()) { + EpisodeMatcher matcher = new EpisodeMatcher(filesPerType, episodes, strict); + for (Match it : matcher.match()) { + matches.add(new Match(it.getValue(), ((Episode) it.getCandidate()).clone())); + } } } diff --git a/source/net/filebot/ui/rename/MovieHashMatcher.java b/source/net/filebot/ui/rename/MovieHashMatcher.java index eee5119c..a9b9dae0 100644 --- a/source/net/filebot/ui/rename/MovieHashMatcher.java +++ b/source/net/filebot/ui/rename/MovieHashMatcher.java @@ -65,7 +65,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { } @Override - public List> match(final List files, final SortOrder sortOrder, final Locale locale, final boolean autodetect, final Component parent) throws Exception { + public List> match(final List files, final boolean strict, final SortOrder sortOrder, final Locale locale, final boolean autodetect, final Component parent) throws Exception { if (files.isEmpty()) { return justFetchMovieInfo(locale, parent); } @@ -183,7 +183,23 @@ class MovieHashMatcher implements AutoCompleteMatcher { public Map> call() throws Exception { Map> detection = new LinkedHashMap>(); for (File f : folder) { - detection.put(f, detectMovie(f, null, service, locale, false)); + if (strict) { + // in strict mode, only process movies that follow the name (year) pattern + List year = parseMovieYear(getRelativePathTail(f, 3).getPath()); + if (year.size() > 0) { + // allow only movie matches where the the movie year matches the year pattern in the filename + List matches = new ArrayList(); + for (Movie movie : detectMovie(f, null, service, locale, strict)) { + if (year.contains(movie.getYear())) { + matches.add(movie); + } + } + detection.put(f, matches); + } + } else { + // in non-strict mode just allow all options + detection.put(f, detectMovie(f, null, service, locale, strict)); + } } return detection; } @@ -200,7 +216,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { // auto-select movie or ask user for (Entry> it : detection.get().entrySet()) { File movieFile = it.getKey(); - Movie movie = grabMovieName(movieFile, it.getValue(), locale, autodetect, memory, parent); + Movie movie = grabMovieName(movieFile, it.getValue(), strict, locale, autodetect, memory, parent); if (movie != null) { movieByFile.put(movieFile, movie); } @@ -262,13 +278,9 @@ class MovieHashMatcher implements AutoCompleteMatcher { return matches; } - protected Movie grabMovieName(File movieFile, Collection options, Locale locale, boolean autodetect, Map memory, Component parent) throws Exception { + protected Movie grabMovieName(File movieFile, Collection options, boolean strict, Locale locale, boolean autodetect, Map memory, Component parent) throws Exception { // allow manual user input - if (!autodetect || options.isEmpty()) { - if (autodetect && memory.containsKey("repeat")) { - return null; - } - + if (!strict && (!autodetect || options.isEmpty()) && !(autodetect && memory.containsKey("repeat"))) { String suggestion = options.isEmpty() ? stripReleaseInfo(getName(movieFile)) : options.iterator().next().getName(); @SuppressWarnings("unchecked") @@ -284,12 +296,12 @@ class MovieHashMatcher implements AutoCompleteMatcher { if (input != null) { options = service.searchMovie(input, locale); if (options.size() > 0) { - return selectMovie(movieFile, input, options, memory, parent); + return selectMovie(movieFile, strict, input, options, memory, parent); } } } - return options.isEmpty() ? null : selectMovie(movieFile, null, options, memory, parent); + return options.isEmpty() ? null : selectMovie(movieFile, strict, null, options, memory, parent); } protected String getQueryInputMessage(File file) throws Exception { @@ -314,12 +326,12 @@ class MovieHashMatcher implements AutoCompleteMatcher { return html.toString(); } - protected String checkedStripReleaseInfo(File file) throws Exception { + protected String checkedStripReleaseInfo(File file, boolean strict) throws Exception { String name = stripReleaseInfo(getName(file)); // try to redeem possible false negative matches if (name.length() < 2) { - Movie match = checkMovie(file, false); + Movie match = checkMovie(file, strict); if (match != null) { return match.getName(); } @@ -328,18 +340,18 @@ class MovieHashMatcher implements AutoCompleteMatcher { return name; } - protected Movie selectMovie(final File movieFile, final String userQuery, final Collection options, final Map memory, final Component parent) throws Exception { + protected Movie selectMovie(final File movieFile, final boolean strict, final String userQuery, final Collection options, final Map memory, final Component parent) throws Exception { // just auto-pick singleton results if (options.size() == 1) { return options.iterator().next(); } // 1. movie by filename - final String fileQuery = (userQuery != null) ? userQuery : checkedStripReleaseInfo(movieFile); + final String fileQuery = (userQuery != null) ? userQuery : checkedStripReleaseInfo(movieFile, strict); // 2. movie by directory final File movieFolder = guessMovieFolder(movieFile); - final String folderQuery = (userQuery != null || movieFolder == null) ? "" : checkedStripReleaseInfo(movieFolder); + final String folderQuery = (userQuery != null || movieFolder == null) ? "" : checkedStripReleaseInfo(movieFolder, strict); // auto-ignore invalid files if (userQuery == null && fileQuery.length() < 2 && folderQuery.length() < 2) { @@ -364,7 +376,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { for (Movie result : options) { float maxSimilarity = 0; for (String query : new String[] { fileQuery, folderQuery }) { - for (String name : result.getEffectiveNamesWithoutYear()) { + for (String name : strict ? result.getEffectiveNames() : result.getEffectiveNamesWithoutYear()) { if (maxSimilarity >= threshold) continue; @@ -381,6 +393,11 @@ class MovieHashMatcher implements AutoCompleteMatcher { return probableMatches.get(0); } + // if we haven't confirmed a match at this point then the file is probably badly named and should be ignored + if (strict) { + return null; + } + // show selection dialog on EDT final RunnableFuture showSelectDialog = new FutureTask(new Callable() { diff --git a/source/net/filebot/ui/rename/RenamePanel.java b/source/net/filebot/ui/rename/RenamePanel.java index 24deabf9..0a2d5a79 100644 --- a/source/net/filebot/ui/rename/RenamePanel.java +++ b/source/net/filebot/ui/rename/RenamePanel.java @@ -97,6 +97,7 @@ public class RenamePanel extends JComponent { private static final PreferencesEntry persistentFileFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.file"); private static final PreferencesEntry persistentLastFormatState = Settings.forPackage(RenamePanel.class).entry("rename.last.format.state"); + private static final PreferencesEntry persistentPreferredMatchMode = Settings.forPackage(RenamePanel.class).entry("rename.match.mode").defaultValue("Opportunistic"); private static final PreferencesEntry persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en"); private static final PreferencesEntry persistentPreferredEpisodeOrder = Settings.forPackage(RenamePanel.class).entry("rename.episode.order").defaultValue("Airdate"); @@ -392,6 +393,9 @@ public class RenamePanel extends JComponent { @Override public void actionPerformed(ActionEvent evt) { + String[] modes = new String[] { "Opportunistic", "Strict" }; + JComboBox modeCombo = new JComboBox(modes); + List languages = new ArrayList(); languages.addAll(Language.preferredLanguages()); // add preferred languages first languages.addAll(Language.availableLanguages()); // then others @@ -411,31 +415,41 @@ public class RenamePanel extends JComponent { } }); - // pre-select current preferences + // restore current preference values try { - orderCombo.setSelectedItem(SortOrder.forName(persistentPreferredEpisodeOrder.getValue())); - } catch (IllegalArgumentException e) { - // ignore - } - for (Language language : languages) { - if (language.getCode().equals(persistentPreferredLanguage.getValue())) { - languageList.setSelectedValue(language, true); - break; + modeCombo.setSelectedItem(persistentPreferredMatchMode.getValue()); + for (Language language : languages) { + if (language.getCode().equals(persistentPreferredLanguage.getValue())) { + languageList.setSelectedValue(language, true); + break; + } } + orderCombo.setSelectedItem(SortOrder.forName(persistentPreferredEpisodeOrder.getValue())); + } catch (Exception e) { + Logger.getLogger(RenamePanel.class.getName()).log(Level.WARNING, e.getMessage(), e); } + JScrollPane spModeCombo = new JScrollPane(modeCombo, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + spModeCombo.setBorder(new CompoundBorder(new TitledBorder("Match Mode"), spModeCombo.getBorder())); JScrollPane spLanguageList = new JScrollPane(languageList); - spLanguageList.setBorder(new CompoundBorder(new TitledBorder("Preferred Language"), spLanguageList.getBorder())); + spLanguageList.setBorder(new CompoundBorder(new TitledBorder("Language"), spLanguageList.getBorder())); JScrollPane spOrderCombo = new JScrollPane(orderCombo, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - spOrderCombo.setBorder(new CompoundBorder(new TitledBorder("Preferred Episode Order"), spOrderCombo.getBorder())); + spOrderCombo.setBorder(new CompoundBorder(new TitledBorder("Episode Order"), spOrderCombo.getBorder())); + + // fix background issues on OSX + spModeCombo.setOpaque(false); + spLanguageList.setOpaque(false); + spOrderCombo.setOpaque(false); JPanel message = new JPanel(new MigLayout("fill, flowy, insets 0")); - message.add(spLanguageList, "grow"); + message.add(spModeCombo, "grow, hmin 24px"); + message.add(spLanguageList, "grow, hmin 50px"); message.add(spOrderCombo, "grow, hmin 24px"); JOptionPane pane = new JOptionPane(message, PLAIN_MESSAGE, OK_CANCEL_OPTION); pane.createDialog(getWindowAncestor(RenamePanel.this), "Preferences").setVisible(true); if (pane.getValue() != null && pane.getValue().equals(OK_OPTION)) { + persistentPreferredMatchMode.setValue((String) modeCombo.getSelectedItem()); persistentPreferredLanguage.setValue(((Language) languageList.getSelectedValue()).getCode()); persistentPreferredEpisodeOrder.setValue(((SortOrder) orderCombo.getSelectedItem()).name()); } @@ -631,6 +645,7 @@ public class RenamePanel extends JComponent { renameModel.values().clear(); final List remainingFiles = new LinkedList(renameModel.files()); + final boolean strict = "strict".equalsIgnoreCase(persistentPreferredMatchMode.getValue()); final SortOrder order = SortOrder.forName(persistentPreferredEpisodeOrder.getValue()); final Locale locale = new Locale(persistentPreferredLanguage.getValue()); final boolean autodetection = !isShiftOrAltDown(evt); // skip name auto-detection if SHIFT is pressed @@ -645,7 +660,7 @@ public class RenamePanel extends JComponent { @Override protected List> doInBackground() throws Exception { - List> matches = matcher.match(remainingFiles, order, locale, autodetection, getWindow(RenamePanel.this)); + List> matches = matcher.match(remainingFiles, strict, order, locale, autodetection, getWindow(RenamePanel.this)); // remove matched files for (Match match : matches) { diff --git a/website/data/query-blacklist.txt b/website/data/query-blacklist.txt index b6cd7018..d6940fa9 100644 --- a/website/data/query-blacklist.txt +++ b/website/data/query-blacklist.txt @@ -37,6 +37,7 @@ ^BTN$ ^Cartoon$ ^Cinema$ +^Classic$ ^clean$ ^cleaned$ ^Comedy$