diff --git a/lib/commons-logging.jar b/lib/commons-logging.jar new file mode 100644 index 00000000..8758a96b Binary files /dev/null and b/lib/commons-logging.jar differ diff --git a/lib/junrar-custom.jar b/lib/junrar-custom.jar new file mode 100644 index 00000000..2410215e Binary files /dev/null and b/lib/junrar-custom.jar differ diff --git a/source/ehcache.xml b/source/ehcache.xml index 28b94cb0..3cf7757b 100644 --- a/source/ehcache.xml +++ b/source/ehcache.xml @@ -125,7 +125,7 @@ Simple memory cache named web. Time to live is 5 min. This cache is used by TheTVDBClient and TVRageClient. --> , |, \r and \n @@ -36,13 +26,13 @@ public final class FileBotUtil { * @param filename original filename * @return valid filename stripped of invalid characters */ - public static String validateFileName(String filename) { + public static String validateFileName(CharSequence filename) { // strip invalid characters from filename return INVALID_CHARACTERS_PATTERN.matcher(filename).replaceAll(""); } - public static boolean isInvalidFileName(String filename) { + public static boolean isInvalidFileName(CharSequence filename) { return INVALID_CHARACTERS_PATTERN.matcher(filename).find(); } @@ -54,13 +44,13 @@ public final class FileBotUtil { public static final Pattern EMBEDDED_CHECKSUM_PATTERN = Pattern.compile("(?<=\\[|\\()(\\p{XDigit}{8,})(?=\\]|\\))"); - public static String getEmbeddedChecksum(String string) { + public static String getEmbeddedChecksum(CharSequence string) { Matcher matcher = EMBEDDED_CHECKSUM_PATTERN.matcher(string); String embeddedChecksum = null; // get last match while (matcher.find()) { - embeddedChecksum = matcher.group(0); + embeddedChecksum = matcher.group(); } return embeddedChecksum; @@ -71,41 +61,52 @@ public final class FileBotUtil { return string.replaceAll("[\\(\\[]\\p{XDigit}{8}[\\]\\)]", ""); } - public static final List TORRENT_FILE_EXTENSIONS = unmodifiableList("torrent"); - public static final List SFV_FILE_EXTENSIONS = unmodifiableList("sfv"); - public static final List LIST_FILE_EXTENSIONS = unmodifiableList("txt", "list", ""); - public static final List SUBTITLE_FILE_EXTENSIONS = unmodifiableList("srt", "sub", "ssa", "smi"); - - - private static List unmodifiableList(String... elements) { - return Collections.unmodifiableList(Arrays.asList(elements)); - } - - public static boolean containsOnlyFolders(Iterable files) { - for (File file : files) { - if (!file.isDirectory()) - return false; + public static String join(Object[] values, String separator) { + if (values == null) { + return null; } - return true; - } - - - public static boolean containsOnly(Iterable files, Iterable extensions) { - for (File file : files) { - if (!FileUtil.hasExtension(file, extensions)) - return false; + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + sb.append(values[i]); + + if (i < values.length - 1) { + sb.append(separator); + } } - return true; + return sb.toString(); } + public static List asStringList(final List list) { + return new AbstractList() { + + @Override + public String get(int index) { + return list.get(index).toString(); + } + + + @Override + public int size() { + return list.size(); + } + }; + } + + public static final FileFilter TORRENT_FILES = new ExtensionFileFilter("torrent"); + public static final FileFilter SFV_FILES = new ExtensionFileFilter("sfv"); + public static final FileFilter LIST_FILES = new ExtensionFileFilter("txt", "list", ""); + public static final FileFilter SUBTITLE_FILES = new ExtensionFileFilter("srt", "sub", "ssa", "ass", "smi"); + + /** * Dummy constructor to prevent instantiation. */ - private FileBotUtil() { + private FileBotUtilities() { throw new UnsupportedOperationException(); } diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index 2e6c365e..6854f9a2 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -5,8 +5,6 @@ package net.sourceforge.filebot; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.prefs.BackingStoreException; -import java.util.prefs.Preferences; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -23,11 +21,24 @@ public class Main { /** * @param args */ - public static void main(String... args) { + public static void main(String... args) throws Exception { - final ArgumentBean argumentBean = handleArguments(args); + final ArgumentBean argumentBean = initializeArgumentBean(args); - setupLogging(); + if (argumentBean.isHelp()) { + printUsage(argumentBean); + + // just print help message and exit afterwards + System.exit(0); + } + + if (argumentBean.isClear()) { + // clear preferences + Settings.userRoot().clear(); + } + + initializeLogging(); + initializeSettings(); try { // UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); @@ -57,7 +68,7 @@ public class Main { } - private static void setupLogging() { + private static void initializeLogging() { Logger uiLogger = Logger.getLogger("ui"); // don't use parent handlers @@ -74,35 +85,24 @@ public class Main { } - private static ArgumentBean handleArguments(String... args) { - + private static void initializeSettings() { + Settings.userRoot().putDefault("thetvdb.apikey", "58B4AA94C59AD656"); + } + + + private static ArgumentBean initializeArgumentBean(String... args) throws CmdLineException { ArgumentBean argumentBean = new ArgumentBean(); - CmdLineParser argumentParser = new CmdLineParser(argumentBean); - try { - argumentParser.parseArgument(args); - } catch (CmdLineException e) { - Logger.getLogger("global").log(Level.WARNING, e.getMessage()); - } - - if (argumentBean.isHelp()) { - System.out.println("Options:"); - argumentParser.printUsage(System.out); - - // just print help message and exit afterwards - System.exit(0); - } - - if (argumentBean.isClear()) { - // clear preferences - try { - Preferences.userNodeForPackage(Main.class).removeNode(); - } catch (BackingStoreException e) { - Logger.getLogger("global").log(Level.SEVERE, e.toString(), e); - } - } + new CmdLineParser(argumentBean).parseArgument(args); return argumentBean; } + + private static void printUsage(ArgumentBean argumentBean) { + System.out.println("Options:"); + + new CmdLineParser(argumentBean).printUsage(System.out); + } + } diff --git a/source/net/sourceforge/filebot/Settings.java b/source/net/sourceforge/filebot/Settings.java new file mode 100644 index 00000000..b7e34316 --- /dev/null +++ b/source/net/sourceforge/filebot/Settings.java @@ -0,0 +1,102 @@ + +package net.sourceforge.filebot; + + +import java.util.List; +import java.util.Map; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import net.sourceforge.tuned.ExceptionUtil; +import net.sourceforge.tuned.PreferencesList; +import net.sourceforge.tuned.PreferencesMap; +import net.sourceforge.tuned.PreferencesMap.Adapter; + + +public final class Settings { + + public static String getApplicationName() { + return "FileBot"; + }; + + + public static String getApplicationVersion() { + return "1.9"; + }; + + private static final Settings userRoot = new Settings(Preferences.userRoot(), getApplicationName()); + + + public static Settings userRoot() { + return userRoot; + } + + private final Preferences prefs; + + + private Settings(Preferences parentNode, String name) { + this.prefs = parentNode.node(name.toLowerCase()); + } + + + public Settings node(String nodeName) { + return new Settings(prefs, nodeName); + } + + + public void put(String key, String value) { + prefs.put(key, value); + } + + + public void putDefault(String key, String value) { + if (get(key) == null) { + put(key, value); + } + } + + + public String get(String key) { + return get(key, null); + } + + + public String get(String key, String def) { + return prefs.get(key, def); + } + + + public Map asMap(Class type) { + return PreferencesMap.map(prefs, type); + } + + + public Map asMap(Adapter adapter) { + return PreferencesMap.map(prefs, adapter); + } + + + public List asList(Class type) { + return PreferencesList.map(prefs, type); + } + + + public List asList(Adapter adapter) { + return PreferencesList.map(prefs, adapter); + } + + + public void clear() { + try { + // remove child nodes + for (String nodeName : prefs.childrenNames()) { + prefs.node(nodeName).removeNode(); + } + + // remove entries + prefs.clear(); + } catch (BackingStoreException e) { + throw ExceptionUtil.asRuntimeException(e); + } + } +} diff --git a/source/net/sourceforge/filebot/resources/action.match.small.png b/source/net/sourceforge/filebot/resources/action.match.small.png new file mode 100644 index 00000000..691965ed Binary files /dev/null and b/source/net/sourceforge/filebot/resources/action.match.small.png differ diff --git a/source/net/sourceforge/filebot/similarity/Matcher.java b/source/net/sourceforge/filebot/similarity/Matcher.java index e0a7df64..eb34d639 100644 --- a/source/net/sourceforge/filebot/similarity/Matcher.java +++ b/source/net/sourceforge/filebot/similarity/Matcher.java @@ -48,7 +48,7 @@ public class Matcher { } // match recursively - match(possibleMatches, 0); + deepMatch(possibleMatches, 0); // restore order according to the given values List> result = new ArrayList>(); @@ -74,17 +74,17 @@ public class Matcher { } - public List remainingValues() { + public synchronized List remainingValues() { return Collections.unmodifiableList(values); } - public List remainingCandidates() { + public synchronized List remainingCandidates() { return Collections.unmodifiableList(candidates); } - protected void match(Collection> possibleMatches, int level) throws InterruptedException { + protected void deepMatch(Collection> possibleMatches, int level) throws InterruptedException { if (level >= metrics.size() || possibleMatches.isEmpty()) { // no further refinement possible disjointMatchCollection.addAll(possibleMatches); @@ -106,8 +106,8 @@ public class Matcher { // remove invalid matches removeCollected(matchesWithEqualSimilarity); - // matches are ambiguous, more refined matching required - match(matchesWithEqualSimilarity, level + 1); + // matches may be ambiguous, more refined matching required + deepMatch(matchesWithEqualSimilarity, level + 1); } } diff --git a/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java b/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java index d18fdf2b..4372f4f5 100644 --- a/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java +++ b/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.similarity; -import static net.sourceforge.filebot.FileBotUtil.removeEmbeddedChecksum; +import static net.sourceforge.filebot.FileBotUtilities.removeEmbeddedChecksum; import uk.ac.shef.wit.simmetrics.similaritymetrics.AbstractStringMetric; import uk.ac.shef.wit.simmetrics.similaritymetrics.MongeElkan; import uk.ac.shef.wit.simmetrics.tokenisers.TokeniserQGram3Extended; @@ -30,7 +30,7 @@ public class NameSimilarityMetric implements SimilarityMetric { String name = removeEmbeddedChecksum(object.toString()); // normalize separators - name = name.replaceAll("[^\\p{Alnum}]+", " "); + name = name.replaceAll("[\\p{Punct}\\p{Space}]+", " "); // normalize case and trim return name.trim().toLowerCase(); diff --git a/source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java b/source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java index 93b92379..79780e21 100644 --- a/source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java +++ b/source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.similarity; -import static net.sourceforge.filebot.FileBotUtil.removeEmbeddedChecksum; +import static net.sourceforge.filebot.FileBotUtilities.removeEmbeddedChecksum; import java.util.ArrayList; import java.util.HashSet; diff --git a/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java b/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java new file mode 100644 index 00000000..59c2c6dc --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java @@ -0,0 +1,150 @@ + +package net.sourceforge.filebot.similarity; + + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class SeasonEpisodeMatcher { + + private final SeasonEpisodePattern[] patterns; + + + public SeasonEpisodeMatcher() { + patterns = new SeasonEpisodePattern[3]; + + // match patterns like S01E01, s01e02, ... [s01]_[e02], s01.e02, ... + patterns[0] = new SeasonEpisodePattern("(? match(CharSequence name) { + for (SeasonEpisodePattern pattern : patterns) { + List match = pattern.match(name); + + if (!match.isEmpty()) { + // current pattern did match + return match; + } + } + + return null; + } + + + public int find(CharSequence name) { + for (SeasonEpisodePattern pattern : patterns) { + int index = pattern.find(name); + + if (index >= 0) { + // current pattern did match + return index; + } + } + + return -1; + } + + + public static class SxE { + + public final int season; + public final int episode; + + + public SxE(int season, int episode) { + this.season = season; + this.episode = episode; + } + + + public SxE(String season, String episode) { + this.season = parse(season); + this.episode = parse(episode); + } + + + protected int parse(String number) { + return number == null || number.isEmpty() ? 0 : Integer.parseInt(number); + } + + + @Override + public boolean equals(Object object) { + if (object instanceof SxE) { + SxE other = (SxE) object; + return this.season == other.season && this.episode == other.episode; + } + + return false; + } + + + @Override + public String toString() { + return String.format("%dx%02d", season, episode); + } + } + + + protected static class SeasonEpisodePattern { + + protected final Pattern pattern; + + protected final int seasonGroup; + protected final int episodeGroup; + + + public SeasonEpisodePattern(String pattern) { + this(Pattern.compile(pattern), 1, 2); + } + + + public SeasonEpisodePattern(Pattern pattern, int seasonGroup, int episodeGroup) { + this.pattern = pattern; + this.seasonGroup = seasonGroup; + this.episodeGroup = episodeGroup; + } + + + public List match(CharSequence name) { + // name will probably contain no more than one match, but may contain more + List matches = new ArrayList(1); + + Matcher matcher = pattern.matcher(name); + + while (matcher.find()) { + matches.add(new SxE(matcher.group(seasonGroup), matcher.group(episodeGroup))); + } + + return matches; + } + + + public int find(CharSequence name) { + Matcher matcher = pattern.matcher(name); + + if (matcher.find()) + return matcher.start(); + + return -1; + } + } + +} diff --git a/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java b/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java index b217da0e..c93c15e0 100644 --- a/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java +++ b/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java @@ -2,38 +2,23 @@ package net.sourceforge.filebot.similarity; -import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE; public class SeasonEpisodeSimilarityMetric implements SimilarityMetric { private final NumericSimilarityMetric fallbackMetric = new NumericSimilarityMetric(); - private final SeasonEpisodePattern[] patterns; + private final SeasonEpisodeMatcher seasonEpisodeMatcher = new SeasonEpisodeMatcher(); - public SeasonEpisodeSimilarityMetric() { - patterns = new SeasonEpisodePattern[3]; - - // match patterns like S01E01, s01e02, ... [s01]_[e02], s01.e02, ... - patterns[0] = new SeasonEpisodePattern("(? sxeVector1 = match(normalize(o1)); - List sxeVector2 = match(normalize(o2)); + Collection sxeVector1 = parse(o1); + Collection sxeVector2 = parse(o2); if (sxeVector1 == null || sxeVector2 == null) { // name does not match any known pattern, return numeric similarity @@ -50,29 +35,8 @@ public class SeasonEpisodeSimilarityMetric implements SimilarityMetric { } - /** - * Try to get season and episode numbers for the given string. - * - * @param name match this string against the a set of know patterns - * @return the matches returned by the first pattern that returns any matches for this - * string, or null if no pattern returned any matches - */ - protected List match(String name) { - for (SeasonEpisodePattern pattern : patterns) { - List match = pattern.match(name); - - if (!match.isEmpty()) { - // current pattern did match - return match; - } - } - - return null; - } - - - protected String normalize(Object object) { - return object.toString(); + protected Collection parse(Object o) { + return seasonEpisodeMatcher.match(o.toString()); } @@ -93,79 +57,4 @@ public class SeasonEpisodeSimilarityMetric implements SimilarityMetric { return getClass().getName(); } - - protected static class SxE { - - public final int season; - public final int episode; - - - public SxE(int season, int episode) { - this.season = season; - this.episode = episode; - } - - - public SxE(String season, String episode) { - this(parseNumber(season), parseNumber(episode)); - } - - - private static int parseNumber(String number) { - return number == null || number.isEmpty() ? 0 : Integer.parseInt(number); - } - - - @Override - public boolean equals(Object object) { - if (object instanceof SxE) { - SxE other = (SxE) object; - return this.season == other.season && this.episode == other.episode; - } - - return false; - } - - - @Override - public String toString() { - return String.format("%dx%02d", season, episode); - } - } - - - protected static class SeasonEpisodePattern { - - protected final Pattern pattern; - - protected final int seasonGroup; - protected final int episodeGroup; - - - public SeasonEpisodePattern(String pattern) { - this(Pattern.compile(pattern), 1, 2); - } - - - public SeasonEpisodePattern(Pattern pattern, int seasonGroup, int episodeGroup) { - this.pattern = pattern; - this.seasonGroup = seasonGroup; - this.episodeGroup = episodeGroup; - } - - - public List match(String name) { - // name will probably contain no more than one match, but may contain more - List matches = new ArrayList(1); - - Matcher matcher = pattern.matcher(name); - - while (matcher.find()) { - matches.add(new SxE(matcher.group(seasonGroup), matcher.group(episodeGroup))); - } - - return matches; - } - } - } diff --git a/source/net/sourceforge/filebot/similarity/SeriesNameMatcher.java b/source/net/sourceforge/filebot/similarity/SeriesNameMatcher.java new file mode 100644 index 00000000..2e1524f9 --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/SeriesNameMatcher.java @@ -0,0 +1,319 @@ + +package net.sourceforge.filebot.similarity; + + +import static net.sourceforge.filebot.FileBotUtilities.join; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.TreeMap; + + +public class SeriesNameMatcher { + + protected final SeasonEpisodeMatcher seasonEpisodeMatcher = new SeasonEpisodeMatcher(); + + protected final int threshold; + + + public SeriesNameMatcher(int threshold) { + if (threshold <= 0) + throw new IllegalArgumentException("threshold must be greater than 0"); + + this.threshold = threshold; + } + + + public Collection matchAll(List names) { + SeriesNameCollection seriesNames = new SeriesNameCollection(); + + // use pattern matching with frequency threshold + seriesNames.addAll(flatMatchAll(names)); + + // deep match common word sequences + seriesNames.addAll(deepMatchAll(names)); + + return seriesNames; + } + + + /** + * 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 + */ + protected Collection flatMatchAll(Iterable names) { + ThresholdCollection seriesNames = new ThresholdCollection(threshold, String.CASE_INSENSITIVE_ORDER); + + for (String name : names) { + String match = matchBySeasonEpisodePattern(name); + + if (match != null) { + seriesNames.add(match); + } + } + + return seriesNames; + } + + + /** + * Try to match all common word sequences in the given list. + * + * @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) { + return Collections.emptySet(); + } + + String common = matchByFirstCommonWordSequence(names); + + if (common != null) { + // common word sequence found + return Collections.singleton(common); + } + + // 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()))); + } + + return results; + } + + + /** + * 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 + */ + public String matchBySeasonEpisodePattern(String name) { + int seasonEpisodePosition = seasonEpisodeMatcher.find(name); + + if (seasonEpisodePosition > 0) { + // series name ends at the first season episode pattern + return normalize(name.substring(0, seasonEpisodePosition)); + } + + return null; + } + + + /** + * Try to match a series name from the first common word sequence. + * + * @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; + } + + String[] common = null; + + for (String name : names) { + String[] words = normalize(name).split("\\s+"); + + if (common == null) { + // initialize common with current word array + common = words; + } else { + // find common sequence + common = firstCommonSequence(common, words, String.CASE_INSENSITIVE_ORDER); + + if (common == null) { + // no common sequence + return null; + } + } + } + + // join will return null, if common is null + return join(common, " "); + } + + + protected String normalize(String name) { + // remove group names (remove any [...]) + name = name.replaceAll("\\[[^\\]]+\\]", ""); + + // remove special characters + name = name.replaceAll("[\\p{Punct}\\p{Space}]+", " "); + + return name.trim(); + } + + + protected T[] firstCommonSequence(T[] seq1, T[] seq2, Comparator equalsComparator) { + for (int i = 0; i < seq1.length; i++) { + for (int j = 0; j < seq2.length; j++) { + // common sequence length + int len = 0; + + // iterate over common sequence + while ((i + len < seq1.length) && (j + len < seq2.length) && (equalsComparator.compare(seq1[i + len], seq2[j + len]) == 0)) { + len++; + } + + // check if a common sequence was found + if (len > 0) { + if (i == 0 && len == seq1.length) + return seq1; + + if (j == 0 && len == seq2.length) + return seq2; + + return Arrays.copyOfRange(seq1, i, i + len); + } + } + } + + // no intersection at all + return null; + } + + + protected static class SeriesNameCollection extends AbstractCollection { + + private final Map data = new LinkedHashMap(); + + + @Override + public boolean add(String value) { + String key = value.toLowerCase(); + String current = data.get(key); + + // prefer strings with similar upper/lower case ration (e.g. prefer Roswell over roswell) + if (current == null || firstCharacterCaseBalance(current) < firstCharacterCaseBalance(value)) { + data.put(key, value); + return true; + } + + return false; + } + + + protected float firstCharacterCaseBalance(String s) { + int upper = 0; + int lower = 0; + + Scanner scanner = new Scanner(s); // Scanner has white space delimiter by default + + while (scanner.hasNext()) { + char c = scanner.next().charAt(0); + + if (Character.isLowerCase(c)) + lower++; + else if (Character.isUpperCase(c)) + upper++; + } + + // give upper case characters a slight boost + return (lower + (upper * 1.01f)) / Math.abs(lower - upper); + } + + + @Override + public boolean contains(Object o) { + return data.containsKey(o.toString().toLowerCase()); + } + + + @Override + public Iterator iterator() { + return data.values().iterator(); + } + + + @Override + public int size() { + return data.size(); + } + + } + + + protected static class ThresholdCollection extends AbstractCollection { + + private final Collection heaven; + private final Map> limbo; + + private final int threshold; + + + public ThresholdCollection(int threshold, Comparator equalityComparator) { + this.heaven = new ArrayList(); + this.limbo = new TreeMap>(equalityComparator); + this.threshold = threshold; + } + + + @Override + public boolean add(E e) { + Collection buffer = limbo.get(e); + + if (buffer == null) { + // initialize buffer + buffer = new ArrayList(threshold); + limbo.put(e, buffer); + } + + if (buffer == heaven) { + // threshold reached + heaven.add(e); + return true; + } + + // add element to buffer + buffer.add(e); + + // check if threshold has been reached + if (buffer.size() >= threshold) { + heaven.addAll(buffer); + + // replace buffer with heaven + limbo.put(e, heaven); + return true; + } + + return false; + }; + + + @Override + public Iterator iterator() { + return heaven.iterator(); + } + + + @Override + public int size() { + return heaven.size(); + } + + } + +} diff --git a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java index 3b6ebc49..33243952 100644 --- a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java +++ b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java @@ -33,7 +33,7 @@ import net.sourceforge.filebot.web.SearchResult; import net.sourceforge.tuned.ExceptionUtil; import net.sourceforge.tuned.ui.LabelProvider; import net.sourceforge.tuned.ui.SelectButtonTextField; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.swing.AutoCompleteSupport; @@ -86,7 +86,7 @@ public abstract class AbstractSearchPanel extends FileBotPanel { AutoCompleteSupport.install(searchTextField.getEditor(), searchHistory); - TunedUtil.putActionForKeystroke(this, KeyStroke.getKeyStroke("ENTER"), searchAction); + TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ENTER"), searchAction); } @@ -356,7 +356,7 @@ public abstract class AbstractSearchPanel extends FileBotPanel { protected void configureSelectDialog(SelectDialog selectDialog) { - selectDialog.setIconImage(TunedUtil.getImage(getIcon())); + selectDialog.setIconImage(TunedUtilities.getImage(getIcon())); } diff --git a/source/net/sourceforge/filebot/ui/FileBotList.java b/source/net/sourceforge/filebot/ui/FileBotList.java index 210a60fc..90a1ed1b 100644 --- a/source/net/sourceforge/filebot/ui/FileBotList.java +++ b/source/net/sourceforge/filebot/ui/FileBotList.java @@ -18,7 +18,7 @@ import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; import net.sourceforge.filebot.ui.transfer.TextFileExportHandler; import net.sourceforge.filebot.ui.transfer.TransferablePolicy; import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.swing.EventListModel; @@ -50,7 +50,7 @@ public class FileBotList extends JComponent { // Shortcut DELETE, disabled by default removeAction.setEnabled(false); - TunedUtil.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); + TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); } diff --git a/source/net/sourceforge/filebot/ui/FileBotPanel.java b/source/net/sourceforge/filebot/ui/FileBotPanel.java index 85320276..8be2f096 100644 --- a/source/net/sourceforge/filebot/ui/FileBotPanel.java +++ b/source/net/sourceforge/filebot/ui/FileBotPanel.java @@ -34,4 +34,10 @@ public class FileBotPanel extends JComponent { return null; } + + @Override + public String toString() { + return getPanelName(); + } + } diff --git a/source/net/sourceforge/filebot/ui/FileBotPanelSelectionList.java b/source/net/sourceforge/filebot/ui/FileBotPanelSelectionList.java index b99f6a74..6ecd462b 100644 --- a/source/net/sourceforge/filebot/ui/FileBotPanelSelectionList.java +++ b/source/net/sourceforge/filebot/ui/FileBotPanelSelectionList.java @@ -18,7 +18,7 @@ import javax.swing.Timer; import javax.swing.border.EmptyBorder; import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.swing.EventListModel; @@ -95,7 +95,7 @@ class FileBotPanelSelectionList extends JList { @Override public void dragEnter(DropTargetDragEvent dtde) { - dragEnterTimer = TunedUtil.invokeLater(SELECTDELAY_ON_DRAG_OVER, new Runnable() { + dragEnterTimer = TunedUtilities.invokeLater(SELECTDELAY_ON_DRAG_OVER, new Runnable() { @Override public void run() { diff --git a/source/net/sourceforge/filebot/ui/FileBotTabComponent.java b/source/net/sourceforge/filebot/ui/FileBotTabComponent.java index 922504ab..c3c5b122 100644 --- a/source/net/sourceforge/filebot/ui/FileBotTabComponent.java +++ b/source/net/sourceforge/filebot/ui/FileBotTabComponent.java @@ -14,7 +14,7 @@ import javax.swing.SwingConstants; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.ui.ProgressIndicator; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class FileBotTabComponent extends JComponent { @@ -57,7 +57,7 @@ public class FileBotTabComponent extends JComponent { public void setIcon(Icon icon) { iconLabel.setIcon(icon); - progressIndicator.setPreferredSize(icon != null ? TunedUtil.getDimension(icon) : progressIndicator.getMinimumSize()); + progressIndicator.setPreferredSize(icon != null ? TunedUtilities.getDimension(icon) : progressIndicator.getMinimumSize()); } @@ -88,7 +88,7 @@ public class FileBotTabComponent extends JComponent { JButton button = new JButton(icon); button.setRolloverIcon(rolloverIcon); - button.setPreferredSize(TunedUtil.getDimension(rolloverIcon)); + button.setPreferredSize(TunedUtilities.getDimension(rolloverIcon)); button.setMaximumSize(button.getPreferredSize()); button.setContentAreaFilled(false); diff --git a/source/net/sourceforge/filebot/ui/FileBotWindow.java b/source/net/sourceforge/filebot/ui/FileBotWindow.java index ecc1f398..b44ca156 100644 --- a/source/net/sourceforge/filebot/ui/FileBotWindow.java +++ b/source/net/sourceforge/filebot/ui/FileBotWindow.java @@ -2,14 +2,14 @@ package net.sourceforge.filebot.ui; -import static net.sourceforge.filebot.FileBotUtil.getApplicationName; +import static net.sourceforge.filebot.FileBotUtilities.asStringList; +import static net.sourceforge.filebot.Settings.getApplicationName; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Image; import java.util.ArrayList; import java.util.List; -import java.util.prefs.Preferences; import javax.swing.JComponent; import javax.swing.JFrame; @@ -22,6 +22,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.ui.panel.analyze.AnalyzePanel; import net.sourceforge.filebot.ui.panel.episodelist.EpisodeListPanel; import net.sourceforge.filebot.ui.panel.list.ListPanel; @@ -65,7 +66,7 @@ public class FileBotWindow extends JFrame implements ListSelectionListener { // restore the panel selection from last time, // switch to EpisodeListPanel by default (e.g. first start) - int selectedPanel = Preferences.userNodeForPackage(getClass()).getInt("selectedPanel", 3); + int selectedPanel = asStringList(panelSelectionList.getPanelModel()).indexOf(Settings.userRoot().get("selectedPanel")); panelSelectionList.setSelectedIndex(selectedPanel); // connect message handlers to message bus @@ -103,7 +104,7 @@ public class FileBotWindow extends JFrame implements ListSelectionListener { c.revalidate(); c.repaint(); - Preferences.userNodeForPackage(getClass()).putInt("selectedPanel", panelSelectionList.getSelectedIndex()); + Settings.userRoot().put("selectedPanel", panelSelectionList.getSelectedValue().toString()); } diff --git a/source/net/sourceforge/filebot/ui/HistoryPanel.java b/source/net/sourceforge/filebot/ui/HistoryPanel.java index b100dd38..6d938348 100644 --- a/source/net/sourceforge/filebot/ui/HistoryPanel.java +++ b/source/net/sourceforge/filebot/ui/HistoryPanel.java @@ -9,12 +9,13 @@ import java.util.ArrayList; import java.util.List; import javax.swing.Icon; +import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingConstants; import net.miginfocom.swing.MigLayout; -import net.sourceforge.tuned.ui.HyperlinkLabel; +import net.sourceforge.tuned.ui.LinkButton; public class HistoryPanel extends JPanel { @@ -63,19 +64,16 @@ public class HistoryPanel extends JPanel { public void add(String column1, URI link, Icon icon, String column2, String column3) { - JLabel label1 = (link != null) ? new HyperlinkLabel(column1, link) : new JLabel(column1); - JLabel label2 = new JLabel(column2, SwingConstants.RIGHT); - JLabel label3 = new JLabel(column3, SwingConstants.RIGHT); + JComponent c1 = link != null ? new LinkButton(column1, icon, link) : new JLabel(column1, icon, SwingConstants.LEFT); + JComponent c2 = new JLabel(column2, SwingConstants.RIGHT); + JComponent c3 = new JLabel(column3, SwingConstants.RIGHT); - label1.setIcon(icon); - label1.setIconTextGap(7); - - add(label1, "align left"); + add(c1, "align left"); // set minimum with to 100px so the text is aligned to the right, // even though the whole label is centered - add(label2, "align center, wmin 100"); + add(c2, "align center, wmin 100"); - add(label3, "align right"); + add(c3, "align right"); } } diff --git a/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java b/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java index 3e039bf7..9fc06699 100644 --- a/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java +++ b/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.ui; -import static net.sourceforge.filebot.FileBotUtil.getApplicationName; +import static net.sourceforge.filebot.Settings.getApplicationName; import java.util.logging.Handler; import java.util.logging.Level; @@ -35,26 +35,39 @@ public class NotificationLoggingHandler extends Handler { @Override - public void publish(final LogRecord record) { + public void publish(LogRecord record) { + final Level level = record.getLevel(); + final String message = getMessage(record); + SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - Level level = record.getLevel(); - if (level == Level.INFO) { - show(record.getMessage(), ResourceManager.getIcon("message.info"), timeout * 1); + show(message, ResourceManager.getIcon("message.info"), timeout * 1); } else if (level == Level.WARNING) { - show(record.getMessage(), ResourceManager.getIcon("message.warning"), timeout * 2); + show(message, ResourceManager.getIcon("message.warning"), timeout * 2); } else if (level == Level.SEVERE) { - show(record.getMessage(), ResourceManager.getIcon("message.error"), timeout * 3); + show(message, ResourceManager.getIcon("message.error"), timeout * 3); } } }); } - private void show(String message, Icon icon, int timeout) { + protected String getMessage(LogRecord record) { + String message = record.getMessage(); + + if (message == null || message.isEmpty()) { + // if message is empty, display exception string + message = record.getThrown().toString(); + } + + return message; + } + + + protected void show(String message, Icon icon, int timeout) { notificationManager.show(new MessageNotification(getApplicationName(), message, icon, timeout)); } diff --git a/source/net/sourceforge/filebot/ui/SelectDialog.java b/source/net/sourceforge/filebot/ui/SelectDialog.java index 74a8d73d..57ce713f 100644 --- a/source/net/sourceforge/filebot/ui/SelectDialog.java +++ b/source/net/sourceforge/filebot/ui/SelectDialog.java @@ -23,7 +23,7 @@ import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.ui.ArrayListModel; import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class SelectDialog extends JDialog { @@ -62,13 +62,13 @@ public class SelectDialog extends JDialog { // set default size and location setSize(new Dimension(210, 210)); - setLocation(TunedUtil.getPreferredLocation(this)); + setLocation(TunedUtilities.getPreferredLocation(this)); // Shortcut Enter - TunedUtil.putActionForKeystroke(list, KeyStroke.getKeyStroke("released ENTER"), selectAction); + TunedUtilities.putActionForKeystroke(list, KeyStroke.getKeyStroke("released ENTER"), selectAction); // Shortcut Escape - TunedUtil.putActionForKeystroke(list, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); + TunedUtilities.putActionForKeystroke(list, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); } diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java b/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java index bc3a7f17..f6ac1615 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java @@ -2,6 +2,8 @@ package net.sourceforge.filebot.ui.panel.analyze; +import static net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy.LOADING_PROPERTY; + import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -18,7 +20,7 @@ import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; import net.sourceforge.filebot.ui.transfer.LoadAction; import net.sourceforge.tuned.ui.LoadingOverlayPane; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; class FileTreePanel extends JComponent { @@ -38,10 +40,20 @@ class FileTreePanel extends JComponent { add(new JButton(loadAction)); add(new JButton(clearAction), "gap 1.2mm, wrap 1.2mm"); - transferablePolicy.addPropertyChangeListener("loading", loadingListener); + TunedUtilities.syncPropertyChangeEvents(boolean.class, LOADING_PROPERTY, transferablePolicy, this); + + // update tree when loading is finished + transferablePolicy.addPropertyChangeListener(new PropertyChangeListener() { + + public void propertyChange(PropertyChangeEvent evt) { + if (LOADING_PROPERTY.equals(evt.getPropertyName()) && !(Boolean) evt.getNewValue()) { + fireFileTreeChange(); + } + } + }); // Shortcut DELETE - TunedUtil.putActionForKeystroke(fileTree, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); + TunedUtilities.putActionForKeystroke(fileTree, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); } @@ -91,18 +103,4 @@ class FileTreePanel extends JComponent { firePropertyChange("filetree", null, fileTree); } - private final PropertyChangeListener loadingListener = new PropertyChangeListener() { - - public void propertyChange(PropertyChangeEvent evt) { - boolean loading = (Boolean) evt.getNewValue(); - - // relay loading property changes for loading overlay - firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); - - if (!loading) { - fireFileTreeChange(); - } - } - }; - } diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/FileTreeTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/analyze/FileTreeTransferablePolicy.java index 8cf66de3..b761e443 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/FileTreeTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/FileTreeTransferablePolicy.java @@ -9,7 +9,7 @@ import net.sourceforge.filebot.ui.panel.analyze.FileTree.AbstractTreeNode; import net.sourceforge.filebot.ui.panel.analyze.FileTree.FileNode; import net.sourceforge.filebot.ui.panel.analyze.FileTree.FolderNode; import net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; class FileTreeTransferablePolicy extends BackgroundFileTransferablePolicy { @@ -68,7 +68,7 @@ class FileTreeTransferablePolicy extends BackgroundFileTransferablePolicy implements ChangeListener { private long getSplitSize() { - return spinnerModel.getNumber().intValue() * FileUtil.MEGA; + return spinnerModel.getNumber().intValue() * FileUtilities.MEGA; } private FolderNode sourceModel = null; diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java b/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java index 4b2861c7..0b6e1441 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java @@ -2,9 +2,10 @@ package net.sourceforge.filebot.ui.panel.analyze; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; +import static net.sourceforge.tuned.ui.LoadingOverlayPane.LOADING_PROPERTY; + import java.io.File; +import java.util.ConcurrentModificationException; import java.util.List; import java.util.concurrent.Semaphore; import java.util.logging.Level; @@ -15,8 +16,9 @@ import javax.swing.SwingWorker; import net.sourceforge.filebot.ui.panel.analyze.FileTree.FileNode; import net.sourceforge.filebot.ui.panel.analyze.FileTree.FolderNode; -import net.sourceforge.tuned.FileUtil; -import net.sourceforge.tuned.ui.LoadingOverlayPane; +import net.sourceforge.tuned.ExceptionUtil; +import net.sourceforge.tuned.FileUtilities; +import net.sourceforge.tuned.ui.TunedUtilities; abstract class Tool extends JComponent { @@ -36,7 +38,10 @@ abstract class Tool extends JComponent { } updateTask = new UpdateModelTask(sourceModel); - updateTask.addPropertyChangeListener(loadingListener); + + // sync events for loading overlay + TunedUtilities.syncPropertyChangeEvents(boolean.class, LOADING_PROPERTY, updateTask, this); + updateTask.execute(); } @@ -66,9 +71,9 @@ abstract class Tool extends JComponent { M model = null; if (!isCancelled()) { - firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, false, true); + firePropertyChange(LOADING_PROPERTY, false, true); model = createModelInBackground(sourceModel); - firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, true, false); + firePropertyChange(LOADING_PROPERTY, true, false); } return model; @@ -85,8 +90,12 @@ abstract class Tool extends JComponent { try { setModel(get()); } catch (Exception e) { - // should not happen - Logger.getLogger("global").log(Level.WARNING, e.toString()); + if (ExceptionUtil.getRootCause(e) instanceof ConcurrentModificationException) { + // if it happens, it is supposed to + } else { + // should not happen + Logger.getLogger("global").log(Level.WARNING, e.toString(), e); + } } } } @@ -107,21 +116,9 @@ abstract class Tool extends JComponent { String numberOfFiles = String.format("%,d %s", files.size(), files.size() == 1 ? "file" : "files"); // set node text (e.g. txt (1 file, 42 Byte)) - folder.setTitle(String.format("%s (%s, %s)", name, numberOfFiles, FileUtil.formatSize(totalSize))); + folder.setTitle(String.format("%s (%s, %s)", name, numberOfFiles, FileUtilities.formatSize(totalSize))); return folder; } - private final PropertyChangeListener loadingListener = new PropertyChangeListener() { - - @Override - public void propertyChange(PropertyChangeEvent evt) { - // propagate loading events - if (evt.getPropertyName().equals(LoadingOverlayPane.LOADING_PROPERTY)) { - firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); - } - - } - }; - } diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java b/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java index bcbf6eb1..69bacc17 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java @@ -4,10 +4,12 @@ package net.sourceforge.filebot.ui.panel.analyze; import java.io.File; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.TreeMap; -import java.util.Map.Entry; +import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JScrollPane; @@ -17,7 +19,7 @@ import javax.swing.tree.TreeModel; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ui.panel.analyze.FileTree.FolderNode; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.ui.LoadingOverlayPane; @@ -42,11 +44,11 @@ public class TypeTool extends Tool { @Override protected TreeModel createModelInBackground(FolderNode sourceModel) throws InterruptedException { - TreeMap> map = new TreeMap>(); + Map> map = new HashMap>(); for (Iterator iterator = sourceModel.fileIterator(); iterator.hasNext();) { File file = iterator.next(); - String extension = FileUtil.getExtension(file); + String extension = FileUtilities.getExtension(file); List files = map.get(extension); @@ -58,10 +60,22 @@ public class TypeTool extends Tool { files.add(file); } + List keys = new ArrayList(map.keySet()); + + // sort strings like always, handle null as empty string + Collections.sort(keys, new Comparator() { + + @Override + public int compare(String s1, String s2) { + return ((s1 != null) ? s1 : "").compareTo((s2 != null) ? s2 : ""); + } + }); + + // create tree model FolderNode root = new FolderNode(); - for (Entry> entry : map.entrySet()) { - root.add(createStatisticsNode(entry.getKey(), entry.getValue())); + for (String key : keys) { + root.add(createStatisticsNode(key, map.get(key))); // unwind thread, if we have been cancelled if (Thread.interrupted()) { diff --git a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java index 6b9d9dca..2ecde261 100644 --- a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java @@ -3,6 +3,7 @@ package net.sourceforge.filebot.ui.panel.episodelist; import static net.sourceforge.filebot.ui.panel.episodelist.SeasonSpinnerModel.ALL_SEASONS; +import static net.sourceforge.filebot.web.Episode.formatEpisodeNumbers; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; @@ -21,6 +22,7 @@ import javax.swing.JSpinner; import javax.swing.KeyStroke; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.ui.AbstractSearchPanel; import net.sourceforge.filebot.ui.FileBotList; import net.sourceforge.filebot.ui.FileBotListExportHandler; @@ -38,7 +40,7 @@ import net.sourceforge.filebot.web.TheTVDBClient; import net.sourceforge.tuned.ui.LabelProvider; import net.sourceforge.tuned.ui.SelectButton; import net.sourceforge.tuned.ui.SimpleLabelProvider; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class EpisodeListPanel extends AbstractSearchPanel { @@ -65,8 +67,8 @@ public class EpisodeListPanel extends AbstractSearchPanel maxLength) { - maxLength = num.length(); - } - } - - // pad episode numbers with zeros (e.g. %02d) so all episode numbers have the same number of digits - String format = "%0" + maxLength + "d"; - for (Episode episode : episodes) { - - try { - episode.setEpisodeNumber(String.format(format, Integer.parseInt(episode.getEpisodeNumber()))); - } catch (NumberFormatException e) { - // ignore - } - } - - return episodes; + return formatEpisodeNumbers(episodes, 2); } diff --git a/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java index 42b031be..c881e85f 100644 --- a/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/list/FileListTransferablePolicy.java @@ -2,9 +2,9 @@ package net.sourceforge.filebot.ui.panel.list; -import static net.sourceforge.filebot.FileBotUtil.TORRENT_FILE_EXTENSIONS; -import static net.sourceforge.filebot.FileBotUtil.containsOnly; -import static net.sourceforge.filebot.FileBotUtil.containsOnlyFolders; +import static net.sourceforge.filebot.FileBotUtilities.TORRENT_FILES; +import static net.sourceforge.tuned.FileUtilities.FOLDERS; +import static net.sourceforge.tuned.FileUtilities.containsOnly; import java.io.File; import java.io.IOException; @@ -16,7 +16,7 @@ import java.util.logging.Logger; import net.sourceforge.filebot.torrent.Torrent; import net.sourceforge.filebot.ui.FileBotList; import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; class FileListTransferablePolicy extends FileTransferablePolicy { @@ -44,15 +44,15 @@ class FileListTransferablePolicy extends FileTransferablePolicy { @Override protected void load(List files) { // set title based on parent folder of first file - list.setTitle(FileUtil.getFolderName(files.get(0).getParentFile())); + list.setTitle(FileUtilities.getFolderName(files.get(0).getParentFile())); - if (containsOnlyFolders(files)) { + if (containsOnly(files, FOLDERS)) { loadFolders(files); - } else if (containsOnly(files, TORRENT_FILE_EXTENSIONS)) { + } else if (containsOnly(files, TORRENT_FILES)) { loadTorrents(files); } else { for (File file : files) { - list.getModel().add(FileUtil.getFileName(file)); + list.getModel().add(FileUtilities.getName(file)); } } } @@ -61,12 +61,12 @@ class FileListTransferablePolicy extends FileTransferablePolicy { private void loadFolders(List folders) { if (folders.size() == 1) { // if only one folder was dropped, use its name as title - list.setTitle(FileUtil.getFolderName(folders.get(0))); + list.setTitle(FileUtilities.getFolderName(folders.get(0))); } for (File folder : folders) { for (File file : folder.listFiles()) { - list.getModel().add(FileUtil.getFileName(file)); + list.getModel().add(FileUtilities.getName(file)); } } } @@ -81,12 +81,12 @@ class FileListTransferablePolicy extends FileTransferablePolicy { } if (torrentFiles.size() == 1) { - list.setTitle(FileUtil.getNameWithoutExtension(torrents.get(0).getName())); + list.setTitle(FileUtilities.getNameWithoutExtension(torrents.get(0).getName())); } for (Torrent torrent : torrents) { for (Torrent.Entry entry : torrent.getFiles()) { - list.getModel().add(FileUtil.getNameWithoutExtension(entry.getName())); + list.getModel().add(FileUtilities.getNameWithoutExtension(entry.getName())); } } } catch (IOException e) { diff --git a/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java b/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java index 83fa6b65..4211c058 100644 --- a/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java @@ -29,7 +29,7 @@ import net.sourceforge.filebot.ui.FileTransferableMessageHandler; import net.sourceforge.filebot.ui.transfer.LoadAction; import net.sourceforge.filebot.ui.transfer.SaveAction; import net.sourceforge.tuned.MessageHandler; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class ListPanel extends FileBotPanel { @@ -80,7 +80,7 @@ public class ListPanel extends FileBotPanel { list.add(buttonPanel, BorderLayout.SOUTH); - TunedUtil.putActionForKeystroke(this, KeyStroke.getKeyStroke("ENTER"), createAction); + TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ENTER"), createAction); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java b/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java new file mode 100644 index 00000000..c70fac99 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/AutoEpisodeListMatcher.java @@ -0,0 +1,124 @@ + +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.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javax.swing.SwingWorker; + +import net.sourceforge.filebot.similarity.Match; +import net.sourceforge.filebot.similarity.Matcher; +import net.sourceforge.filebot.similarity.SeriesNameMatcher; +import net.sourceforge.filebot.similarity.SimilarityMetric; +import net.sourceforge.filebot.web.Episode; +import net.sourceforge.filebot.web.EpisodeListClient; +import net.sourceforge.filebot.web.SearchResult; + + +class AutoEpisodeListMatcher extends SwingWorker>, Void> { + + private final List remainingFiles = new ArrayList(); + + private final List files; + + private final EpisodeListClient client; + + private final Collection metrics; + + + public AutoEpisodeListMatcher(EpisodeListClient client, List files, Collection metrics) { + this.client = client; + this.files = files; + this.metrics = metrics; + } + + + public Collection remainingFiles() { + return Collections.unmodifiableCollection(remainingFiles); + } + + + protected Collection matchSeriesNames(List episodes) { + int threshold = Math.min(episodes.size(), 5); + + return new SeriesNameMatcher(threshold).matchAll(asStringList(episodes)); + } + + + @Override + protected List> doInBackground() throws Exception { + List>> fetchTasks = new ArrayList>>(); + + // match series names and create episode list fetch tasks + for (final String seriesName : matchSeriesNames(files)) { + fetchTasks.add(new Callable>() { + + @Override + public Collection call() throws Exception { + Collection searchResults = client.search(seriesName); + + if (searchResults.isEmpty()) + return Collections.emptyList(); + + return formatEpisodeNumbers(client.getEpisodeList(searchResults.iterator().next()), 2); + } + }); + } + + if (fetchTasks.isEmpty()) { + throw new IllegalArgumentException("Failed to auto-detect series name."); + } + + // fetch episode lists concurrently + List episodeList = new ArrayList(); + ExecutorService executor = Executors.newFixedThreadPool(fetchTasks.size()); + + for (Future> future : executor.invokeAll(fetchTasks)) { + episodeList.addAll(future.get()); + } + + executor.shutdown(); + + List> matches = new ArrayList>(); + + for (List entryList : splitByFileType(files)) { + Matcher matcher = new Matcher(entryList, episodeList, metrics); + matches.addAll(matcher.match()); + remainingFiles.addAll(matcher.remainingValues()); + } + + return matches; + } + + + @SuppressWarnings("unchecked") + protected Collection> splitByFileType(Collection files) { + List subtitles = new ArrayList(); + List other = new ArrayList(); + + for (FileEntry file : files) { + // check for for subtitles first, then files in general + if (SUBTITLE_FILES.accept(file.getFile())) { + subtitles.add(file); + } else if (FILES.accept(file.getFile())) { + other.add(file); + } + } + + return Arrays.asList(other, subtitles); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java index b77b176f..da07a48f 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java @@ -4,7 +4,7 @@ package net.sourceforge.filebot.ui.panel.rename; import java.io.File; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; class FileEntry extends AbstractFileEntry { @@ -14,10 +14,10 @@ class FileEntry extends AbstractFileEntry { public FileEntry(File file) { - super(FileUtil.getFileName(file), file.length()); + super(FileUtilities.getName(file), file.length()); this.file = file; - this.type = FileUtil.getFileType(file); + this.type = FileUtilities.getType(file); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java index 389e2101..ac6da201 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java @@ -2,7 +2,8 @@ package net.sourceforge.filebot.ui.panel.rename; -import static net.sourceforge.filebot.FileBotUtil.containsOnlyFolders; +import static net.sourceforge.tuned.FileUtilities.FOLDERS; +import static net.sourceforge.tuned.FileUtilities.containsOnly; import java.io.File; import java.util.Arrays; @@ -35,7 +36,7 @@ class FilesListTransferablePolicy extends FileTransferablePolicy { @Override protected void load(List files) { - if (containsOnlyFolders(files)) { + if (containsOnly(files, FOLDERS)) { for (File folder : files) { loadFiles(Arrays.asList(folder.listFiles())); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/HighlightListCellRenderer.java b/source/net/sourceforge/filebot/ui/panel/rename/HighlightListCellRenderer.java index 777e0240..7359b33f 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/HighlightListCellRenderer.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/HighlightListCellRenderer.java @@ -20,7 +20,7 @@ import javax.swing.text.Highlighter; import javax.swing.text.JTextComponent; import net.sourceforge.tuned.ui.AbstractFancyListCellRenderer; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; class HighlightListCellRenderer extends AbstractFancyListCellRenderer { @@ -42,7 +42,7 @@ class HighlightListCellRenderer extends AbstractFancyListCellRenderer { textComponent.setBorder(new EmptyBorder(padding, padding, padding, padding)); // make text component transparent, should work for all LAFs (setOpaque(false) may not, e.g. Nimbus) - textComponent.setBackground(TunedUtil.TRANSLUCENT); + textComponent.setBackground(TunedUtilities.TRANSLUCENT); this.add(textComponent, BorderLayout.WEST); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java index 0eea901b..2a57a3a9 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java @@ -7,6 +7,8 @@ import java.awt.Window; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -26,6 +28,8 @@ import net.sourceforge.filebot.similarity.Matcher; import net.sourceforge.filebot.similarity.NameSimilarityMetric; import net.sourceforge.filebot.similarity.SeasonEpisodeSimilarityMetric; import net.sourceforge.filebot.similarity.SimilarityMetric; +import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE; +import net.sourceforge.filebot.web.Episode; import net.sourceforge.tuned.ui.ProgressDialog; import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter; import net.sourceforge.tuned.ui.ProgressDialog.Cancellable; @@ -35,17 +39,21 @@ class MatchAction extends AbstractAction { private final RenameModel model; - private final SimilarityMetric[] metrics; + private final Collection metrics; public MatchAction(RenameModel model) { super("Match", ResourceManager.getIcon("action.match")); - putValue(SHORT_DESCRIPTION, "Match names to files"); - this.model = model; + this.metrics = createMetrics(); - metrics = new SimilarityMetric[3]; + putValue(SHORT_DESCRIPTION, "Match names to files"); + } + + + protected Collection createMetrics() { + SimilarityMetric[] metrics = new SimilarityMetric[3]; // 1. pass: match by file length (fast, but only works when matching torrents or files) metrics[0] = new LengthEqualsMetric() { @@ -61,19 +69,48 @@ class MatchAction extends AbstractAction { }; // 2. pass: match by season / episode numbers, or generic numeric similarity - metrics[1] = new SeasonEpisodeSimilarityMetric(); + metrics[1] = new SeasonEpisodeSimilarityMetric() { + + @Override + protected Collection parse(Object o) { + if (o instanceof Episode) { + Episode episode = (Episode) o; + + try { + // create SxE from episode + return Collections.singleton(new SxE(episode.getSeasonNumber(), episode.getEpisodeNumber())); + } catch (NumberFormatException e) { + // some kind of special episode, no SxE + return null; + } + } + + return super.parse(o); + } + }; // 3. pass: match by generic name similarity (slow, but most matches will have been determined in second pass) metrics[2] = new NameSimilarityMetric(); + + return Arrays.asList(metrics); + } + + + public Collection getMetrics() { + return Collections.unmodifiableCollection(metrics); } public void actionPerformed(ActionEvent evt) { + if (model.names().isEmpty() || model.files().isEmpty()) { + return; + } + JComponent eventSource = (JComponent) evt.getSource(); SwingUtilities.getRoot(eventSource).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - BackgroundMatcher backgroundMatcher = new BackgroundMatcher(model, Arrays.asList(metrics)); + BackgroundMatcher backgroundMatcher = new BackgroundMatcher(model, metrics); backgroundMatcher.execute(); try { @@ -114,16 +151,12 @@ class MatchAction extends AbstractAction { } - protected static class BackgroundMatcher extends SwingWorker>, Void> implements Cancellable { - - private final RenameModel model; + protected class BackgroundMatcher extends SwingWorker>, Void> implements Cancellable { private final Matcher matcher; - public BackgroundMatcher(RenameModel model, List metrics) { - this.model = model; - + public BackgroundMatcher(RenameModel model, Collection metrics) { // match names against files this.matcher = new Matcher(model.names(), model.files(), metrics); } @@ -141,8 +174,15 @@ class MatchAction extends AbstractAction { return; try { + List> matches = get(); + + model.clear(); + // put new data into model - model.setData(get()); + for (Match match : matches) { + model.names().add(match.getValue()); + model.files().add(match.getCandidate()); + } // insert objects that could not be matched at the end model.names().addAll(matcher.remainingValues()); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java index e05eef22..dcad926b 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java @@ -3,12 +3,12 @@ package net.sourceforge.filebot.ui.panel.rename; import static java.awt.datatransfer.DataFlavor.stringFlavor; -import static net.sourceforge.filebot.FileBotUtil.LIST_FILE_EXTENSIONS; -import static net.sourceforge.filebot.FileBotUtil.TORRENT_FILE_EXTENSIONS; -import static net.sourceforge.filebot.FileBotUtil.containsOnly; -import static net.sourceforge.filebot.FileBotUtil.containsOnlyFolders; -import static net.sourceforge.filebot.FileBotUtil.isInvalidFileName; -import static net.sourceforge.tuned.FileUtil.getNameWithoutExtension; +import static net.sourceforge.filebot.FileBotUtilities.LIST_FILES; +import static net.sourceforge.filebot.FileBotUtilities.TORRENT_FILES; +import static net.sourceforge.filebot.FileBotUtilities.isInvalidFileName; +import static net.sourceforge.tuned.FileUtilities.FOLDERS; +import static net.sourceforge.tuned.FileUtilities.containsOnly; +import static net.sourceforge.tuned.FileUtilities.getNameWithoutExtension; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; @@ -25,7 +25,7 @@ import javax.swing.SwingUtilities; import net.sourceforge.filebot.torrent.Torrent; import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; class NamesListTransferablePolicy extends FileTransferablePolicy { @@ -121,11 +121,11 @@ class NamesListTransferablePolicy extends FileTransferablePolicy { @Override protected void load(List files) { - if (containsOnly(files, LIST_FILE_EXTENSIONS)) { + if (containsOnly(files, LIST_FILES)) { loadListFiles(files); - } else if (containsOnly(files, TORRENT_FILE_EXTENSIONS)) { + } else if (containsOnly(files, TORRENT_FILES)) { loadTorrentFiles(files); - } else if (containsOnlyFolders(files)) { + } else if (containsOnly(files, FOLDERS)) { // load files from each folder for (File folder : files) { loadFiles(Arrays.asList(folder.listFiles())); @@ -138,7 +138,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy { protected void loadFiles(List files) { for (File file : files) { - list.getModel().add(new AbstractFileEntry(FileUtil.getFileName(file), file.length())); + list.getModel().add(new AbstractFileEntry(FileUtilities.getName(file), file.length())); } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java index 0165cb1f..0cca5b10 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java @@ -13,7 +13,7 @@ import javax.swing.AbstractAction; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.similarity.Match; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; class RenameAction extends AbstractAction { @@ -38,8 +38,16 @@ class RenameAction extends AbstractAction { for (Match match : model.matches()) { File source = match.getCandidate().getFile(); - String newName = match.getValue().toString() + FileUtil.getExtension(source, true); - File target = new File(source.getParentFile(), newName); + String extension = FileUtilities.getExtension(source); + + StringBuilder nameBuilder = new StringBuilder(); + nameBuilder.append(match.getValue()); + + if (extension != null) { + nameBuilder.append(".").append(extension); + } + + File target = new File(source.getParentFile(), nameBuilder.toString()); todoQueue.addLast(new Match(source, target)); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java index 7589085c..080aed65 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java @@ -28,6 +28,7 @@ class RenameList extends FileBotList { setModel(model); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setFixedCellHeight(28); list.addMouseListener(dndReorderMouseAdapter); list.addMouseMotionListener(dndReorderMouseAdapter); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java index 1a32860c..38a103ee 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java @@ -8,12 +8,13 @@ import java.util.Collection; import net.sourceforge.filebot.similarity.Match; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; +import ca.odell.glazedlists.GlazedLists; class RenameModel { - private final EventList names = new BasicEventList(); - private final EventList files = new BasicEventList(); + private final EventList names = GlazedLists.threadSafeList(new BasicEventList()); + private final EventList files = GlazedLists.threadSafeList(new BasicEventList()); public EventList names() { @@ -32,18 +33,6 @@ class RenameModel { } - public void setData(Collection> matches) { - // clear names and files - clear(); - - // add all matches - for (Match match : matches) { - names.add(match.getValue()); - files.add(match.getCandidate()); - } - } - - public int matchCount() { return Math.min(names.size(), files.size()); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java index be5f7f5f..a5cd2ebb 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java @@ -2,16 +2,37 @@ package net.sourceforge.filebot.ui.panel.rename; +import static javax.swing.SwingUtilities.getWindowAncestor; +import static net.sourceforge.tuned.ui.LoadingOverlayPane.LOADING_PROPERTY; +import static net.sourceforge.filebot.FileBotUtilities.*; import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.AbstractAction; +import javax.swing.Action; import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; +import javax.swing.JComponent; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.Settings; +import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.ui.FileBotPanel; +import net.sourceforge.filebot.web.AnidbClient; +import net.sourceforge.filebot.web.Episode; +import net.sourceforge.filebot.web.EpisodeListClient; +import net.sourceforge.filebot.web.TVRageClient; +import net.sourceforge.filebot.web.TheTVDBClient; +import net.sourceforge.tuned.ExceptionUtil; +import net.sourceforge.tuned.ui.ActionPopup; +import net.sourceforge.tuned.ui.LoadingOverlayPane; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; @@ -28,6 +49,8 @@ public class RenamePanel extends FileBotPanel { private RenameAction renameAction = new RenameAction(model); + private ActionPopup matchActionPopup = new ActionPopup("Fetch Episode List", ResourceManager.getIcon("action.match.small")); + public RenamePanel() { super("Rename", ResourceManager.getIcon("panel.rename")); @@ -63,9 +86,18 @@ public class RenamePanel extends FileBotPanel { renameButton.setVerticalTextPosition(SwingConstants.BOTTOM); renameButton.setHorizontalTextPosition(SwingConstants.CENTER); + // create actions for match popup + matchActionPopup.add(new AutoFetchEpisodeListAction(new TVRageClient())); + matchActionPopup.add(new AutoFetchEpisodeListAction(new AnidbClient())); + matchActionPopup.add(new AutoFetchEpisodeListAction(new TheTVDBClient(Settings.userRoot().get("thetvdb.apikey")))); + + // set match action popup + matchButton.setComponentPopupMenu(matchActionPopup); + matchButton.addActionListener(showPopupAction); + setLayout(new MigLayout("fill, insets dialog, gapx 10px", null, "align 33%")); - add(namesList, "grow"); + add(new LoadingOverlayPane(namesList, this, "28px", "30px"), "grow, sizegroupx list"); // make buttons larger matchButton.setMargin(new Insets(3, 14, 2, 14)); @@ -74,15 +106,115 @@ public class RenamePanel extends FileBotPanel { add(matchButton, "split 2, flowy, sizegroupx button"); add(renameButton, "gapy 30px, sizegroupx button"); - add(filesList, "grow"); + add(filesList, "grow, sizegroupx list"); // repaint on change model.names().addListEventListener(new RepaintHandler()); model.files().addListEventListener(new RepaintHandler()); } + protected final Action showPopupAction = new AbstractAction("Show Popup") { + + @Override + public void actionPerformed(ActionEvent e) { + // show popup on actionPerformed only when names list is empty + if (model.names().isEmpty()) { + JComponent source = (JComponent) e.getSource(); + + // display popup below component + source.getComponentPopupMenu().show(source, -3, source.getHeight() + 4); + } + } + }; - private class RepaintHandler implements ListEventListener { + private boolean autoMatchInProgress = false; + + + protected void setAutoMatchInProgress(boolean flag) { + this.autoMatchInProgress = flag; + firePropertyChange(LOADING_PROPERTY, !flag, flag); + matchActionPopup.setStatus(flag ? "in progress" : null); + } + + + protected boolean isAutoMatchInProgress() { + return autoMatchInProgress; + } + + + protected class AutoFetchEpisodeListAction extends AbstractAction { + + private final EpisodeListClient client; + + + public AutoFetchEpisodeListAction(EpisodeListClient client) { + super(client.getName(), client.getIcon()); + + this.client = client; + } + + + @Override + public void actionPerformed(ActionEvent evt) { + if (model.files().isEmpty() || isAutoMatchInProgress()) + return; + + AutoEpisodeListMatcher worker = new AutoEpisodeListMatcher(client, new ArrayList(model.files()), matchAction.getMetrics()) { + + @Override + protected void done() { + // background worker is finished + setAutoMatchInProgress(false); + + try { + List names = new ArrayList(); + List files = new ArrayList(); + + List invalidNames = new ArrayList(); + + for (Match match : get()) { + StringEntry name = new StringEntry(match.getCandidate()); + + if (isInvalidFileName(name.toString())) { + invalidNames.add(name); + } + + names.add(new StringEntry(name)); + files.add(match.getValue()); + } + + if (!invalidNames.isEmpty()) { + ValidateNamesDialog dialog = new ValidateNamesDialog(getWindowAncestor(RenamePanel.this), invalidNames); + dialog.setVisible(true); + + if (dialog.isCancelled()) { + // don't touch model + return; + } + } + + model.clear(); + + model.names().addAll(names); + model.files().addAll(files); + + // add remaining file entries again + model.files().addAll(remainingFiles()); + } catch (Exception e) { + Logger.getLogger("ui").log(Level.WARNING, ExceptionUtil.getRootCause(e).getMessage(), e); + } + } + }; + + worker.execute(); + + // background worker started + setAutoMatchInProgress(true); + } + } + + + protected class RepaintHandler implements ListEventListener { @Override public void listChanged(ListEvent listChanges) { diff --git a/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java index 8139d70b..2f340652 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java @@ -7,8 +7,8 @@ class StringEntry { private String value; - public StringEntry(String value) { - this.value = value; + public StringEntry(Object value) { + setValue(value); } @@ -17,8 +17,8 @@ class StringEntry { } - public void setValue(String value) { - this.value = value; + public void setValue(Object value) { + this.value = String.valueOf(value); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java index ebd78963..f0d8d736 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java @@ -2,8 +2,8 @@ package net.sourceforge.filebot.ui.panel.rename; -import static net.sourceforge.filebot.FileBotUtil.INVALID_CHARACTERS_PATTERN; -import static net.sourceforge.filebot.FileBotUtil.validateFileName; +import static net.sourceforge.filebot.FileBotUtilities.INVALID_CHARACTERS_PATTERN; +import static net.sourceforge.filebot.FileBotUtilities.validateFileName; import java.awt.AlphaComposite; import java.awt.Color; @@ -26,7 +26,7 @@ import javax.swing.KeyStroke; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.ui.ArrayListModel; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; class ValidateNamesDialog extends JDialog { @@ -67,7 +67,7 @@ class ValidateNamesDialog extends JDialog { setSize(365, 280); - TunedUtil.putActionForKeystroke(c, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); + TunedUtilities.putActionForKeystroke(c, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); } diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java index 87be2caa..e2d88681 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java @@ -7,7 +7,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import net.sourceforge.filebot.FileBotUtil; +import net.sourceforge.filebot.FileBotUtilities; public class ChecksumRow { @@ -50,7 +50,7 @@ public class ChecksumRow { */ private static Long getEmbeddedChecksum(String name) { // look for a checksum pattern like [49A93C5F] - String match = FileBotUtil.getEmbeddedChecksum(name); + String match = FileBotUtilities.getEmbeddedChecksum(name); if (match != null) return Long.parseLong(match, 16); diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java index b0b2090b..77cd5ad1 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java @@ -11,7 +11,7 @@ import java.util.Map; import java.util.Map.Entry; import net.sourceforge.filebot.ui.transfer.TextFileExportHandler; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; public class ChecksumTableExportHandler extends TextFileExportHandler { @@ -75,7 +75,7 @@ public class ChecksumTableExportHandler extends TextFileExportHandler { String name = ""; if (column != null) - name = FileUtil.getFileName(column); + name = FileUtilities.getName(column); if (name.isEmpty()) name = "name"; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java index e43f0f05..52e54faf 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java @@ -16,7 +16,7 @@ import javax.swing.event.TableModelEvent; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableModel; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; class ChecksumTableModel extends AbstractTableModel { @@ -48,7 +48,7 @@ class ChecksumTableModel extends AbstractTableModel { File column = columns.get(columnIndex - checksumColumnOffset); // works for files too and simply returns the name unchanged - return FileUtil.getFolderName(column); + return FileUtilities.getFolderName(column); } return null; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java b/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java index 7a570fe1..52c30cfd 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java @@ -22,9 +22,9 @@ import net.sourceforge.filebot.ui.FileTransferableMessageHandler; import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.ui.transfer.LoadAction; import net.sourceforge.filebot.ui.transfer.SaveAction; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.MessageHandler; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class SfvPanel extends FileBotPanel { @@ -54,7 +54,7 @@ public class SfvPanel extends FileBotPanel { contentPane.add(totalProgressPanel, "gap left indent:push, gap bottom 2px, gap right 7px, hidemode 3"); // Shortcut DELETE - TunedUtil.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); + TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); } @@ -150,7 +150,7 @@ public class SfvPanel extends FileBotPanel { @Override protected String convertValueToString(Object value) { - return FileUtil.getFolderName((File) value); + return FileUtilities.getFolderName((File) value); } }; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/SfvTable.java b/source/net/sourceforge/filebot/ui/panel/sfv/SfvTable.java index b86ea04a..d55cc7a8 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/SfvTable.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/SfvTable.java @@ -13,7 +13,7 @@ import javax.swing.plaf.basic.BasicTableUI; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; -import net.sourceforge.filebot.FileBotUtil; +import net.sourceforge.filebot.FileBotUtilities; import net.sourceforge.filebot.ui.panel.sfv.ChecksumTableModel.ChecksumTableModelEvent; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; @@ -46,7 +46,7 @@ class SfvTable extends JTable { setUI(new DragDropRowTableUI()); // highlight CRC32 patterns in filenames in green and with smaller font-size - setDefaultRenderer(String.class, new HighlightPatternCellRenderer(FileBotUtil.EMBEDDED_CHECKSUM_PATTERN, "#009900", "smaller")); + setDefaultRenderer(String.class, new HighlightPatternCellRenderer(FileBotUtilities.EMBEDDED_CHECKSUM_PATTERN, "#009900", "smaller")); setDefaultRenderer(ChecksumRow.State.class, new StateIconTableCellRenderer()); setDefaultRenderer(Checksum.class, new ChecksumTableCellRenderer()); } diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/SfvTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/sfv/SfvTransferablePolicy.java index cb3a85b8..31e85d1c 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/SfvTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/SfvTransferablePolicy.java @@ -2,8 +2,8 @@ package net.sourceforge.filebot.ui.panel.sfv; -import static net.sourceforge.filebot.FileBotUtil.SFV_FILE_EXTENSIONS; -import static net.sourceforge.filebot.FileBotUtil.containsOnly; +import static net.sourceforge.filebot.FileBotUtilities.SFV_FILES; +import static net.sourceforge.tuned.FileUtilities.containsOnly; import java.io.BufferedReader; import java.io.File; @@ -96,7 +96,7 @@ class SfvTransferablePolicy extends BackgroundFileTransferablePolicy files) { try { - if (containsOnly(files, SFV_FILE_EXTENSIONS)) { + if (containsOnly(files, SFV_FILES)) { // one or more sfv files for (File file : files) { loadSfvFile(file); diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/TotalProgressPanel.java b/source/net/sourceforge/filebot/ui/panel/sfv/TotalProgressPanel.java index d4ffc9d4..6eb67647 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/TotalProgressPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/TotalProgressPanel.java @@ -10,7 +10,7 @@ import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JProgressBar; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; class TotalProgressPanel extends Box { @@ -51,7 +51,7 @@ class TotalProgressPanel extends Box { Boolean active = (Boolean) evt.getNewValue(); if (active) { - TunedUtil.invokeLater(millisToSetVisible, new Runnable() { + TunedUtilities.invokeLater(millisToSetVisible, new Runnable() { @Override public void run() { diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageSelectionPanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageSelectionPanel.java deleted file mode 100644 index 0425c26f..00000000 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageSelectionPanel.java +++ /dev/null @@ -1,180 +0,0 @@ - -package net.sourceforge.filebot.ui.panel.subtitle; - - -import java.awt.AlphaComposite; -import java.awt.Cursor; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.util.Map; -import java.util.TreeMap; - -import javax.swing.JPanel; -import javax.swing.JToggleButton; - -import ca.odell.glazedlists.EventList; -import ca.odell.glazedlists.FunctionList; -import ca.odell.glazedlists.ListSelection; -import ca.odell.glazedlists.UniqueList; -import ca.odell.glazedlists.FunctionList.Function; -import ca.odell.glazedlists.event.ListEvent; -import ca.odell.glazedlists.event.ListEventListener; - - -public class LanguageSelectionPanel extends JPanel { - - private final ListSelection selectionModel; - - private final Map defaultSelection = new TreeMap(String.CASE_INSENSITIVE_ORDER); - - - // private final Map globalSelection = Settings.getSettings().asBooleanMap(Settings.SUBTITLE_LANGUAGE); - - public LanguageSelectionPanel(EventList source) { - super(new FlowLayout(FlowLayout.RIGHT, 5, 1)); - - // defaultSelection.putAll(globalSelection); - - EventList languageList = new FunctionList(source, new LanguageFunction()); - EventList languageSet = new UniqueList(languageList); - - selectionModel = new ListSelection(languageSet); - - selectionModel.getSource().addListEventListener(new SourceChangeHandler()); - } - - - public EventList getSelected() { - return selectionModel.getSelected(); - } - - - private boolean isSelectedByDefault(Language language) { - Boolean selected = defaultSelection.get(language.getName()); - - if (selected != null) - return selected; - - // deselected by default - return false; - } - - - private void setSelected(Language language, boolean selected) { - String key = language.getName(); - - defaultSelection.put(key, selected); - // globalSelection.put(key, selected); - - if (selected) - selectionModel.select(language); - else - selectionModel.deselect(selectionModel.getSource().indexOf(language)); - } - - - /** - * Provide the binding between this panel and the source {@link EventList}. - */ - private class SourceChangeHandler implements ListEventListener { - - /** - * Handle an inserted element. - */ - private void insert(int index) { - Language language = selectionModel.getSource().get(index); - - LanguageToggleButton button = new LanguageToggleButton(language); - button.setSelected(isSelectedByDefault(language)); - - add(button, index); - } - - - /** - * Handle a deleted element. - */ - private void delete(int index) { - remove(index); - } - - - /** - * When the components list changes, this updates the panel. - */ - public void listChanged(ListEvent listChanges) { - while (listChanges.next()) { - int type = listChanges.getType(); - int index = listChanges.getIndex(); - - if (type == ListEvent.INSERT) { - insert(index); - } else if (type == ListEvent.DELETE) { - delete(index); - } - } - - // repaint the panel - revalidate(); - repaint(); - } - } - - - private class LanguageToggleButton extends JToggleButton implements ItemListener { - - private final Language language; - - - public LanguageToggleButton(Language language) { - super(language.getIcon()); - - this.language = language; - - setToolTipText(language.getName()); - setContentAreaFilled(false); - setFocusPainted(false); - - setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - setPreferredSize(new Dimension(getIcon().getIconWidth(), getIcon().getIconHeight())); - - addItemListener(this); - } - - - @Override - protected void paintComponent(Graphics g) { - Graphics2D g2d = (Graphics2D) g; - - // make transparent if not selected - if (!isSelected()) { - AlphaComposite composite = AlphaComposite.SrcOver.derive(0.2f); - g2d.setComposite(composite); - } - - super.paintComponent(g2d); - } - - - @Override - public void itemStateChanged(ItemEvent e) { - LanguageSelectionPanel.this.setSelected(language, isSelected()); - } - - } - - - private class LanguageFunction implements Function { - - @Override - public Language evaluate(SubtitlePackage sourceValue) { - return sourceValue.getLanguage(); - } - - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleCellRenderer.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleCellRenderer.java index 8a3ec1b2..59e9c1ce 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleCellRenderer.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleCellRenderer.java @@ -14,7 +14,7 @@ import javax.swing.SwingConstants; import net.sourceforge.tuned.ui.ColorTintImageFilter; import net.sourceforge.tuned.ui.IconViewCellRenderer; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class SubtitleCellRenderer extends IconViewCellRenderer { @@ -56,7 +56,7 @@ public class SubtitleCellRenderer extends IconViewCellRenderer { Icon icon = subtitle.getArchiveIcon(); if (isSelected) { - setIcon(new ImageIcon(createImage(new FilteredImageSource(TunedUtil.getImage(icon).getSource(), new ColorTintImageFilter(list.getSelectionBackground(), 0.5f))))); + setIcon(new ImageIcon(createImage(new FilteredImageSource(TunedUtilities.getImage(icon).getSource(), new ColorTintImageFilter(list.getSelectionBackground(), 0.5f))))); info1.setForeground(list.getSelectionForeground()); info2.setForeground(list.getSelectionForeground()); diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadPanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadPanel.java index 6c7485d0..62931b81 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadPanel.java @@ -4,10 +4,10 @@ package net.sourceforge.filebot.ui.panel.subtitle; import java.awt.BorderLayout; -import javax.swing.JPanel; +import javax.swing.JComponent; -public class SubtitleDownloadPanel extends JPanel { +public class SubtitleDownloadPanel extends JComponent { private final SubtitlePackagePanel packagePanel = new SubtitlePackagePanel(); diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackagePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackagePanel.java index e1f5fb55..e23c96cd 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackagePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackagePanel.java @@ -3,20 +3,22 @@ package net.sourceforge.filebot.ui.panel.subtitle; import java.awt.BorderLayout; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.EventListener; +import javax.swing.JComponent; import javax.swing.JList; -import javax.swing.JPanel; import javax.swing.JScrollPane; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.FilterList; -import ca.odell.glazedlists.GlazedLists; import ca.odell.glazedlists.ObservableElementList; import ca.odell.glazedlists.swing.EventListModel; -public class SubtitlePackagePanel extends JPanel { +public class SubtitlePackagePanel extends JComponent { private final EventList model = new BasicEventList(); @@ -24,7 +26,7 @@ public class SubtitlePackagePanel extends JPanel { public SubtitlePackagePanel() { - super(new BorderLayout()); + setLayout(new BorderLayout()); add(languageSelection, BorderLayout.NORTH); add(new JScrollPane(createList()), BorderLayout.CENTER); } @@ -37,13 +39,58 @@ public class SubtitlePackagePanel extends JPanel { protected JList createList() { FilterList filterList = new FilterList(model, new LanguageMatcherEditor(languageSelection)); - ObservableElementList observableList = new ObservableElementList(filterList, GlazedLists.beanConnector(SubtitlePackage.class)); + ObservableElementList observableList = new ObservableElementList(filterList, new SubtitlePackageConnector()); JList list = new JList(new EventListModel(observableList)); return list; } + + private static class SubtitlePackageConnector implements ObservableElementList.Connector { + + /** + * The list which contains the elements being observed via this + * {@link ObservableElementList.Connector}. + */ + private ObservableElementList list = null; + + + public EventListener installListener(SubtitlePackage element) { + PropertyChangeListener listener = new SubtitlePackageListener(element); + element.getDownloadTask().addPropertyChangeListener(listener); + + return listener; + } + + + public void uninstallListener(SubtitlePackage element, EventListener listener) { + element.getDownloadTask().removePropertyChangeListener((PropertyChangeListener) listener); + } + + + public void setObservableElementList(ObservableElementList list) { + this.list = list; + } + + + private class SubtitlePackageListener implements PropertyChangeListener { + + private final SubtitlePackage subtitlePackage; + + + public SubtitlePackageListener(SubtitlePackage subtitlePackage) { + this.subtitlePackage = subtitlePackage; + } + + + public void propertyChange(PropertyChangeEvent evt) { + list.elementChanged(subtitlePackage); + } + } + + } + /* private void updateLanguageFilterButtonPanel() { diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java index fd4a3ef5..614307d6 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java @@ -2,17 +2,17 @@ package net.sourceforge.filebot.ui.panel.subtitle; -import static net.sourceforge.filebot.FileBotUtil.getApplicationName; -import static net.sourceforge.filebot.FileBotUtil.getApplicationVersion; +import static net.sourceforge.filebot.Settings.getApplicationName; +import static net.sourceforge.filebot.Settings.getApplicationVersion; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; -import java.util.prefs.Preferences; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.ui.AbstractSearchPanel; import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.web.OpenSubtitlesSubtitleClient; @@ -21,7 +21,6 @@ import net.sourceforge.filebot.web.SubsceneSubtitleClient; import net.sourceforge.filebot.web.SubtitleClient; import net.sourceforge.filebot.web.SubtitleDescriptor; import net.sourceforge.tuned.ListChangeSynchronizer; -import net.sourceforge.tuned.PreferencesList; import net.sourceforge.tuned.ui.LabelProvider; import net.sourceforge.tuned.ui.SimpleLabelProvider; @@ -35,10 +34,8 @@ public class SubtitlePanel extends AbstractSearchPanel persistentSearchHistory = PreferencesList.map(historyNode, String.class); + // and get a StringList that read and writes directly from and to the preferences + List persistentSearchHistory = Settings.userRoot().node("subtitles/history").asList(String.class); // add history from the preferences to the current in-memory history (for completion) getSearchHistory().addAll(persistentSearchHistory); diff --git a/source/net/sourceforge/filebot/ui/transfer/BackgroundFileTransferablePolicy.java b/source/net/sourceforge/filebot/ui/transfer/BackgroundFileTransferablePolicy.java index c7487264..33ba611a 100644 --- a/source/net/sourceforge/filebot/ui/transfer/BackgroundFileTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/transfer/BackgroundFileTransferablePolicy.java @@ -125,12 +125,12 @@ public abstract class BackgroundFileTransferablePolicy extends FileTransferab private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); - public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { - propertyChangeSupport.addPropertyChangeListener(propertyName, listener); + public void addPropertyChangeListener(PropertyChangeListener listener) { + propertyChangeSupport.addPropertyChangeListener(listener); } - public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { - propertyChangeSupport.removePropertyChangeListener(propertyName, listener); + public void removePropertyChangeListener(PropertyChangeListener listener) { + propertyChangeSupport.removePropertyChangeListener(listener); } } diff --git a/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java b/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java index 68eae34c..a7fef7c0 100644 --- a/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java @@ -10,6 +10,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Scanner; import java.util.logging.Level; @@ -29,7 +30,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy { public boolean accept(Transferable tr) { List files = getFilesFromTransferable(tr); - if (files == null || files.isEmpty()) + if (files.isEmpty()) return false; return accept(files); @@ -81,7 +82,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy { throw new RuntimeException(e); } - return null; + return Collections.emptyList(); } diff --git a/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java b/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java index 5c961626..d9d32b2c 100644 --- a/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java +++ b/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java @@ -2,8 +2,8 @@ package net.sourceforge.filebot.ui.transfer; -import static net.sourceforge.filebot.FileBotUtil.getApplicationName; -import static net.sourceforge.filebot.FileBotUtil.validateFileName; +import static net.sourceforge.filebot.FileBotUtilities.validateFileName; +import static net.sourceforge.filebot.Settings.getApplicationName; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; diff --git a/source/net/sourceforge/filebot/ui/transfer/SaveAction.java b/source/net/sourceforge/filebot/ui/transfer/SaveAction.java index 27651513..1f7a0e5d 100644 --- a/source/net/sourceforge/filebot/ui/transfer/SaveAction.java +++ b/source/net/sourceforge/filebot/ui/transfer/SaveAction.java @@ -12,7 +12,7 @@ import javax.swing.AbstractAction; import javax.swing.JComponent; import javax.swing.JFileChooser; -import net.sourceforge.filebot.FileBotUtil; +import net.sourceforge.filebot.FileBotUtilities; import net.sourceforge.filebot.ResourceManager; @@ -60,7 +60,7 @@ public class SaveAction extends AbstractAction { chooser.setMultiSelectionEnabled(false); - chooser.setSelectedFile(new File(getDefaultFolder(), FileBotUtil.validateFileName(getDefaultFileName()))); + chooser.setSelectedFile(new File(getDefaultFolder(), FileBotUtilities.validateFileName(getDefaultFileName()))); if (chooser.showSaveDialog((JComponent) evt.getSource()) != JFileChooser.APPROVE_OPTION) return; diff --git a/source/net/sourceforge/filebot/web/Episode.java b/source/net/sourceforge/filebot/web/Episode.java index edb0679a..dca01625 100644 --- a/source/net/sourceforge/filebot/web/Episode.java +++ b/source/net/sourceforge/filebot/web/Episode.java @@ -7,22 +7,22 @@ import java.io.Serializable; public class Episode implements Serializable { - private String showName; + private String seriesName; private String seasonNumber; private String episodeNumber; private String title; - public Episode(String showName, String seasonNumber, String episodeNumber, String title) { - this.showName = showName; + public Episode(String seriesName, String seasonNumber, String episodeNumber, String title) { + this.seriesName = seriesName; this.seasonNumber = seasonNumber; this.episodeNumber = episodeNumber; this.title = title; } - public Episode(String showName, String episodeNumber, String title) { - this(showName, null, episodeNumber, title); + public Episode(String seriesName, String episodeNumber, String title) { + this(seriesName, null, episodeNumber, title); } @@ -36,8 +36,8 @@ public class Episode implements Serializable { } - public String getShowName() { - return showName; + public String getSeriesName() { + return seriesName; } @@ -46,8 +46,8 @@ public class Episode implements Serializable { } - public void setShowName(String seriesName) { - this.showName = seriesName; + public void setSeriesName(String seriesName) { + this.seriesName = seriesName; } @@ -70,17 +70,43 @@ public class Episode implements Serializable { public String toString() { StringBuilder sb = new StringBuilder(40); - sb.append(showName); - sb.append(" - "); + sb.append(seriesName).append(" - "); - if (seasonNumber != null) - sb.append(seasonNumber + "x"); + if (seasonNumber != null) { + sb.append(seasonNumber).append("x"); + } - sb.append(episodeNumber); - - sb.append(" - "); - sb.append(title); + sb.append(episodeNumber).append(" - ").append(title); return sb.toString(); } + + + public static > T formatEpisodeNumbers(T episodes, int minDigits) { + // find max. episode number length + for (Episode episode : episodes) { + try { + int n = Integer.parseInt(episode.getEpisodeNumber()); + + if (n > 0) { + minDigits = Math.max(minDigits, (int) (Math.log(n) / Math.log(10))); + } + } catch (NumberFormatException e) { + // ignore + } + } + + // pad episode numbers with zeros (e.g. %02d) so all episode numbers have the same number of digits + String numberFormat = "%0" + minDigits + "d"; + + for (Episode episode : episodes) { + try { + episode.setEpisodeNumber(String.format(numberFormat, Integer.parseInt(episode.getEpisodeNumber()))); + } catch (NumberFormatException e) { + // ignore + } + } + + return episodes; + } } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java index 33f0586e..91f57355 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import net.sourceforge.filebot.web.OpenSubtitlesSubtitleDescriptor.Property; import redstone.xmlrpc.XmlRpcClient; import redstone.xmlrpc.XmlRpcException; import redstone.xmlrpc.XmlRpcFault; @@ -156,8 +157,8 @@ public class OpenSubtitlesClient { List subs = new ArrayList(); try { - for (Map subtitle : response.get("data")) { - subs.add(new OpenSubtitlesSubtitleDescriptor(subtitle)); + for (Map subtitleData : response.get("data")) { + subs.add(new OpenSubtitlesSubtitleDescriptor(Property.asEnumMap(subtitleData))); } } catch (ClassCastException e) { // if the response is an error message, generic types won't match diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java index 511a991f..d697d77a 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java @@ -4,8 +4,10 @@ package net.sourceforge.filebot.web; import java.net.MalformedURLException; import java.net.URL; -import java.util.HashMap; +import java.util.Collections; +import java.util.EnumMap; import java.util.Map; +import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; @@ -19,10 +21,10 @@ import net.sourceforge.tuned.DownloadTask; */ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { - private final Map properties; + private final Map properties; - public static enum Properties { + public static enum Property { IDSubMovieFile, MovieHash, MovieByteSize, @@ -54,45 +56,59 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { ISO639, LanguageName, SubDownloadLink, - ZipDownloadLink, + ZipDownloadLink; + + public static EnumMap asEnumMap(Map stringMap) { + EnumMap enumMap = new EnumMap(Property.class); + + for (Entry entry : stringMap.entrySet()) { + try { + enumMap.put(Property.valueOf(entry.getKey()), entry.getValue()); + } catch (IllegalArgumentException e) { + // illegal enum constant, just ignore + } + } + + return enumMap; + } } - public OpenSubtitlesSubtitleDescriptor(Map properties) { - this.properties = new HashMap(properties); + public OpenSubtitlesSubtitleDescriptor(Map properties) { + this.properties = new EnumMap(properties); } - public String getProperty(Properties property) { - return properties.get(property.name()); + public Map getProperties() { + return Collections.unmodifiableMap(properties); } @Override public String getName() { - return getProperty(Properties.SubFileName); + return properties.get(Property.SubFileName); } @Override public String getLanguageName() { - return getProperty(Properties.LanguageName); + return properties.get(Property.LanguageName); } @Override public String getAuthor() { - return getProperty(Properties.UserNickName); + return properties.get(Property.UserNickName); } public long getSize() { - return Long.parseLong(getProperty(Properties.SubSize)); + return Long.parseLong(properties.get(Property.SubSize)); } public URL getDownloadLink() { - String link = getProperty(Properties.ZipDownloadLink); + String link = properties.get(Property.ZipDownloadLink); try { return new URL(link); diff --git a/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java b/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java index 4822f50c..9d82b5be 100644 --- a/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java +++ b/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java @@ -4,13 +4,13 @@ package net.sourceforge.filebot.web; public class SeasonOutOfBoundsException extends IndexOutOfBoundsException { - private final String showName; + private final String seriesName; private final int season; private final int maxSeason; - public SeasonOutOfBoundsException(String showName, int season, int maxSeason) { - this.showName = showName; + public SeasonOutOfBoundsException(String seriesName, int season, int maxSeason) { + this.seriesName = seriesName; this.season = season; this.maxSeason = maxSeason; } @@ -18,12 +18,12 @@ public class SeasonOutOfBoundsException extends IndexOutOfBoundsException { @Override public String getMessage() { - return String.format("%s has only %d season%s.", showName, maxSeason, maxSeason != 1 ? "s" : ""); + return String.format("%s has only %d season%s.", seriesName, maxSeason, maxSeason != 1 ? "s" : ""); } - public String getShowName() { - return showName; + public String getSeriesName() { + return seriesName; } diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java index f63b5b6f..bf284eab 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java @@ -27,7 +27,7 @@ import java.util.regex.Pattern; import javax.swing.Icon; import net.sourceforge.filebot.ResourceManager; -import net.sourceforge.tuned.FileUtil; +import net.sourceforge.tuned.FileUtilities; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -239,7 +239,7 @@ public class SubsceneSubtitleClient implements SubtitleClient { private URL getDownloadUrl(URL referer, String subtitleId, String typeId) throws MalformedURLException { - String basePath = FileUtil.getNameWithoutExtension(referer.getFile()); + String basePath = FileUtilities.getNameWithoutExtension(referer.getFile()); String path = String.format("%s-dlpath-%s/%s.zipx", basePath, subtitleId, typeId); return new URL(referer.getProtocol(), referer.getHost(), path); diff --git a/source/net/sourceforge/filebot/web/TVDotComClient.java b/source/net/sourceforge/filebot/web/TVDotComClient.java index 1f9dbebb..00bd0a80 100644 --- a/source/net/sourceforge/filebot/web/TVDotComClient.java +++ b/source/net/sourceforge/filebot/web/TVDotComClient.java @@ -11,7 +11,6 @@ import java.net.URI; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -141,12 +140,6 @@ public class TVDotComClient implements EpisodeListClient { List nodes = selectNodes("id('eps_table')//TD[@class='ep_title']/parent::TR", dom); - // create mutable list from nodes so we can reverse the list - nodes = new ArrayList(nodes); - - // episodes are ordered in reverse ... we definitely don't want that! - Collections.reverse(nodes); - Integer episodeOffset = null; ArrayList episodes = new ArrayList(nodes.size()); diff --git a/source/net/sourceforge/filebot/web/TVRageClient.java b/source/net/sourceforge/filebot/web/TVRageClient.java index 69d2ef19..13f73fa7 100644 --- a/source/net/sourceforge/filebot/web/TVRageClient.java +++ b/source/net/sourceforge/filebot/web/TVRageClient.java @@ -94,7 +94,7 @@ public class TVRageClient implements EpisodeListClient { Document dom = getDocument(episodeListUrl); - String showName = selectString("Show/name", dom); + String seriesName = selectString("Show/name", dom); List nodes = selectNodes("Show/Episodelist/Season/episode", dom); List episodes = new ArrayList(nodes.size()); @@ -104,7 +104,7 @@ public class TVRageClient implements EpisodeListClient { String episodeNumber = getTextContent("seasonnum", node); String seasonNumber = node.getParentNode().getAttributes().getNamedItem("no").getTextContent(); - episodes.add(new Episode(showName, seasonNumber, episodeNumber, title)); + episodes.add(new Episode(seriesName, seasonNumber, episodeNumber, title)); } // populate cache diff --git a/source/net/sourceforge/filebot/web/TheTVDBClient.java b/source/net/sourceforge/filebot/web/TheTVDBClient.java index 1e2de3a5..636ff9ce 100644 --- a/source/net/sourceforge/filebot/web/TheTVDBClient.java +++ b/source/net/sourceforge/filebot/web/TheTVDBClient.java @@ -51,6 +51,9 @@ public class TheTVDBClient implements EpisodeListClient { public TheTVDBClient(String apikey) { + if (apikey == null) + throw new NullPointerException("apikey must not be null"); + this.apikey = apikey; } @@ -131,8 +134,9 @@ public class TheTVDBClient implements EpisodeListClient { } } - if (episodes.isEmpty()) + if (episodes.isEmpty()) { throw new SeasonOutOfBoundsException(searchResult.getName(), season, maxSeason); + } return episodes; } diff --git a/source/net/sourceforge/tuned/FileUtil.java b/source/net/sourceforge/tuned/FileUtil.java deleted file mode 100644 index aee0967d..00000000 --- a/source/net/sourceforge/tuned/FileUtil.java +++ /dev/null @@ -1,126 +0,0 @@ - -package net.sourceforge.tuned; - - -import java.io.File; - - -public final class FileUtil { - - public final static long KILO = 1024; - - public final static long MEGA = KILO * 1024; - - public final static long GIGA = MEGA * 1024; - - - public static String formatSize(long size) { - if (size >= MEGA) - return String.format("%,d MB", size / MEGA); - else if (size >= KILO) - return String.format("%,d KB", size / KILO); - else - return String.format("%,d Byte", size); - } - - - public static boolean hasExtension(File file, Iterable extensions) { - if (file.isDirectory()) - return false; - - return hasExtension(file.getName(), extensions); - } - - - public static boolean hasExtension(String filename, Iterable extensions) { - String extension = getExtension(filename, false); - - for (String ext : extensions) { - if (ext.equalsIgnoreCase(extension)) - return true; - } - - return false; - } - - - public static String getExtension(File file) { - return getExtension(file, false); - } - - - public static String getExtension(File file, boolean includeDot) { - return getExtension(file.getName(), includeDot); - } - - - public static String getExtension(String name, boolean includeDot) { - int dotIndex = name.lastIndexOf("."); - - // .config -> no extension, just hidden - if (dotIndex >= 1) { - int startIndex = dotIndex; - - if (!includeDot) - startIndex += 1; - - if (startIndex <= name.length()) { - return name.substring(startIndex, name.length()); - } - } - - return ""; - } - - - public static String getNameWithoutExtension(String name) { - int dotIndex = name.lastIndexOf("."); - - if (dotIndex < 1) - return name; - - return name.substring(0, dotIndex); - } - - - public static String getFileName(File file) { - if (file.isDirectory()) - return getFolderName(file); - - return getNameWithoutExtension(file.getName()); - } - - - public static String getFolderName(File file) { - String name = file.getName(); - - if (!name.isEmpty()) - return name; - - // file might be a drive (only has a path, but no name) - return file.toString(); - } - - - public static String getFileType(File file) { - if (file.isDirectory()) - return "Folder"; - - String extension = getExtension(file.getName(), false); - - if (!extension.isEmpty()) - return extension; - - // some file with no suffix - return "File"; - } - - - /** - * Dummy constructor to prevent instantiation. - */ - private FileUtil() { - throw new UnsupportedOperationException(); - } - -} diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java new file mode 100644 index 00000000..d97077f2 --- /dev/null +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -0,0 +1,164 @@ + +package net.sourceforge.tuned; + + +import java.io.File; +import java.io.FileFilter; + + +public final class FileUtilities { + + public static final long KILO = 1024; + public static final long MEGA = KILO * 1024; + public static final long GIGA = MEGA * 1024; + + + public static String formatSize(long size) { + if (size >= MEGA) + return String.format("%,d MB", size / MEGA); + else if (size >= KILO) + return String.format("%,d KB", size / KILO); + else + return String.format("%,d Byte", size); + } + + + public static String getExtension(File file) { + return getExtension(file.getName()); + } + + + public static String getExtension(String name) { + int dotIndex = name.lastIndexOf("."); + + // .hidden -> no extension, just hidden + if (dotIndex > 0 && dotIndex < name.length() - 1) { + return name.substring(dotIndex + 1); + } + + return null; + } + + + public static boolean hasExtension(File file, String... extensions) { + if (file.isDirectory()) + return false; + + return hasExtension(file.getName(), extensions); + } + + + public static boolean hasExtension(String filename, String... extensions) { + String extension = getExtension(filename); + + if (extension != null) { + for (String entry : extensions) { + if (extension.equalsIgnoreCase(entry)) + return true; + } + } + + return false; + } + + + public static String getNameWithoutExtension(String name) { + int dotIndex = name.lastIndexOf("."); + + if (dotIndex > 0) + return name.substring(0, dotIndex); + + // no extension, return given name + return name; + } + + + public static String getName(File file) { + if (file.isDirectory()) + return getFolderName(file); + + return getNameWithoutExtension(file.getName()); + } + + + public static String getFolderName(File file) { + String name = file.getName(); + + if (!name.isEmpty()) + return name; + + // file might be a drive (only has a path, but no name) + return file.toString(); + } + + + public static String getType(File file) { + if (file.isDirectory()) + return "Folder"; + + String extension = getExtension(file.getName()); + + if (!extension.isEmpty()) + return extension; + + // some file with no extension + return "File"; + } + + + public static boolean containsOnly(Iterable files, FileFilter filter) { + for (File file : files) { + if (!filter.accept(file)) + return false; + } + + return true; + } + + public static final FileFilter FOLDERS = new FileFilter() { + + @Override + public boolean accept(File file) { + return file.isDirectory(); + } + }; + + public static final FileFilter FILES = new FileFilter() { + + @Override + public boolean accept(File file) { + return file.isFile(); + } + }; + + + public static class ExtensionFileFilter implements FileFilter { + + private final String[] extensions; + + + public ExtensionFileFilter(String... extensions) { + this.extensions = extensions; + } + + + @Override + public boolean accept(File file) { + return hasExtension(file, extensions); + } + + + public String[] getExtensions() { + return extensions.clone(); + } + } + + + /** + * Dummy constructor to prevent instantiation. + */ + private FileUtilities() { + throw new UnsupportedOperationException(); + } + +} diff --git a/source/net/sourceforge/tuned/ui/ActionPopup.java b/source/net/sourceforge/tuned/ui/ActionPopup.java new file mode 100644 index 00000000..57e94401 --- /dev/null +++ b/source/net/sourceforge/tuned/ui/ActionPopup.java @@ -0,0 +1,109 @@ + +package net.sourceforge.tuned.ui; + + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; + +import net.miginfocom.swing.MigLayout; + + +public class ActionPopup extends JPopupMenu { + + protected JLabel headerLabel = new JLabel(); + protected JLabel descriptionLabel = new JLabel(); + protected JLabel statusLabel = new JLabel(); + + protected JPanel actionPanel = new JPanel(new MigLayout("insets 0, wrap 1")); + + + public ActionPopup(String label, Icon icon) { + headerLabel.setText(label); + headerLabel.setIcon(icon); + headerLabel.setIconTextGap(5); + + statusLabel.setFont(statusLabel.getFont().deriveFont(10f)); + statusLabel.setForeground(Color.GRAY); + + actionPanel.setOpaque(false); + + setLayout(new MigLayout("nogrid, fill, insets 0")); + + add(headerLabel, "gapx 4px 4px, gapy 1px, wrap 3px"); + add(new JSeparator(), "growx, wrap 1px"); + add(descriptionLabel, "gapx 4px, wrap 3px"); + add(actionPanel, "gapx 12px 12px, wrap"); + add(new JSeparator(), "growx, wrap 0px"); + add(statusLabel, "growx, h 11px!, gapx 3px, wrap 1px"); + } + + + @Override + public JMenuItem add(Action a) { + LinkButton link = new LinkButton(a); + + // close popup when action is triggered + link.addActionListener(closeListener); + + // underline text + link.setText(String.format("%s", link.getText())); + + // use rollover color + link.setRolloverEnabled(false); + link.setColor(link.getRolloverColor()); + + actionPanel.add(link); + + return null; + } + + + @Override + public void setLabel(String label) { + headerLabel.setText(label); + } + + + @Override + public String getLabel() { + return headerLabel.getText(); + } + + + public void setDescription(String string) { + descriptionLabel.setText(string); + } + + + public String getDescription() { + return descriptionLabel.getText(); + } + + + public void setStatus(String string) { + statusLabel.setText(string); + } + + + public String getStatus() { + return statusLabel.getText(); + } + + private final ActionListener closeListener = new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + setVisible(false); + } + }; + +} diff --git a/source/net/sourceforge/tuned/ui/HyperlinkLabel.java b/source/net/sourceforge/tuned/ui/HyperlinkLabel.java deleted file mode 100644 index 35cdad0e..00000000 --- a/source/net/sourceforge/tuned/ui/HyperlinkLabel.java +++ /dev/null @@ -1,58 +0,0 @@ - -package net.sourceforge.tuned.ui; - - -import java.awt.Color; -import java.awt.Cursor; -import java.awt.Desktop; -import java.awt.SystemColor; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.net.URI; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.swing.JLabel; - - -public class HyperlinkLabel extends JLabel { - - private final URI link; - private final Color defaultColor; - - - public HyperlinkLabel(String label, URI link) { - super(label); - this.link = link; - defaultColor = getForeground(); - addMouseListener(linker); - setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - } - - private MouseListener linker = new MouseAdapter() { - - @Override - public void mouseClicked(MouseEvent event) { - try { - Desktop.getDesktop().browse(link); - } catch (Exception e) { - // should not happen - Logger.getLogger("global").log(Level.SEVERE, e.toString(), e); - } - } - - - @Override - public void mouseEntered(MouseEvent e) { - setForeground(SystemColor.textHighlight); - } - - - @Override - public void mouseExited(MouseEvent e) { - setForeground(defaultColor); - } - }; - -} diff --git a/source/net/sourceforge/tuned/ui/LinkButton.java b/source/net/sourceforge/tuned/ui/LinkButton.java new file mode 100644 index 00000000..eb390efd --- /dev/null +++ b/source/net/sourceforge/tuned/ui/LinkButton.java @@ -0,0 +1,124 @@ + +package net.sourceforge.tuned.ui; + + +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Desktop; +import java.awt.SystemColor; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JButton; + + +public class LinkButton extends JButton { + + private Color color = getForeground(); + private Color rolloverColor = SystemColor.textHighlight; + + + public LinkButton(String text, Icon icon, URI uri) { + this(new OpenUriAction(text, icon, uri)); + } + + + public LinkButton(Action action) { + setAction(action); + + setFocusPainted(false); + setOpaque(false); + setContentAreaFilled(false); + setBorder(null); + + setIconTextGap(6); + setRolloverEnabled(true); + + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } + + + @Override + public void setRolloverEnabled(boolean enabled) { + super.setRolloverEnabled(enabled); + + // always remove listener if there is one + removeMouseListener(rolloverListener); + + if (enabled) { + // add listener again, if enabled + addMouseListener(rolloverListener); + } + } + + + public Color getColor() { + return color; + } + + + public void setColor(Color color) { + this.color = color; + this.setForeground(color); + } + + + public Color getRolloverColor() { + return rolloverColor; + } + + + public void setRolloverColor(Color rolloverColor) { + this.rolloverColor = rolloverColor; + } + + protected final MouseListener rolloverListener = new MouseAdapter() { + + @Override + public void mouseEntered(MouseEvent e) { + setForeground(rolloverColor); + } + + + @Override + public void mouseExited(MouseEvent e) { + setForeground(color); + } + }; + + + protected static class OpenUriAction extends AbstractAction { + + public static final String URI = "uri"; + + + public OpenUriAction(String text, Icon icon, URI uri) { + super(text, icon); + putValue(URI, uri); + } + + + @Override + public void actionPerformed(ActionEvent evt) { + try { + URI uri = (URI) getValue(URI); + + if (uri != null) { + Desktop.getDesktop().browse(uri); + } + } catch (Exception e) { + // should not happen + Logger.getLogger("global").log(Level.SEVERE, e.toString(), e); + } + } + } + +} diff --git a/source/net/sourceforge/tuned/ui/LoadingOverlayPane.java b/source/net/sourceforge/tuned/ui/LoadingOverlayPane.java index 1e768bcc..67e12582 100644 --- a/source/net/sourceforge/tuned/ui/LoadingOverlayPane.java +++ b/source/net/sourceforge/tuned/ui/LoadingOverlayPane.java @@ -22,21 +22,27 @@ public class LoadingOverlayPane extends JComponent { public LoadingOverlayPane(JComponent component, JComponent propertyChangeSource) { - this(component, new ProgressIndicator(), propertyChangeSource); + this(component, propertyChangeSource, null, null); } - public LoadingOverlayPane(JComponent component, JComponent animationComponent, JComponent propertyChangeSource) { + public LoadingOverlayPane(JComponent component, JComponent propertyChangeSource, String offsetX, String offsetY) { setLayout(new MigLayout("insets 0, fill")); - this.animationComponent = animationComponent; - - add(animationComponent, "pos n 8px 100%-18px n"); - add(component, "grow"); + animationComponent = new ProgressIndicator(); animationComponent.setVisible(false); + add(animationComponent, String.format("pos n %s 100%%-%s n", offsetY != null ? offsetY : "8px", offsetX != null ? offsetX : "18px")); + add(component, "grow"); + if (propertyChangeSource != null) { - propertyChangeSource.addPropertyChangeListener(LOADING_PROPERTY, loadingListener); + propertyChangeSource.addPropertyChangeListener(LOADING_PROPERTY, new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + setOverlayVisible((Boolean) evt.getNewValue()); + } + }); } } @@ -51,7 +57,7 @@ public class LoadingOverlayPane extends JComponent { overlayEnabled = b; if (overlayEnabled) { - TunedUtil.invokeLater(millisToOverlay, new Runnable() { + TunedUtilities.invokeLater(millisToOverlay, new Runnable() { @Override public void run() { @@ -66,15 +72,4 @@ public class LoadingOverlayPane extends JComponent { } } - private final PropertyChangeListener loadingListener = new PropertyChangeListener() { - - @Override - public void propertyChange(PropertyChangeEvent evt) { - Boolean loading = (Boolean) evt.getNewValue(); - - setOverlayVisible(loading); - } - - }; - } diff --git a/source/net/sourceforge/tuned/ui/ProgressDialog.java b/source/net/sourceforge/tuned/ui/ProgressDialog.java index afa584ed..8481a7a4 100644 --- a/source/net/sourceforge/tuned/ui/ProgressDialog.java +++ b/source/net/sourceforge/tuned/ui/ProgressDialog.java @@ -50,7 +50,7 @@ public class ProgressDialog extends JDialog { setSize(240, 155); - setLocation(TunedUtil.getPreferredLocation(this)); + setLocation(TunedUtilities.getPreferredLocation(this)); } diff --git a/source/net/sourceforge/tuned/ui/SelectButtonTextField.java b/source/net/sourceforge/tuned/ui/SelectButtonTextField.java index 6ef5d973..73dc194f 100644 --- a/source/net/sourceforge/tuned/ui/SelectButtonTextField.java +++ b/source/net/sourceforge/tuned/ui/SelectButtonTextField.java @@ -43,8 +43,8 @@ public class SelectButtonTextField extends JComponent { editor.setUI(new TextFieldComboBoxUI()); - TunedUtil.putActionForKeystroke(this, KeyStroke.getKeyStroke("ctrl UP"), new SpinClientAction(-1)); - TunedUtil.putActionForKeystroke(this, KeyStroke.getKeyStroke("ctrl DOWN"), new SpinClientAction(1)); + TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ctrl UP"), new SpinClientAction(-1)); + TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ctrl DOWN"), new SpinClientAction(1)); } diff --git a/source/net/sourceforge/tuned/ui/TunedUtil.java b/source/net/sourceforge/tuned/ui/TunedUtilities.java similarity index 55% rename from source/net/sourceforge/tuned/ui/TunedUtil.java rename to source/net/sourceforge/tuned/ui/TunedUtilities.java index a72a5dae..1b7ae859 100644 --- a/source/net/sourceforge/tuned/ui/TunedUtil.java +++ b/source/net/sourceforge/tuned/ui/TunedUtilities.java @@ -11,6 +11,9 @@ import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.Method; import javax.swing.Action; import javax.swing.Icon; @@ -21,8 +24,10 @@ import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.Timer; +import net.sourceforge.tuned.ExceptionUtil; -public final class TunedUtil { + +public final class TunedUtilities { public static final Color TRANSLUCENT = new Color(255, 255, 255, 0); @@ -55,10 +60,13 @@ public final class TunedUtil { public static Image getImage(Icon icon) { - if (icon instanceof ImageIcon) { - return ((ImageIcon) icon).getImage(); - } + if (icon == null) + return null; + if (icon instanceof ImageIcon) + return ((ImageIcon) icon).getImage(); + + // draw icon into a new image BufferedImage image = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = image.createGraphics(); @@ -91,10 +99,58 @@ public final class TunedUtil { } + public static void syncPropertyChangeEvents(Class propertyType, String property, Object from, Object to) { + PropertyChangeDelegate.create(propertyType, property, from, to); + } + + + private static class PropertyChangeDelegate implements PropertyChangeListener { + + private final String property; + + private final Object target; + private final Method firePropertyChange; + + + public static PropertyChangeDelegate create(Class propertyType, String property, Object source, Object target) { + try { + + PropertyChangeDelegate listener = new PropertyChangeDelegate(propertyType, property, target); + source.getClass().getMethod("addPropertyChangeListener", PropertyChangeListener.class).invoke(source, listener); + + return listener; + } catch (Exception e) { + throw ExceptionUtil.asRuntimeException(e); + } + } + + + protected PropertyChangeDelegate(Class propertyType, String property, Object target) throws SecurityException, NoSuchMethodException { + this.property = property; + this.target = target; + + this.firePropertyChange = target.getClass().getMethod("firePropertyChange", String.class, propertyType, propertyType); + } + + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (property.equals(evt.getPropertyName())) { + try { + firePropertyChange.invoke(target, evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); + } catch (Exception e) { + throw ExceptionUtil.asRuntimeException(e); + } + } + } + + } + + /** * Dummy constructor to prevent instantiation. */ - private TunedUtil() { + private TunedUtilities() { throw new UnsupportedOperationException(); } diff --git a/source/net/sourceforge/tuned/ui/notification/NotificationManager.java b/source/net/sourceforge/tuned/ui/notification/NotificationManager.java index 9a223474..f064adb9 100644 --- a/source/net/sourceforge/tuned/ui/notification/NotificationManager.java +++ b/source/net/sourceforge/tuned/ui/notification/NotificationManager.java @@ -9,7 +9,7 @@ package net.sourceforge.tuned.ui.notification; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class NotificationManager { @@ -28,7 +28,7 @@ public class NotificationManager { public void show(NotificationWindow notification) { - TunedUtil.checkEventDispatchThread(); + TunedUtilities.checkEventDispatchThread(); notification.addWindowListener(new RemoveListener()); layout.add(notification); diff --git a/source/net/sourceforge/tuned/ui/notification/NotificationWindow.java b/source/net/sourceforge/tuned/ui/notification/NotificationWindow.java index 388bbf79..445c59f3 100644 --- a/source/net/sourceforge/tuned/ui/notification/NotificationWindow.java +++ b/source/net/sourceforge/tuned/ui/notification/NotificationWindow.java @@ -17,7 +17,7 @@ import java.awt.event.WindowEvent; import javax.swing.JWindow; import javax.swing.Timer; -import net.sourceforge.tuned.ui.TunedUtil; +import net.sourceforge.tuned.ui.TunedUtilities; public class NotificationWindow extends JWindow { @@ -51,7 +51,7 @@ public class NotificationWindow extends JWindow { public final void close() { - TunedUtil.checkEventDispatchThread(); + TunedUtilities.checkEventDispatchThread(); // window events are not fired automatically, required for layout updates processWindowEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); @@ -72,7 +72,7 @@ public class NotificationWindow extends JWindow { @Override public void componentShown(ComponentEvent e) { if (timeout >= 0) { - timer = TunedUtil.invokeLater(timeout, new Runnable() { + timer = TunedUtilities.invokeLater(timeout, new Runnable() { @Override public void run() { diff --git a/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java b/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java index 5ed15122..b1231040 100644 --- a/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java +++ b/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java @@ -15,13 +15,13 @@ public class NameSimilarityMetricTest { @Test public void getSimilarity() { // normalize separators, lower-case - assertEquals(1, metric.getSimilarity("test s01e01 first", "test.S01E01.First")); - assertEquals(1, metric.getSimilarity("test s01e02 second", "test_[S01E02]_Second")); - assertEquals(1, metric.getSimilarity("test s01e03 third", "__test__S01E03__Third__")); - assertEquals(1, metric.getSimilarity("test s01e04 four", "test s01e04 four")); + assertEquals(1, metric.getSimilarity("test s01e01 first", "test.S01E01.First"), 0); + assertEquals(1, metric.getSimilarity("test s01e02 second", "test_[S01E02]_Second"), 0); + assertEquals(1, metric.getSimilarity("test s01e03 third", "__test__S01E03__Third__"), 0); + assertEquals(1, metric.getSimilarity("test s01e04 four", "test s01e04 four"), 0); // remove checksum - assertEquals(1, metric.getSimilarity("test", "test [EF62DF13]")); + assertEquals(1, metric.getSimilarity("test", "test [EF62DF13]"), 0); } } diff --git a/test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java b/test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java index 38824e66..86dda2b2 100644 --- a/test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java +++ b/test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java @@ -89,8 +89,6 @@ public class NumericSimilarityMetricTest { } } - // System.out.println(String.format("[%f, %s, %s]", maxSimilarity, value, mostSimilar)); - return mostSimilar; } } diff --git a/test/net/sourceforge/filebot/similarity/SeasonEpisodeMatcherTest.java b/test/net/sourceforge/filebot/similarity/SeasonEpisodeMatcherTest.java new file mode 100644 index 00000000..9ee5c217 --- /dev/null +++ b/test/net/sourceforge/filebot/similarity/SeasonEpisodeMatcherTest.java @@ -0,0 +1,72 @@ + +package net.sourceforge.filebot.similarity; + + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + + +public class SeasonEpisodeMatcherTest { + + private static SeasonEpisodeMatcher matcher = new SeasonEpisodeMatcher(); + + + @Test + public void patternPrecedence() { + // S01E01 pattern has highest precedence + assertEquals("1x03", matcher.match("Test.101.1x02.S01E03").get(0).toString()); + + // multiple values + assertEquals("1x02", matcher.match("Test.42.s01e01.s01e02.300").get(1).toString()); + } + + + @Test + public void pattern_1x01() { + assertEquals("1x01", matcher.match("1x01").get(0).toString()); + + // test multiple matches + assertEquals("1x02", matcher.match("Test - 1x01 and 1x02 - Multiple MatchCollection").get(1).toString()); + + // test high values + assertEquals("12x345", matcher.match("Test - 12x345 - High Values").get(0).toString()); + + // test lookahead and lookbehind + assertEquals("1x03", matcher.match("Test_-_103_[1280x720]").get(0).toString()); + } + + + @Test + public void pattern_S01E01() { + assertEquals("1x01", matcher.match("S01E01").get(0).toString()); + + // test multiple matches + assertEquals("1x02", matcher.match("S01E01 and S01E02 - Multiple MatchCollection").get(1).toString()); + + // test separated values + assertEquals("1x03", matcher.match("[s01]_[e03]").get(0).toString()); + + // test high values + assertEquals("12x345", matcher.match("Test - S12E345 - High Values").get(0).toString()); + } + + + @Test + public void pattern_101() { + assertEquals("1x01", matcher.match("Test.101").get(0).toString()); + + // test 2-digit number + assertEquals("0x02", matcher.match("02").get(0).toString()); + + // test high values + assertEquals("10x01", matcher.match("[Test]_1001_High_Values").get(0).toString()); + + // first two digits <= 29 + assertEquals(null, matcher.match("The 4400")); + + // test lookbehind + assertEquals(null, matcher.match("720p")); + } + +} diff --git a/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java b/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java index 15cdcdfd..e7a0c33e 100644 --- a/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java +++ b/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java @@ -15,82 +15,24 @@ public class SeasonEpisodeSimilarityMetricTest { @Test public void getSimilarity() { // single pattern match, single episode match - assertEquals(1.0, metric.getSimilarity("1x01", "s01e01")); + assertEquals(1.0, metric.getSimilarity("1x01", "s01e01"), 0); // multiple pattern matches, single episode match - assertEquals(1.0, metric.getSimilarity("1x02a", "101 102 103")); + assertEquals(1.0, metric.getSimilarity("1x02a", "101 102 103"), 0); // multiple pattern matches, no episode match - assertEquals(0.0, metric.getSimilarity("1x03b", "104 105 106")); + assertEquals(0.0, metric.getSimilarity("1x03b", "104 105 106"), 0); // no pattern match, no episode match - assertEquals(0.0, metric.getSimilarity("abc", "xyz")); + assertEquals(0.0, metric.getSimilarity("abc", "xyz"), 0); } @Test public void fallbackMetric() { - assertEquals(1.0, metric.getSimilarity("1x01", "sno=1, eno=1")); + assertEquals(1.0, metric.getSimilarity("1x01", "sno=1, eno=1"), 0); - assertEquals(1.0, metric.getSimilarity("1x02", "Dexter - Staffel 1 Episode 2")); - } - - - @Test - public void patternPrecedence() { - // S01E01 pattern has highest precedence - assertEquals("1x03", metric.match("Test.101.1x02.S01E03").get(0).toString()); - - // multiple values - assertEquals("1x02", metric.match("Test.42.s01e01.s01e02.300").get(1).toString()); - } - - - @Test - public void pattern_1x01() { - assertEquals("1x01", metric.match("1x01").get(0).toString()); - - // test multiple matches - assertEquals("1x02", metric.match("Test - 1x01 and 1x02 - Multiple MatchCollection").get(1).toString()); - - // test high values - assertEquals("12x345", metric.match("Test - 12x345 - High Values").get(0).toString()); - - // test lookahead and lookbehind - assertEquals("1x03", metric.match("Test_-_103_[1280x720]").get(0).toString()); - } - - - @Test - public void pattern_S01E01() { - assertEquals("1x01", metric.match("S01E01").get(0).toString()); - - // test multiple matches - assertEquals("1x02", metric.match("S01E01 and S01E02 - Multiple MatchCollection").get(1).toString()); - - // test separated values - assertEquals("1x03", metric.match("[s01]_[e03]").get(0).toString()); - - // test high values - assertEquals("12x345", metric.match("Test - S12E345 - High Values").get(0).toString()); - } - - - @Test - public void pattern_101() { - assertEquals("1x01", metric.match("Test.101").get(0).toString()); - - // test 2-digit number - assertEquals("0x02", metric.match("02").get(0).toString()); - - // test high values - assertEquals("10x01", metric.match("[Test]_1001_High_Values").get(0).toString()); - - // first two digits <= 29 - assertEquals(null, metric.match("The 4400")); - - // test lookbehind - assertEquals(null, metric.match("720p")); + assertEquals(1.0, metric.getSimilarity("1x02", "Dexter - Staffel 1 Episode 2"), 0); } } diff --git a/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java b/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java new file mode 100644 index 00000000..a5be70c7 --- /dev/null +++ b/test/net/sourceforge/filebot/similarity/SeriesNameMatcherTest.java @@ -0,0 +1,57 @@ + +package net.sourceforge.filebot.similarity; + + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import net.sourceforge.filebot.similarity.SeriesNameMatcher.SeriesNameCollection; + +import org.junit.Test; + + +public class SeriesNameMatcherTest { + + private static SeriesNameMatcher matcher = new SeriesNameMatcher(5); + + + @Test + public void matchBeforeSeasonEpisodePattern() { + assertEquals("The Test", matcher.matchBySeasonEpisodePattern("The Test - 1x01")); + + // real world test + assertEquals("Mushishi", matcher.matchBySeasonEpisodePattern("[niizk]_Mushishi_-_01_-_The_Green_Gathering")); + } + + + @Test + public void normalize() { + // non-letter and non-digit characters + assertEquals("The Test", matcher.normalize("_The_Test_-_ ...")); + + // brackets + assertEquals("Luffy", matcher.normalize("[strawhat] Luffy [D.] [@Monkey]")); + } + + + @Test + public void firstCommonSequence() { + String[] seq1 = "[abc] Common Name 1".split("\\s"); + String[] seq2 = "[xyz] Common Name 2".split("\\s"); + + assertArrayEquals(new String[] { "Common", "Name" }, matcher.firstCommonSequence(seq1, seq2, String.CASE_INSENSITIVE_ORDER)); + } + + + @Test + public void firstCharacterCaseBalance() { + SeriesNameCollection n = new SeriesNameCollection(); + + assertTrue(n.firstCharacterCaseBalance("My Name is Earl") > n.firstCharacterCaseBalance("My Name Is Earl")); + assertTrue(n.firstCharacterCaseBalance("My Name is Earl") > n.firstCharacterCaseBalance("my name is earl")); + + // boost upper case ration + assertTrue(n.firstCharacterCaseBalance("Roswell") > n.firstCharacterCaseBalance("roswell")); + + } +} diff --git a/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java b/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java index ac7975ef..e81df87d 100644 --- a/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java +++ b/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java @@ -8,7 +8,7 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses( { NameSimilarityMetricTest.class, NumericSimilarityMetricTest.class, SeasonEpisodeSimilarityMetricTest.class }) +@SuiteClasses( { SeriesNameMatcherTest.class, SeasonEpisodeMatcherTest.class, NameSimilarityMetricTest.class, NumericSimilarityMetricTest.class, SeasonEpisodeSimilarityMetricTest.class }) public class SimilarityTestSuite { } diff --git a/test/net/sourceforge/filebot/web/AnidbClientTest.java b/test/net/sourceforge/filebot/web/AnidbClientTest.java index 8fb1468f..d4d7f4c9 100644 --- a/test/net/sourceforge/filebot/web/AnidbClientTest.java +++ b/test/net/sourceforge/filebot/web/AnidbClientTest.java @@ -65,7 +65,7 @@ public class AnidbClientTest { Episode first = list.get(0); - assertEquals("Monster", first.getShowName()); + assertEquals("Monster", first.getSeriesName()); assertEquals("Herr Dr. Tenma", first.getTitle()); assertEquals("1", first.getEpisodeNumber()); assertEquals(null, first.getSeasonNumber()); @@ -80,7 +80,7 @@ public class AnidbClientTest { Episode first = list.get(0); - assertEquals("Juuni Kokuki", first.getShowName()); + assertEquals("Juuni Kokuki", first.getSeriesName()); assertEquals("Shadow of the Moon, The Sea of Shadow - Chapter 1", first.getTitle()); assertEquals("1", first.getEpisodeNumber()); assertEquals(null, first.getSeasonNumber()); diff --git a/test/net/sourceforge/filebot/web/TVDotComClientTest.java b/test/net/sourceforge/filebot/web/TVDotComClientTest.java index 97ade34b..a2ed4d0c 100644 --- a/test/net/sourceforge/filebot/web/TVDotComClientTest.java +++ b/test/net/sourceforge/filebot/web/TVDotComClientTest.java @@ -62,7 +62,7 @@ public class TVDotComClientTest { Episode chosen = list.get(21); - assertEquals("Buffy the Vampire Slayer", chosen.getShowName()); + assertEquals("Buffy the Vampire Slayer", chosen.getSeriesName()); assertEquals("Chosen", chosen.getTitle()); assertEquals("22", chosen.getEpisodeNumber()); assertEquals("7", chosen.getSeasonNumber()); @@ -77,7 +77,7 @@ public class TVDotComClientTest { Episode first = list.get(0); - assertEquals("Buffy the Vampire Slayer", first.getShowName()); + assertEquals("Buffy the Vampire Slayer", first.getSeriesName()); assertEquals("Unaired Pilot", first.getTitle()); assertEquals("Pilot", first.getEpisodeNumber()); assertEquals(null, first.getSeasonNumber()); @@ -92,7 +92,7 @@ public class TVDotComClientTest { Episode fourth = list.get(3); - assertEquals("Firefly", fourth.getShowName()); + assertEquals("Firefly", fourth.getSeriesName()); assertEquals("Jaynestown", fourth.getTitle()); assertEquals("4", fourth.getEpisodeNumber()); assertEquals("1", fourth.getSeasonNumber()); @@ -116,7 +116,7 @@ public class TVDotComClientTest { Episode episode = list.get(13); - assertEquals("Lost", episode.getShowName()); + assertEquals("Lost", episode.getSeriesName()); assertEquals("Exposé", episode.getTitle()); assertEquals("14", episode.getEpisodeNumber()); assertEquals("3", episode.getSeasonNumber()); diff --git a/test/net/sourceforge/filebot/web/TVRageClientTest.java b/test/net/sourceforge/filebot/web/TVRageClientTest.java index 14069080..aee409ae 100644 --- a/test/net/sourceforge/filebot/web/TVRageClientTest.java +++ b/test/net/sourceforge/filebot/web/TVRageClientTest.java @@ -41,7 +41,7 @@ public class TVRageClientTest { Episode chosen = list.get(21); - assertEquals("Buffy the Vampire Slayer", chosen.getShowName()); + assertEquals("Buffy the Vampire Slayer", chosen.getSeriesName()); assertEquals("Chosen", chosen.getTitle()); assertEquals("22", chosen.getEpisodeNumber()); assertEquals("7", chosen.getSeasonNumber()); @@ -56,7 +56,7 @@ public class TVRageClientTest { Episode first = list.get(0); - assertEquals("Buffy the Vampire Slayer", first.getShowName()); + assertEquals("Buffy the Vampire Slayer", first.getSeriesName()); assertEquals("Unaired Pilot", first.getTitle()); assertEquals("00", first.getEpisodeNumber()); assertEquals("0", first.getSeasonNumber()); diff --git a/test/net/sourceforge/filebot/web/TheTVDBClientTest.java b/test/net/sourceforge/filebot/web/TheTVDBClientTest.java index adf6a669..43f4899b 100644 --- a/test/net/sourceforge/filebot/web/TheTVDBClientTest.java +++ b/test/net/sourceforge/filebot/web/TheTVDBClientTest.java @@ -69,7 +69,7 @@ public class TheTVDBClientTest { Episode first = list.get(0); - assertEquals("Buffy the Vampire Slayer", first.getShowName()); + assertEquals("Buffy the Vampire Slayer", first.getSeriesName()); assertEquals("Unaired Pilot", first.getTitle()); assertEquals("1", first.getEpisodeNumber()); assertEquals("0", first.getSeasonNumber()); @@ -84,7 +84,7 @@ public class TheTVDBClientTest { Episode chosen = list.get(21); - assertEquals("Buffy the Vampire Slayer", chosen.getShowName()); + assertEquals("Buffy the Vampire Slayer", chosen.getSeriesName()); assertEquals("Chosen", chosen.getTitle()); assertEquals("22", chosen.getEpisodeNumber()); assertEquals("7", chosen.getSeasonNumber());