diff --git a/build.xml b/build.xml index 19f338cb..b3944563 100644 --- a/build.xml +++ b/build.xml @@ -92,6 +92,10 @@ + + + + diff --git a/source/net/sourceforge/filebot/FileBotUtilities.java b/source/net/sourceforge/filebot/FileBotUtilities.java index c5e0399e..18d92dfa 100644 --- a/source/net/sourceforge/filebot/FileBotUtilities.java +++ b/source/net/sourceforge/filebot/FileBotUtilities.java @@ -2,12 +2,14 @@ package net.sourceforge.filebot; +import java.io.File; import java.io.FileFilter; import java.util.AbstractList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter; @@ -63,10 +65,6 @@ public final class FileBotUtilities { public static String join(Object[] values, String separator) { - if (values == null) { - return null; - } - StringBuilder sb = new StringBuilder(); for (int i = 0; i < values.length; i++) { @@ -81,12 +79,12 @@ public final class FileBotUtilities { } - public static List asStringList(final List list) { + public static List asFileNameList(final List list) { return new AbstractList() { @Override public String get(int index) { - return list.get(index).toString(); + return FileUtilities.getName(list.get(index)); } diff --git a/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java b/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java index 59c2c6dc..50499dbf 100644 --- a/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java +++ b/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java @@ -23,7 +23,7 @@ public class SeasonEpisodeMatcher { patterns[1] = new SeasonEpisodePattern("(? matchAll(List names) { + public String match(File file) { + return match(file.getName(), file.getParent()); + } + + + public Collection matchAll(File... files) { SeriesNameCollection seriesNames = new SeriesNameCollection(); - // use pattern matching with frequency threshold + // group files by parent folder + for (Entry entry : mapNamesByFolder(files).entrySet()) { + String parent = entry.getKey().getName(); + String[] names = entry.getValue(); + + for (String nameMatch : matchAll(names)) { + String commonMatch = matchByFirstCommonWordSequence(nameMatch, parent); + + // prefer common match, but use name match if there is no matching word sequence + seriesNames.add(commonMatch != null ? commonMatch : nameMatch); + } + } + + return seriesNames; + } + + + public Collection matchAll(String... names) { + SeriesNameCollection seriesNames = new SeriesNameCollection(); + + // 1. use pattern matching with frequency threshold seriesNames.addAll(flatMatchAll(names)); - // deep match common word sequences + // 2. match common word sequences seriesNames.addAll(deepMatchAll(names)); return seriesNames; @@ -49,11 +78,11 @@ public class SeriesNameMatcher { /** * Try to match and verify all series names using known season episode patterns. * - * @param names list of episode names - * @return series names that have been matched one or multiple times depending on the size - * of the given list + * @param names episode names + * @return series names that have been matched one or multiple times depending on the + * threshold */ - protected Collection flatMatchAll(Iterable names) { + private Collection flatMatchAll(String[] names) { ThresholdCollection seriesNames = new ThresholdCollection(threshold, String.CASE_INSENSITIVE_ORDER); for (String name : names) { @@ -74,9 +103,9 @@ public class SeriesNameMatcher { * @param names list of episode names * @return all common word sequences that have been found */ - protected Collection deepMatchAll(List names) { - // don't use common word sequence matching for less than 5 names - if (names.size() < threshold) { + private Collection deepMatchAll(String[] names) { + // can't use common word sequence matching for less than 2 names + if (names.length < 2 || names.length < threshold) { return Collections.emptySet(); } @@ -90,23 +119,44 @@ public class SeriesNameMatcher { // recursive divide and conquer List results = new ArrayList(); - if (names.size() >= 2) { - // split list in two and try to match common word sequence on those - results.addAll(deepMatchAll(names.subList(0, names.size() / 2))); - results.addAll(deepMatchAll(names.subList(names.size() / 2, names.size()))); - } + // split list in two and try to match common word sequence on those + results.addAll(deepMatchAll(Arrays.copyOfRange(names, 0, names.length / 2))); + results.addAll(deepMatchAll(Arrays.copyOfRange(names, names.length / 2, names.length))); return results; } + /** + * Match series name using season episode pattern and then try to find a common word + * sequence between the first match and the given parent. + * + * @param name episode name + * @param parent a string that contains the series name + * @return a likely series name + */ + public String match(String name, String parent) { + String nameMatch = matchBySeasonEpisodePattern(name); + + if (nameMatch != null) { + String commonMatch = matchByFirstCommonWordSequence(nameMatch, parent); + + if (commonMatch != null) { + return commonMatch; + } + } + + return nameMatch; + } + + /** * Try to match a series name from the given episode name using known season episode * patterns. * * @param name episode name * @return a substring of the given name that ends before the first occurrence of a season - * episode pattern, or null + * episode pattern, or null if there is no such pattern */ public String matchBySeasonEpisodePattern(String name) { int seasonEpisodePosition = seasonEpisodeMatcher.find(name); @@ -126,10 +176,9 @@ public class SeriesNameMatcher { * @param names various episode names (5 or more for accurate results) * @return a word sequence all episode names have in common, or null */ - public String matchByFirstCommonWordSequence(Collection names) { - if (names.size() <= 1) { - // can't match common sequence from less than two names - return null; + public String matchByFirstCommonWordSequence(String... names) { + if (names.length < 2) { + throw new IllegalArgumentException("Can't match common sequence from less than two names"); } String[] common = null; @@ -151,14 +200,19 @@ public class SeriesNameMatcher { } } - // join will return null, if common is null + if (common == null) + return null; + return join(common, " "); } protected String normalize(String name) { - // remove group names (remove any [...]) - name = name.replaceAll("\\[[^\\]]+\\]", ""); + // normalize brackets, convert (...) to [...] + name = name.replace('(', '[').replace(')', ']'); + + // remove group names, any [...] + name = name.replaceAll("\\[[^\\[]+\\]", ""); // remove special characters name = name.replaceAll("[\\p{Punct}\\p{Space}]+", " "); @@ -195,6 +249,33 @@ public class SeriesNameMatcher { return null; } + + private Map mapNamesByFolder(File... files) { + Map> filesByFolder = new LinkedHashMap>(); + + for (File file : files) { + File folder = file.getParentFile(); + + List list = filesByFolder.get(folder); + + if (list == null) { + list = new ArrayList(); + filesByFolder.put(folder, list); + } + + list.add(file); + } + + // convert folder->files map to folder->names map + Map namesByFolder = new LinkedHashMap(); + + for (Entry> entry : filesByFolder.entrySet()) { + namesByFolder.put(entry.getKey(), FileBotUtilities.asFileNameList(entry.getValue()).toArray(new String[0])); + } + + return namesByFolder; + } + protected static class SeriesNameCollection extends AbstractCollection { @@ -272,30 +353,30 @@ public class SeriesNameMatcher { @Override - public boolean add(E e) { - Collection buffer = limbo.get(e); + public boolean add(E value) { + Collection buffer = limbo.get(value); if (buffer == null) { // initialize buffer buffer = new ArrayList(threshold); - limbo.put(e, buffer); + limbo.put(value, buffer); } if (buffer == heaven) { // threshold reached - heaven.add(e); + heaven.add(value); return true; } // add element to buffer - buffer.add(e); + buffer.add(value); // check if threshold has been reached if (buffer.size() >= threshold) { heaven.addAll(buffer); // replace buffer with heaven - limbo.put(e, heaven); + limbo.put(value, heaven); return true; } diff --git a/source/net/sourceforge/filebot/ui/FileBotWindow.java b/source/net/sourceforge/filebot/ui/FileBotWindow.java index b44ca156..21ac4fc7 100644 --- a/source/net/sourceforge/filebot/ui/FileBotWindow.java +++ b/source/net/sourceforge/filebot/ui/FileBotWindow.java @@ -2,7 +2,6 @@ package net.sourceforge.filebot.ui; -import static net.sourceforge.filebot.FileBotUtilities.asStringList; import static net.sourceforge.filebot.Settings.getApplicationName; import java.awt.BorderLayout; @@ -64,10 +63,10 @@ public class FileBotWindow extends JFrame implements ListSelectionListener { setSize(760, 615); - // restore the panel selection from last time, + //TODO restore the panel selection from last time, // switch to EpisodeListPanel by default (e.g. first start) - int selectedPanel = asStringList(panelSelectionList.getPanelModel()).indexOf(Settings.userRoot().get("selectedPanel")); - panelSelectionList.setSelectedIndex(selectedPanel); + // int selectedPanel = asStringList(panelSelectionList.getPanelModel()).indexOf(Settings.userRoot().get("selectedPanel")); + // panelSelectionList.setSelectedIndex(selectedPanel); // connect message handlers to message bus MessageBus.getDefault().addMessageHandler("panel", panelSelectMessageHandler); diff --git a/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java index c881e85f..e6932e41 100644 --- a/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java @@ -9,10 +9,12 @@ import static net.sourceforge.tuned.FileUtilities.containsOnly; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import net.sourceforge.filebot.FileBotUtilities; import net.sourceforge.filebot.torrent.Torrent; import net.sourceforge.filebot.ui.FileBotList; import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy; @@ -51,9 +53,7 @@ class FileListTransferablePolicy extends FileTransferablePolicy { } else if (containsOnly(files, TORRENT_FILES)) { loadTorrents(files); } else { - for (File file : files) { - list.getModel().add(FileUtilities.getName(file)); - } + list.getModel().addAll(FileBotUtilities.asFileNameList(files)); } } @@ -65,9 +65,7 @@ class FileListTransferablePolicy extends FileTransferablePolicy { } for (File folder : folders) { - for (File file : folder.listFiles()) { - list.getModel().add(FileUtilities.getName(file)); - } + list.getModel().addAll(FileBotUtilities.asFileNameList(Arrays.asList(folder.listFiles()))); } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java b/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java index c70fac99..64e72b2b 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java @@ -3,10 +3,10 @@ package net.sourceforge.filebot.ui.panel.rename; import static net.sourceforge.filebot.FileBotUtilities.SUBTITLE_FILES; -import static net.sourceforge.filebot.FileBotUtilities.asStringList; import static net.sourceforge.filebot.web.Episode.formatEpisodeNumbers; import static net.sourceforge.tuned.FileUtilities.FILES; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -52,9 +52,16 @@ class AutoEpisodeListMatcher extends SwingWorker> protected Collection matchSeriesNames(List episodes) { - int threshold = Math.min(episodes.size(), 5); + File[] files = new File[episodes.size()]; - return new SeriesNameMatcher(threshold).matchAll(asStringList(episodes)); + for (int i = 0; i < files.length; i++) { + files[i] = episodes.get(i).getFile(); + } + + // allow matching of a small number of episodes, by setting threshold = length if length < 5 + int threshold = Math.min(files.length, 5); + + return new SeriesNameMatcher(threshold).matchAll(files); } diff --git a/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java b/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java index a5be70c7..087e4f88 100644 --- a/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java +++ b/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java @@ -15,6 +15,12 @@ public class SeriesNameMatcherTest { private static SeriesNameMatcher matcher = new SeriesNameMatcher(5); + @Test + public void match() { + assertEquals("Test Series", matcher.match("My Test Series - 1x01", "Test Series - Season 1")); + } + + @Test public void matchBeforeSeasonEpisodePattern() { assertEquals("The Test", matcher.matchBySeasonEpisodePattern("The Test - 1x01")); @@ -30,7 +36,10 @@ public class SeriesNameMatcherTest { assertEquals("The Test", matcher.normalize("_The_Test_-_ ...")); // brackets - assertEquals("Luffy", matcher.normalize("[strawhat] Luffy [D.] [@Monkey]")); + assertEquals("Luffy", matcher.normalize("[strawhat] Luffy [D.] [#Monkey]")); + + // invalid brackets + assertEquals("strawhat Luffy", matcher.normalize("(strawhat [Luffy (#Monkey)")); }