* ground up rewrite of the maching algorithm (I lovingly call it n:m multi-pass matching)
* added SeasonEpisodeSimilarityMetric which detects similarity based on known patterns * moved everything similarity/maching related to net.sourceforge.filebot.similarity Refactoring: * refactoring of all the matching-related stuff in rename panel * remove name2file and file2name maching selection because new maching algorithm works 2-ways from the start and doesn't need that hack * added console handler to ui logger that will log ui warnings and ui errors to console too * some refactoring on all SimilarityMetrics * use Interrupts in analyze tools to abort operation * refactoring of the rename process, if something goes wrong, we will now revert all already renamed files to their original filenames * static LINE_SEPARATOR pattern in FileTransferablePolicy * new maching icon, removed old ones
This commit is contained in:
parent
54b27e69b7
commit
c217d06eeb
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -66,6 +66,11 @@ public final class FileBotUtil {
|
|||
return embeddedChecksum;
|
||||
}
|
||||
|
||||
|
||||
public static String removeEmbeddedChecksum(String string) {
|
||||
return string.replaceAll("[\\(\\[]\\p{XDigit}{8}[\\]\\)]", "");
|
||||
}
|
||||
|
||||
public static final List<String> TORRENT_FILE_EXTENSIONS = unmodifiableList("torrent");
|
||||
public static final List<String> SFV_FILE_EXTENSIONS = unmodifiableList("sfv");
|
||||
public static final List<String> LIST_FILE_EXTENSIONS = unmodifiableList("txt", "list", "");
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
package net.sourceforge.filebot;
|
||||
|
||||
|
||||
import java.util.logging.ConsoleHandler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
|
@ -58,8 +59,18 @@ public class Main {
|
|||
|
||||
private static void setupLogging() {
|
||||
Logger uiLogger = Logger.getLogger("ui");
|
||||
uiLogger.addHandler(new NotificationLoggingHandler());
|
||||
|
||||
// don't use parent handlers
|
||||
uiLogger.setUseParentHandlers(false);
|
||||
|
||||
// ui handler
|
||||
uiLogger.addHandler(new NotificationLoggingHandler());
|
||||
|
||||
// console handler (for warnings and errors only)
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.WARNING);
|
||||
|
||||
uiLogger.addHandler(consoleHandler);
|
||||
}
|
||||
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,49 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
public class LengthEqualsMetric implements SimilarityMetric {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
long l1 = getLength(o1);
|
||||
|
||||
if (l1 >= 0 && l1 == getLength(o2)) {
|
||||
// objects have the same non-negative length
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
protected long getLength(Object o) {
|
||||
if (o instanceof File) {
|
||||
return ((File) o).length();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Check whether file size is equal or not";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Length";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
public class Match<V, C> {
|
||||
|
||||
private final V value;
|
||||
private final C candidate;
|
||||
|
||||
|
||||
public Match(V value, C candidate) {
|
||||
this.value = value;
|
||||
this.candidate = candidate;
|
||||
}
|
||||
|
||||
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public C getCandidate() {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the given match has the same value or the same candidate. This method uses an
|
||||
* <b>identity equality test</b>.
|
||||
*
|
||||
* @param match a match
|
||||
* @return Returns <code>true</code> if the specified match has no value common.
|
||||
*/
|
||||
public boolean disjoint(Match<?, ?> match) {
|
||||
return (value != match.value && candidate != match.candidate);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("[%s, %s]", value, candidate);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
|
||||
public class Matcher<V, C> {
|
||||
|
||||
private final List<V> values;
|
||||
private final List<C> candidates;
|
||||
|
||||
private final List<SimilarityMetric> metrics;
|
||||
|
||||
private final DisjointMatchCollection<V, C> disjointMatchCollection;
|
||||
|
||||
|
||||
public Matcher(Collection<? extends V> values, Collection<? extends C> candidates, Collection<? extends SimilarityMetric> metrics) {
|
||||
this.values = new LinkedList<V>(values);
|
||||
this.candidates = new LinkedList<C>(candidates);
|
||||
|
||||
this.metrics = new ArrayList<SimilarityMetric>(metrics);
|
||||
|
||||
this.disjointMatchCollection = new DisjointMatchCollection<V, C>();
|
||||
}
|
||||
|
||||
|
||||
public synchronized List<Match<V, C>> match() throws InterruptedException {
|
||||
|
||||
// list of all combinations of values and candidates
|
||||
List<Match<V, C>> possibleMatches = new ArrayList<Match<V, C>>(values.size() * candidates.size());
|
||||
|
||||
// populate with all possible matches
|
||||
for (V value : values) {
|
||||
for (C candidate : candidates) {
|
||||
possibleMatches.add(new Match<V, C>(value, candidate));
|
||||
}
|
||||
}
|
||||
|
||||
// match recursively
|
||||
match(possibleMatches, 0);
|
||||
|
||||
// restore order according to the given values
|
||||
List<Match<V, C>> result = new ArrayList<Match<V, C>>();
|
||||
|
||||
for (V value : values) {
|
||||
Match<V, C> match = disjointMatchCollection.getByValue(value);
|
||||
|
||||
if (match != null) {
|
||||
result.add(match);
|
||||
}
|
||||
}
|
||||
|
||||
// remove matched objects
|
||||
for (Match<V, C> match : result) {
|
||||
values.remove(match.getValue());
|
||||
candidates.remove(match.getCandidate());
|
||||
}
|
||||
|
||||
// clear collected matches
|
||||
disjointMatchCollection.clear();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public List<V> remainingValues() {
|
||||
return Collections.unmodifiableList(values);
|
||||
}
|
||||
|
||||
|
||||
public List<C> remainingCandidates() {
|
||||
return Collections.unmodifiableList(candidates);
|
||||
}
|
||||
|
||||
|
||||
protected void match(Collection<Match<V, C>> possibleMatches, int level) throws InterruptedException {
|
||||
if (level >= metrics.size() || possibleMatches.isEmpty()) {
|
||||
// no further refinement possible
|
||||
disjointMatchCollection.addAll(possibleMatches);
|
||||
return;
|
||||
}
|
||||
|
||||
for (List<Match<V, C>> matchesWithEqualSimilarity : mapBySimilarity(possibleMatches, metrics.get(level)).values()) {
|
||||
// some matches may already be unique
|
||||
List<Match<V, C>> disjointMatches = disjointMatches(matchesWithEqualSimilarity);
|
||||
|
||||
if (!disjointMatches.isEmpty()) {
|
||||
// collect disjoint matches
|
||||
disjointMatchCollection.addAll(disjointMatches);
|
||||
|
||||
// no need for further matching
|
||||
matchesWithEqualSimilarity.removeAll(disjointMatches);
|
||||
}
|
||||
|
||||
// remove invalid matches
|
||||
removeCollected(matchesWithEqualSimilarity);
|
||||
|
||||
// matches are ambiguous, more refined matching required
|
||||
match(matchesWithEqualSimilarity, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void removeCollected(Collection<Match<V, C>> matches) {
|
||||
for (Iterator<Match<V, C>> iterator = matches.iterator(); iterator.hasNext();) {
|
||||
if (!disjointMatchCollection.disjoint(iterator.next()))
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected SortedMap<Float, List<Match<V, C>>> mapBySimilarity(Collection<Match<V, C>> possibleMatches, SimilarityMetric metric) throws InterruptedException {
|
||||
// map sorted by similarity descending
|
||||
SortedMap<Float, List<Match<V, C>>> similarityMap = new TreeMap<Float, List<Match<V, C>>>(Collections.reverseOrder());
|
||||
|
||||
// use metric on all matches
|
||||
for (Match<V, C> possibleMatch : possibleMatches) {
|
||||
float similarity = metric.getSimilarity(possibleMatch.getValue(), possibleMatch.getCandidate());
|
||||
|
||||
List<Match<V, C>> list = similarityMap.get(similarity);
|
||||
|
||||
if (list == null) {
|
||||
list = new ArrayList<Match<V, C>>();
|
||||
similarityMap.put(similarity, list);
|
||||
}
|
||||
|
||||
list.add(possibleMatch);
|
||||
|
||||
// unwind this thread if we have been interrupted
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
return similarityMap;
|
||||
}
|
||||
|
||||
|
||||
protected List<Match<V, C>> disjointMatches(Collection<Match<V, C>> collection) {
|
||||
List<Match<V, C>> disjointMatches = new ArrayList<Match<V, C>>();
|
||||
|
||||
for (Match<V, C> m1 : collection) {
|
||||
boolean disjoint = true;
|
||||
|
||||
for (Match<V, C> m2 : collection) {
|
||||
// ignore same element
|
||||
if (m1 != m2 && !m1.disjoint(m2)) {
|
||||
disjoint = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (disjoint) {
|
||||
disjointMatches.add(m1);
|
||||
}
|
||||
}
|
||||
|
||||
return disjointMatches;
|
||||
}
|
||||
|
||||
|
||||
protected static class DisjointMatchCollection<V, C> extends AbstractList<Match<V, C>> {
|
||||
|
||||
private final List<Match<V, C>> matches;
|
||||
|
||||
private final Map<V, Match<V, C>> values;
|
||||
private final Map<C, Match<V, C>> candidates;
|
||||
|
||||
|
||||
public DisjointMatchCollection() {
|
||||
matches = new ArrayList<Match<V, C>>();
|
||||
values = new IdentityHashMap<V, Match<V, C>>();
|
||||
candidates = new IdentityHashMap<C, Match<V, C>>();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean add(Match<V, C> match) {
|
||||
if (disjoint(match)) {
|
||||
values.put(match.getValue(), match);
|
||||
candidates.put(match.getCandidate(), match);
|
||||
|
||||
return matches.add(match);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean disjoint(Match<V, C> match) {
|
||||
return !values.containsKey(match.getValue()) && !candidates.containsKey(match.getCandidate());
|
||||
}
|
||||
|
||||
|
||||
public Match<V, C> getByValue(V value) {
|
||||
return values.get(value);
|
||||
}
|
||||
|
||||
|
||||
public Match<V, C> getByCandidate(C candidate) {
|
||||
return candidates.get(candidate);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Match<V, C> get(int index) {
|
||||
return matches.get(index);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return matches.size();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
matches.clear();
|
||||
values.clear();
|
||||
candidates.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import static net.sourceforge.filebot.FileBotUtil.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;
|
||||
|
||||
|
||||
public class NameSimilarityMetric implements SimilarityMetric {
|
||||
|
||||
private final AbstractStringMetric metric;
|
||||
|
||||
|
||||
public NameSimilarityMetric() {
|
||||
// MongeElkan metric with a QGram3Extended tokenizer seems to work best for similarity of names
|
||||
metric = new MongeElkan(new TokeniserQGram3Extended());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
return metric.getSimilarity(normalize(o1), normalize(o2));
|
||||
}
|
||||
|
||||
|
||||
protected String normalize(Object object) {
|
||||
// remove embedded checksum from name, if any
|
||||
String name = removeEmbeddedChecksum(object.toString());
|
||||
|
||||
// normalize separators
|
||||
name = name.replaceAll("[\\._ ]+", " ");
|
||||
|
||||
// normalize case and trim
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Similarity of names";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return metric.getShortDescriptionString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName();
|
||||
}
|
||||
}
|
|
@ -1,34 +1,42 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import static net.sourceforge.filebot.FileBotUtil.removeEmbeddedChecksum;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
|
||||
import uk.ac.shef.wit.simmetrics.similaritymetrics.AbstractStringMetric;
|
||||
import uk.ac.shef.wit.simmetrics.similaritymetrics.EuclideanDistance;
|
||||
import uk.ac.shef.wit.simmetrics.similaritymetrics.QGramsDistance;
|
||||
import uk.ac.shef.wit.simmetrics.tokenisers.InterfaceTokeniser;
|
||||
import uk.ac.shef.wit.simmetrics.wordhandlers.DummyStopTermHandler;
|
||||
import uk.ac.shef.wit.simmetrics.wordhandlers.InterfaceTermHandler;
|
||||
|
||||
|
||||
public class NumericSimilarityMetric extends AbstractNameSimilarityMetric {
|
||||
public class NumericSimilarityMetric implements SimilarityMetric {
|
||||
|
||||
private final AbstractStringMetric metric;
|
||||
|
||||
|
||||
public NumericSimilarityMetric() {
|
||||
// I have absolutely no clue as to why, but I get a good matching behavior
|
||||
// when using a numeric tokensier with EuclideanDistance
|
||||
metric = new EuclideanDistance(new NumberTokeniser());
|
||||
// I don't really know why, but I get a good matching behavior
|
||||
// when using QGramsDistance or BlockDistance
|
||||
metric = new QGramsDistance(new NumberTokeniser());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float getSimilarity(String a, String b) {
|
||||
return metric.getSimilarity(a, b);
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
return metric.getSimilarity(normalize(o1), normalize(o2));
|
||||
}
|
||||
|
||||
|
||||
protected String normalize(Object object) {
|
||||
// delete checksum pattern, because it will mess with the number tokens
|
||||
return removeEmbeddedChecksum(object.toString());
|
||||
}
|
||||
|
||||
|
||||
|
@ -43,10 +51,16 @@ public class NumericSimilarityMetric extends AbstractNameSimilarityMetric {
|
|||
return "Numbers";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
private static class NumberTokeniser implements InterfaceTokeniser {
|
||||
|
||||
protected static class NumberTokeniser implements InterfaceTokeniser {
|
||||
|
||||
private static final String delimiter = "(\\D)+";
|
||||
private final String delimiter = "\\D+";
|
||||
|
||||
|
||||
@Override
|
||||
|
@ -54,10 +68,13 @@ public class NumericSimilarityMetric extends AbstractNameSimilarityMetric {
|
|||
ArrayList<String> tokens = new ArrayList<String>();
|
||||
|
||||
Scanner scanner = new Scanner(input);
|
||||
|
||||
// scan for number patterns, use non-number pattern as delimiter
|
||||
scanner.useDelimiter(delimiter);
|
||||
|
||||
while (scanner.hasNextInt()) {
|
||||
tokens.add(Integer.toString(scanner.nextInt()));
|
||||
// remove leading zeros from number tokens by scanning for Integers
|
||||
tokens.add(String.valueOf(scanner.nextInt()));
|
||||
}
|
||||
|
||||
return tokens;
|
|
@ -0,0 +1,171 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class SeasonEpisodeSimilarityMetric implements SimilarityMetric {
|
||||
|
||||
private final NumericSimilarityMetric fallbackMetric = new NumericSimilarityMetric();
|
||||
|
||||
private final SeasonEpisodePattern[] patterns;
|
||||
|
||||
|
||||
public SeasonEpisodeSimilarityMetric() {
|
||||
patterns = new SeasonEpisodePattern[3];
|
||||
|
||||
// match patterns like S01E01, s01e02, ... [s01]_[e02], s01.e02, ...
|
||||
patterns[0] = new SeasonEpisodePattern("(?<!\\p{Alnum})[Ss](\\d{1,2})[^\\p{Alnum}]{0,3}[Ee](\\d{1,3})(?!\\p{Digit})");
|
||||
|
||||
// match patterns like 1x01, 1x02, ... 10x01, 10x02, ...
|
||||
patterns[1] = new SeasonEpisodePattern("(?<!\\p{Alnum})(\\d{1,2})x(\\d{1,3})(?!\\p{Digit})");
|
||||
|
||||
// match patterns like 01, 102, 1003 (enclosed in separators)
|
||||
patterns[2] = new SeasonEpisodePattern("(?<=^|[\\._ ])([0-2]?\\d?)(\\d{2})(?=[\\._ ]|$)");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
List<SxE> sxeVector1 = match(normalize(o1));
|
||||
List<SxE> sxeVector2 = match(normalize(o2));
|
||||
|
||||
if (sxeVector1 == null || sxeVector2 == null) {
|
||||
// name does not match any known pattern, return numeric similarity
|
||||
return fallbackMetric.getSimilarity(o1, o2);
|
||||
}
|
||||
|
||||
if (Collections.disjoint(sxeVector1, sxeVector2)) {
|
||||
// vectors have no episode matches in common
|
||||
return 0;
|
||||
}
|
||||
|
||||
// vectors have at least one episode match in common
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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<SxE> match(String name) {
|
||||
for (SeasonEpisodePattern pattern : patterns) {
|
||||
List<SxE> match = pattern.match(name);
|
||||
|
||||
if (!match.isEmpty()) {
|
||||
// current pattern did match
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected String normalize(Object object) {
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Similarity of season and episode numbers";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Season and Episode";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
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<SxE> match(String name) {
|
||||
// name will probably contain no more than one match, but may contain more
|
||||
List<SxE> matches = new ArrayList<SxE>(1);
|
||||
|
||||
Matcher matcher = pattern.matcher(name);
|
||||
|
||||
while (matcher.find()) {
|
||||
matches.add(new SxE(matcher.group(seasonGroup), matcher.group(episodeGroup)));
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
public interface SimilarityMetric {
|
||||
|
||||
public float getSimilarity(Object o1, Object o2);
|
||||
|
||||
|
||||
public String getDescription();
|
||||
|
||||
|
||||
public String getName();
|
||||
|
||||
}
|
|
@ -189,13 +189,13 @@ public class Torrent {
|
|||
}
|
||||
|
||||
|
||||
public Long getLength() {
|
||||
return length;
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
public Long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -184,8 +184,7 @@ public abstract class AbstractSearchPanel<S, E> extends FileBotPanel {
|
|||
} catch (Exception e) {
|
||||
tab.close();
|
||||
|
||||
Logger.getLogger("ui").warning(ExceptionUtil.getRootCause(e).getMessage());
|
||||
Logger.getLogger("global").log(Level.WARNING, "Search failed", e);
|
||||
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtil.getRootCause(e).getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -241,8 +240,7 @@ public abstract class AbstractSearchPanel<S, E> extends FileBotPanel {
|
|||
} catch (Exception e) {
|
||||
tab.close();
|
||||
|
||||
Logger.getLogger("ui").warning(ExceptionUtil.getRootCause(e).getMessage());
|
||||
Logger.getLogger("global").log(Level.WARNING, "Fetch failed", e);
|
||||
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtil.getRootCause(e).getMessage(), e);
|
||||
} finally {
|
||||
tab.setLoading(false);
|
||||
}
|
||||
|
@ -333,7 +331,7 @@ public abstract class AbstractSearchPanel<S, E> extends FileBotPanel {
|
|||
|
||||
switch (searchResults.size()) {
|
||||
case 0:
|
||||
Logger.getLogger("ui").warning(String.format("\"%s\" has not been found.", request.getSearchText()));
|
||||
Logger.getLogger("ui").warning(String.format("'%s' has not been found.", request.getSearchText()));
|
||||
return null;
|
||||
case 1:
|
||||
return searchResults.iterator().next();
|
||||
|
|
|
@ -18,7 +18,6 @@ import java.util.Iterator;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
|
@ -160,7 +159,6 @@ public class FileTree extends JTree {
|
|||
}
|
||||
} catch (Exception e) {
|
||||
Logger.getLogger("ui").warning(e.getMessage());
|
||||
Logger.getLogger("global").log(Level.SEVERE, "Failed to open file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ public class SplitTool extends Tool<TreeModel> implements ChangeListener {
|
|||
|
||||
|
||||
@Override
|
||||
protected TreeModel createModelInBackground(FolderNode sourceModel, Cancellable cancellable) {
|
||||
protected TreeModel createModelInBackground(FolderNode sourceModel) throws InterruptedException {
|
||||
this.sourceModel = sourceModel;
|
||||
|
||||
FolderNode root = new FolderNode();
|
||||
|
@ -87,7 +87,7 @@ public class SplitTool extends Tool<TreeModel> implements ChangeListener {
|
|||
List<File> remainder = new ArrayList<File>(50);
|
||||
long totalSize = 0;
|
||||
|
||||
for (Iterator<File> iterator = sourceModel.fileIterator(); iterator.hasNext() && !cancellable.isCancelled();) {
|
||||
for (Iterator<File> iterator = sourceModel.fileIterator(); iterator.hasNext();) {
|
||||
File file = iterator.next();
|
||||
|
||||
long fileSize = file.length();
|
||||
|
@ -108,6 +108,11 @@ public class SplitTool extends Tool<TreeModel> implements ChangeListener {
|
|||
|
||||
totalSize += fileSize;
|
||||
currentPart.add(file);
|
||||
|
||||
// unwind thread, if we have been cancelled
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentPart.isEmpty()) {
|
||||
|
|
|
@ -32,7 +32,7 @@ abstract class Tool<M> extends JComponent {
|
|||
|
||||
public synchronized void setSourceModel(FolderNode sourceModel) {
|
||||
if (updateTask != null) {
|
||||
updateTask.cancel(false);
|
||||
updateTask.cancel(true);
|
||||
}
|
||||
|
||||
updateTask = new UpdateModelTask(sourceModel);
|
||||
|
@ -41,13 +41,13 @@ abstract class Tool<M> extends JComponent {
|
|||
}
|
||||
|
||||
|
||||
protected abstract M createModelInBackground(FolderNode sourceModel, Cancellable cancellable);
|
||||
protected abstract M createModelInBackground(FolderNode sourceModel) throws InterruptedException;
|
||||
|
||||
|
||||
protected abstract void setModel(M model);
|
||||
|
||||
|
||||
private class UpdateModelTask extends SwingWorker<M, Void> implements Cancellable {
|
||||
private class UpdateModelTask extends SwingWorker<M, Void> {
|
||||
|
||||
private final FolderNode sourceModel;
|
||||
|
||||
|
@ -67,7 +67,7 @@ abstract class Tool<M> extends JComponent {
|
|||
|
||||
if (!isCancelled()) {
|
||||
firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, false, true);
|
||||
model = createModelInBackground(sourceModel, this);
|
||||
model = createModelInBackground(sourceModel);
|
||||
firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, true, false);
|
||||
}
|
||||
|
||||
|
@ -92,12 +92,6 @@ abstract class Tool<M> extends JComponent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
protected static interface Cancellable {
|
||||
|
||||
boolean isCancelled();
|
||||
}
|
||||
|
||||
|
||||
protected FolderNode createStatisticsNode(String name, List<File> files) {
|
||||
FolderNode folder = new FolderNode(null, files.size());
|
||||
|
|
|
@ -41,10 +41,10 @@ public class TypeTool extends Tool<TreeModel> {
|
|||
|
||||
|
||||
@Override
|
||||
protected TreeModel createModelInBackground(FolderNode sourceModel, Cancellable cancellable) {
|
||||
protected TreeModel createModelInBackground(FolderNode sourceModel) throws InterruptedException {
|
||||
TreeMap<String, List<File>> map = new TreeMap<String, List<File>>();
|
||||
|
||||
for (Iterator<File> iterator = sourceModel.fileIterator(); iterator.hasNext() && !cancellable.isCancelled();) {
|
||||
for (Iterator<File> iterator = sourceModel.fileIterator(); iterator.hasNext();) {
|
||||
File file = iterator.next();
|
||||
String extension = FileUtil.getExtension(file);
|
||||
|
||||
|
@ -62,6 +62,11 @@ public class TypeTool extends Tool<TreeModel> {
|
|||
|
||||
for (Entry<String, List<File>> entry : map.entrySet()) {
|
||||
root.add(createStatisticsNode(entry.getKey(), entry.getValue()));
|
||||
|
||||
// unwind thread, if we have been cancelled
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
return new DefaultTreeModel(root);
|
||||
|
|
|
@ -99,7 +99,7 @@ public class ListPanel extends FileBotPanel {
|
|||
String pattern = textField.getText();
|
||||
|
||||
if (!pattern.contains(INDEX_VARIABLE)) {
|
||||
Logger.getLogger("ui").warning(String.format("Pattern does not contain index variable %s.", INDEX_VARIABLE));
|
||||
Logger.getLogger("ui").warning(String.format("Pattern must contain index variable %s.", INDEX_VARIABLE));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename;
|
||||
|
||||
|
||||
public class AbstractFileEntry {
|
||||
|
||||
private final String name;
|
||||
private final long length;
|
||||
|
||||
|
||||
public AbstractFileEntry(String name, long length) {
|
||||
this.name = name;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.entry;
|
||||
package net.sourceforge.filebot.ui.panel.rename;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
|
@ -10,33 +10,24 @@ import net.sourceforge.tuned.FileUtil;
|
|||
public class FileEntry extends AbstractFileEntry {
|
||||
|
||||
private final File file;
|
||||
|
||||
private final long length;
|
||||
private final String type;
|
||||
|
||||
|
||||
public FileEntry(File file) {
|
||||
super(FileUtil.getFileName(file));
|
||||
super(FileUtil.getFileName(file), file.length());
|
||||
|
||||
this.file = file;
|
||||
this.length = file.length();
|
||||
this.type = FileUtil.getFileType(file);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
|
@ -8,17 +8,15 @@ import java.io.File;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.FileEntry;
|
||||
import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy;
|
||||
import ca.odell.glazedlists.EventList;
|
||||
|
||||
|
||||
class FilesListTransferablePolicy extends FileTransferablePolicy {
|
||||
|
||||
private final EventList<? super FileEntry> model;
|
||||
private final List<? super FileEntry> model;
|
||||
|
||||
|
||||
public FilesListTransferablePolicy(EventList<? super FileEntry> model) {
|
||||
public FilesListTransferablePolicy(List<? super FileEntry> model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@ package net.sourceforge.filebot.ui.panel.rename;
|
|||
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -18,141 +20,121 @@ import javax.swing.SwingUtilities;
|
|||
import javax.swing.SwingWorker;
|
||||
|
||||
import net.sourceforge.filebot.ResourceManager;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.FileEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.matcher.Match;
|
||||
import net.sourceforge.filebot.ui.panel.rename.matcher.Matcher;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.CompositeSimilarityMetric;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.NumericSimilarityMetric;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.SimilarityMetric;
|
||||
import net.sourceforge.tuned.ui.SwingWorkerProgressMonitor;
|
||||
import net.sourceforge.filebot.similarity.LengthEqualsMetric;
|
||||
import net.sourceforge.filebot.similarity.Match;
|
||||
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.tuned.ui.ProgressDialog;
|
||||
import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter;
|
||||
import net.sourceforge.tuned.ui.ProgressDialog.Cancellable;
|
||||
|
||||
|
||||
class MatchAction extends AbstractAction {
|
||||
|
||||
private CompositeSimilarityMetric metrics;
|
||||
private final List<Object> namesModel;
|
||||
private final List<FileEntry> filesModel;
|
||||
|
||||
private final RenameList<ListEntry> namesList;
|
||||
private final RenameList<FileEntry> filesList;
|
||||
|
||||
private boolean matchName2File;
|
||||
|
||||
public static final String MATCH_NAMES_2_FILES_DESCRIPTION = "Match names to files";
|
||||
public static final String MATCH_FILES_2_NAMES_DESCRIPTION = "Match files to names";
|
||||
private final SimilarityMetric[] metrics;
|
||||
|
||||
|
||||
public MatchAction(RenameList<ListEntry> namesList, RenameList<FileEntry> filesList) {
|
||||
super("Match");
|
||||
public MatchAction(List<Object> namesModel, List<FileEntry> filesModel) {
|
||||
super("Match", ResourceManager.getIcon("action.match"));
|
||||
|
||||
this.namesList = namesList;
|
||||
this.filesList = filesList;
|
||||
putValue(SHORT_DESCRIPTION, "Match names to files");
|
||||
|
||||
// length similarity will only effect torrent <-> file matches
|
||||
metrics = new CompositeSimilarityMetric(new NumericSimilarityMetric());
|
||||
this.namesModel = namesModel;
|
||||
this.filesModel = filesModel;
|
||||
|
||||
setMatchName2File(true);
|
||||
metrics = new SimilarityMetric[3];
|
||||
|
||||
// 1. pass: match by file length (fast, but only works when matching torrents or files)
|
||||
metrics[0] = new LengthEqualsMetric() {
|
||||
|
||||
@Override
|
||||
protected long getLength(Object o) {
|
||||
if (o instanceof AbstractFileEntry) {
|
||||
return ((AbstractFileEntry) o).getLength();
|
||||
}
|
||||
|
||||
return super.getLength(o);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. pass: match by season / episode numbers, or generic numeric similarity
|
||||
metrics[1] = new SeasonEpisodeSimilarityMetric();
|
||||
|
||||
// 3. pass: match by generic name similarity (slow, but most matches will have been determined in second pass)
|
||||
metrics[2] = new NameSimilarityMetric();
|
||||
}
|
||||
|
||||
|
||||
public void setMatchName2File(boolean matchName2File) {
|
||||
this.matchName2File = matchName2File;
|
||||
|
||||
if (matchName2File) {
|
||||
putValue(SMALL_ICON, ResourceManager.getIcon("action.match.name2file"));
|
||||
putValue(SHORT_DESCRIPTION, MATCH_NAMES_2_FILES_DESCRIPTION);
|
||||
} else {
|
||||
putValue(SMALL_ICON, ResourceManager.getIcon("action.match.file2name"));
|
||||
putValue(SHORT_DESCRIPTION, MATCH_FILES_2_NAMES_DESCRIPTION);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public CompositeSimilarityMetric getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
|
||||
public boolean isMatchName2File() {
|
||||
return matchName2File;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void actionPerformed(ActionEvent evt) {
|
||||
JComponent source = (JComponent) evt.getSource();
|
||||
JComponent eventSource = (JComponent) evt.getSource();
|
||||
|
||||
SwingUtilities.getRoot(source).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
||||
|
||||
RenameList<ListEntry> primaryList = (RenameList<ListEntry>) (matchName2File ? namesList : filesList);
|
||||
RenameList<ListEntry> secondaryList = (RenameList<ListEntry>) (matchName2File ? filesList : namesList);
|
||||
|
||||
BackgroundMatcher backgroundMatcher = new BackgroundMatcher(primaryList, secondaryList, metrics);
|
||||
SwingWorkerProgressMonitor monitor = new SwingWorkerProgressMonitor(SwingUtilities.getWindowAncestor(source), backgroundMatcher, (Icon) getValue(SMALL_ICON));
|
||||
SwingUtilities.getRoot(eventSource).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
||||
|
||||
BackgroundMatcher backgroundMatcher = new BackgroundMatcher(namesModel, filesModel, Arrays.asList(metrics));
|
||||
backgroundMatcher.execute();
|
||||
|
||||
try {
|
||||
// wait a for little while (matcher might finish within a few seconds)
|
||||
backgroundMatcher.get(monitor.getMillisToPopup(), TimeUnit.MILLISECONDS);
|
||||
// wait a for little while (matcher might finish in less than a second)
|
||||
backgroundMatcher.get(2, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
// matcher will take longer, stop blocking EDT
|
||||
monitor.getProgressDialog().setVisible(true);
|
||||
// matcher will probably take a while
|
||||
ProgressDialog progressDialog = createProgressDialog(SwingUtilities.getWindowAncestor(eventSource), backgroundMatcher);
|
||||
|
||||
// display progress dialog and stop blocking EDT
|
||||
progressDialog.setVisible(true);
|
||||
} catch (Exception e) {
|
||||
Logger.getLogger("global").log(Level.SEVERE, e.toString(), e);
|
||||
}
|
||||
|
||||
SwingUtilities.getRoot(source).setCursor(Cursor.getDefaultCursor());
|
||||
SwingUtilities.getRoot(eventSource).setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
|
||||
|
||||
protected ProgressDialog createProgressDialog(Window parent, final BackgroundMatcher worker) {
|
||||
final ProgressDialog progressDialog = new ProgressDialog(parent, worker);
|
||||
|
||||
// configure dialog
|
||||
progressDialog.setTitle("Matching...");
|
||||
progressDialog.setNote("Processing...");
|
||||
progressDialog.setIcon((Icon) getValue(SMALL_ICON));
|
||||
|
||||
// close progress dialog when worker is finished
|
||||
worker.addPropertyChangeListener(new SwingWorkerPropertyChangeAdapter() {
|
||||
|
||||
@Override
|
||||
protected void done(PropertyChangeEvent evt) {
|
||||
progressDialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
|
||||
private static class BackgroundMatcher extends SwingWorker<List<Match>, Void> {
|
||||
protected static class BackgroundMatcher extends SwingWorker<List<Match<Object, FileEntry>>, Void> implements Cancellable {
|
||||
|
||||
private final RenameList<ListEntry> primaryList;
|
||||
private final RenameList<ListEntry> secondaryList;
|
||||
private final List<Object> namesModel;
|
||||
private final List<FileEntry> filesModel;
|
||||
|
||||
private final Matcher matcher;
|
||||
private final Matcher<Object, FileEntry> matcher;
|
||||
|
||||
|
||||
public BackgroundMatcher(RenameList<ListEntry> primaryList, RenameList<ListEntry> secondaryList, SimilarityMetric similarityMetric) {
|
||||
this.primaryList = primaryList;
|
||||
this.secondaryList = secondaryList;
|
||||
public BackgroundMatcher(List<Object> namesModel, List<FileEntry> filesModel, List<SimilarityMetric> metrics) {
|
||||
this.namesModel = namesModel;
|
||||
this.filesModel = filesModel;
|
||||
|
||||
matcher = new Matcher(primaryList.getEntries(), secondaryList.getEntries(), similarityMetric);
|
||||
this.matcher = new Matcher<Object, FileEntry>(namesModel, filesModel, metrics);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<Match> doInBackground() throws Exception {
|
||||
|
||||
firePropertyChange(SwingWorkerProgressMonitor.PROPERTY_TITLE, null, "Matching...");
|
||||
|
||||
int total = matcher.remainingMatches();
|
||||
|
||||
List<Match> matches = new ArrayList<Match>(total);
|
||||
|
||||
while (matcher.hasNext() && !isCancelled()) {
|
||||
firePropertyChange(SwingWorkerProgressMonitor.PROPERTY_NOTE, null, getNote());
|
||||
|
||||
matches.add(matcher.next());
|
||||
|
||||
setProgress((matches.size() * 100) / total);
|
||||
firePropertyChange(SwingWorkerProgressMonitor.PROPERTY_PROGRESS_STRING, null, String.format("%d / %d", matches.size(), total));
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
|
||||
private String getNote() {
|
||||
ListEntry current = matcher.getFirstPrimaryEntry();
|
||||
|
||||
if (current == null)
|
||||
current = matcher.getFirstSecondaryEntry();
|
||||
|
||||
if (current == null)
|
||||
return "";
|
||||
|
||||
return current.getName();
|
||||
protected List<Match<Object, FileEntry>> doInBackground() throws Exception {
|
||||
return matcher.match();
|
||||
}
|
||||
|
||||
|
||||
|
@ -162,23 +144,29 @@ class MatchAction extends AbstractAction {
|
|||
return;
|
||||
|
||||
try {
|
||||
List<Match> matches = get();
|
||||
List<Match<Object, FileEntry>> matches = get();
|
||||
|
||||
primaryList.getModel().clear();
|
||||
secondaryList.getModel().clear();
|
||||
for (Match match : matches) {
|
||||
primaryList.getModel().add(match.getA());
|
||||
secondaryList.getModel().add(match.getB());
|
||||
namesModel.clear();
|
||||
filesModel.clear();
|
||||
|
||||
for (Match<Object, FileEntry> match : matches) {
|
||||
namesModel.add(match.getValue());
|
||||
filesModel.add(match.getCandidate());
|
||||
}
|
||||
|
||||
primaryList.getModel().addAll(matcher.getPrimaryList());
|
||||
secondaryList.getModel().addAll(matcher.getSecondaryList());
|
||||
|
||||
namesModel.addAll(matcher.remainingValues());
|
||||
namesModel.addAll(matcher.remainingCandidates());
|
||||
} catch (Exception e) {
|
||||
Logger.getLogger("global").log(Level.SEVERE, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return cancel(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,39 +2,34 @@
|
|||
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.isInvalidFileName;
|
||||
import static net.sourceforge.tuned.FileUtil.getNameWithoutExtension;
|
||||
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.io.BufferedReader;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import net.sourceforge.filebot.torrent.Torrent;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.StringEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.TorrentEntry;
|
||||
import net.sourceforge.filebot.ui.transfer.StringTransferablePolicy;
|
||||
|
||||
|
||||
class NamesListTransferablePolicy extends FilesListTransferablePolicy {
|
||||
|
||||
private final RenameList<ListEntry> list;
|
||||
|
||||
private final TextPolicy textPolicy = new TextPolicy();
|
||||
private final RenameList<Object> list;
|
||||
|
||||
|
||||
public NamesListTransferablePolicy(RenameList<ListEntry> list) {
|
||||
public NamesListTransferablePolicy(RenameList<Object> list) {
|
||||
super(list.getModel());
|
||||
|
||||
this.list = list;
|
||||
|
@ -43,24 +38,39 @@ class NamesListTransferablePolicy extends FilesListTransferablePolicy {
|
|||
|
||||
@Override
|
||||
public boolean accept(Transferable tr) {
|
||||
return textPolicy.accept(tr) || super.accept(tr);
|
||||
return tr.isDataFlavorSupported(stringFlavor) || super.accept(tr);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleTransferable(Transferable tr, TransferAction action) {
|
||||
if (super.accept(tr))
|
||||
super.handleTransferable(tr, action);
|
||||
else if (textPolicy.accept(tr))
|
||||
textPolicy.handleTransferable(tr, action);
|
||||
if (action == TransferAction.PUT) {
|
||||
clear();
|
||||
}
|
||||
|
||||
if (tr.isDataFlavorSupported(stringFlavor)) {
|
||||
// string transferable
|
||||
try {
|
||||
load((String) tr.getTransferData(stringFlavor));
|
||||
} catch (UnsupportedFlavorException e) {
|
||||
// should not happen
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
// should not happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else if (super.accept(tr)) {
|
||||
// file transferable
|
||||
load(getFilesFromTransferable(tr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void submit(List<ListEntry> entries) {
|
||||
List<ListEntry> invalidEntries = new ArrayList<ListEntry>();
|
||||
protected void submit(List<StringEntry> entries) {
|
||||
List<StringEntry> invalidEntries = new ArrayList<StringEntry>();
|
||||
|
||||
for (ListEntry entry : entries) {
|
||||
if (isInvalidFileName(entry.getName()))
|
||||
for (StringEntry entry : entries) {
|
||||
if (isInvalidFileName(entry.getValue()))
|
||||
invalidEntries.add(entry);
|
||||
}
|
||||
|
||||
|
@ -68,17 +78,35 @@ class NamesListTransferablePolicy extends FilesListTransferablePolicy {
|
|||
ValidateNamesDialog dialog = new ValidateNamesDialog(SwingUtilities.getWindowAncestor(list), invalidEntries);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isCancelled())
|
||||
if (dialog.isCancelled()) {
|
||||
// return immediately, don't add items to list
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
list.getModel().addAll(entries);
|
||||
}
|
||||
|
||||
|
||||
protected void load(String string) {
|
||||
List<StringEntry> entries = new ArrayList<StringEntry>();
|
||||
|
||||
Scanner scanner = new Scanner(string).useDelimiter(LINE_SEPARATOR);
|
||||
|
||||
while (scanner.hasNext()) {
|
||||
String line = scanner.next();
|
||||
|
||||
if (line.trim().length() > 0) {
|
||||
entries.add(new StringEntry(line));
|
||||
}
|
||||
}
|
||||
|
||||
submit(entries);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void load(List<File> files) {
|
||||
|
||||
if (containsOnly(files, LIST_FILE_EXTENSIONS)) {
|
||||
loadListFiles(files);
|
||||
} else if (containsOnly(files, TORRENT_FILE_EXTENSIONS)) {
|
||||
|
@ -91,20 +119,20 @@ class NamesListTransferablePolicy extends FilesListTransferablePolicy {
|
|||
|
||||
private void loadListFiles(List<File> files) {
|
||||
try {
|
||||
List<ListEntry> entries = new ArrayList<ListEntry>();
|
||||
List<StringEntry> entries = new ArrayList<StringEntry>();
|
||||
|
||||
for (File file : files) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
|
||||
Scanner scanner = new Scanner(file, "UTF-8").useDelimiter(LINE_SEPARATOR);
|
||||
|
||||
String line = null;
|
||||
|
||||
while ((line = in.readLine()) != null) {
|
||||
while (scanner.hasNext()) {
|
||||
String line = scanner.next();
|
||||
|
||||
if (line.trim().length() > 0) {
|
||||
entries.add(new StringEntry(line));
|
||||
}
|
||||
}
|
||||
|
||||
in.close();
|
||||
scanner.close();
|
||||
}
|
||||
|
||||
submit(entries);
|
||||
|
@ -116,17 +144,18 @@ class NamesListTransferablePolicy extends FilesListTransferablePolicy {
|
|||
|
||||
private void loadTorrentFiles(List<File> files) {
|
||||
try {
|
||||
List<ListEntry> entries = new ArrayList<ListEntry>();
|
||||
List<AbstractFileEntry> entries = new ArrayList<AbstractFileEntry>();
|
||||
|
||||
for (File file : files) {
|
||||
Torrent torrent = new Torrent(file);
|
||||
|
||||
for (Torrent.Entry entry : torrent.getFiles()) {
|
||||
entries.add(new TorrentEntry(entry));
|
||||
entries.add(new AbstractFileEntry(getNameWithoutExtension(entry.getName()), entry.getLength()));
|
||||
}
|
||||
}
|
||||
|
||||
submit(entries);
|
||||
// add torrent entries directly without checking file names for invalid characters
|
||||
list.getModel().addAll(entries);
|
||||
} catch (IOException e) {
|
||||
Logger.getLogger("global").log(Level.SEVERE, e.toString(), e);
|
||||
}
|
||||
|
@ -138,32 +167,4 @@ class NamesListTransferablePolicy extends FilesListTransferablePolicy {
|
|||
return "text files and torrent files";
|
||||
}
|
||||
|
||||
|
||||
private class TextPolicy extends StringTransferablePolicy {
|
||||
|
||||
@Override
|
||||
protected void clear() {
|
||||
NamesListTransferablePolicy.this.clear();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void load(String string) {
|
||||
List<ListEntry> entries = new ArrayList<ListEntry>();
|
||||
|
||||
String[] lines = string.split("\r?\n");
|
||||
|
||||
for (String line : lines) {
|
||||
|
||||
if (!line.isEmpty())
|
||||
entries.add(new StringEntry(line));
|
||||
}
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
submit(entries);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,63 +4,84 @@ package net.sourceforge.filebot.ui.panel.rename;
|
|||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Action;
|
||||
|
||||
import net.sourceforge.filebot.ResourceManager;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.FileEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.filebot.similarity.Match;
|
||||
import net.sourceforge.tuned.FileUtil;
|
||||
|
||||
|
||||
public class RenameAction extends AbstractAction {
|
||||
|
||||
private final RenameList<ListEntry> namesList;
|
||||
private final RenameList<FileEntry> filesList;
|
||||
private final List<Object> namesModel;
|
||||
private final List<FileEntry> filesModel;
|
||||
|
||||
|
||||
public RenameAction(RenameList<ListEntry> namesList, RenameList<FileEntry> filesList) {
|
||||
public RenameAction(List<Object> namesModel, List<FileEntry> filesModel) {
|
||||
super("Rename", ResourceManager.getIcon("action.rename"));
|
||||
this.namesList = namesList;
|
||||
this.filesList = filesList;
|
||||
|
||||
putValue(Action.SHORT_DESCRIPTION, "Rename files");
|
||||
putValue(SHORT_DESCRIPTION, "Rename files");
|
||||
|
||||
this.namesModel = namesModel;
|
||||
this.filesModel = filesModel;
|
||||
}
|
||||
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
List<ListEntry> nameEntries = namesList.getEntries();
|
||||
List<FileEntry> fileEntries = filesList.getEntries();
|
||||
public void actionPerformed(ActionEvent evt) {
|
||||
|
||||
int minLength = Math.min(nameEntries.size(), fileEntries.size());
|
||||
Deque<Match<File, File>> renameMatches = new ArrayDeque<Match<File, File>>();
|
||||
Deque<Match<File, File>> revertMatches = new ArrayDeque<Match<File, File>>();
|
||||
|
||||
int i = 0;
|
||||
int errors = 0;
|
||||
Iterator<Object> names = namesModel.iterator();
|
||||
Iterator<FileEntry> files = filesModel.iterator();
|
||||
|
||||
for (i = 0; i < minLength; i++) {
|
||||
FileEntry fileEntry = fileEntries.get(i);
|
||||
File f = fileEntry.getFile();
|
||||
while (names.hasNext() && files.hasNext()) {
|
||||
File source = files.next().getFile();
|
||||
|
||||
String newName = nameEntries.get(i).toString() + FileUtil.getExtension(f, true);
|
||||
String targetName = names.next().toString() + FileUtil.getExtension(source, true);
|
||||
File target = new File(source.getParentFile(), targetName);
|
||||
|
||||
File newFile = new File(f.getParentFile(), newName);
|
||||
renameMatches.addLast(new Match<File, File>(source, target));
|
||||
}
|
||||
|
||||
try {
|
||||
int renameCount = renameMatches.size();
|
||||
|
||||
if (f.renameTo(newFile)) {
|
||||
filesList.getModel().remove(fileEntry);
|
||||
} else {
|
||||
errors++;
|
||||
for (Match<File, File> match : renameMatches) {
|
||||
// rename file
|
||||
if (!match.getValue().renameTo(match.getCandidate()))
|
||||
throw new IOException(String.format("Failed to rename file: %s.", match.getValue().getName()));
|
||||
|
||||
// revert in reverse order if renaming of all matches fails
|
||||
revertMatches.addFirst(match);
|
||||
}
|
||||
|
||||
// renamed all matches successfully
|
||||
Logger.getLogger("ui").info(String.format("%d files renamed.", renameCount));
|
||||
} catch (IOException e) {
|
||||
// rename failed
|
||||
Logger.getLogger("ui").warning(e.getMessage());
|
||||
|
||||
boolean revertFailed = false;
|
||||
|
||||
// revert rename operations
|
||||
for (Match<File, File> match : revertMatches) {
|
||||
if (!match.getCandidate().renameTo(match.getValue())) {
|
||||
revertFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (revertFailed) {
|
||||
Logger.getLogger("ui").severe("Failed to revert all rename operations.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors > 0)
|
||||
Logger.getLogger("ui").info(String.format("%d of %d files renamed.", i - errors, i));
|
||||
else
|
||||
Logger.getLogger("ui").info(String.format("%d files renamed.", i));
|
||||
|
||||
namesList.repaint();
|
||||
filesList.repaint();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,11 @@ import javax.swing.ListSelectionModel;
|
|||
import net.miginfocom.swing.MigLayout;
|
||||
import net.sourceforge.filebot.ResourceManager;
|
||||
import net.sourceforge.filebot.ui.FileBotList;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.filebot.ui.transfer.LoadAction;
|
||||
import net.sourceforge.filebot.ui.transfer.TransferablePolicy;
|
||||
|
||||
|
||||
class RenameList<E extends ListEntry> extends FileBotList<E> {
|
||||
class RenameList<E> extends FileBotList<E> {
|
||||
|
||||
private JButton loadButton = new JButton();
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import javax.swing.ListModel;
|
|||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.FileEntry;
|
||||
|
||||
import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer;
|
||||
|
||||
|
||||
|
|
|
@ -2,44 +2,30 @@
|
|||
package net.sourceforge.filebot.ui.panel.rename;
|
||||
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.DefaultListSelectionModel;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JViewport;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.ListDataEvent;
|
||||
import javax.swing.event.ListDataListener;
|
||||
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import net.sourceforge.filebot.ResourceManager;
|
||||
import net.sourceforge.filebot.ui.FileBotPanel;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.FileEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import ca.odell.glazedlists.event.ListEvent;
|
||||
import ca.odell.glazedlists.event.ListEventListener;
|
||||
|
||||
|
||||
public class RenamePanel extends FileBotPanel {
|
||||
|
||||
private RenameList<ListEntry> namesList = new RenameList<ListEntry>();
|
||||
private RenameList<Object> namesList = new RenameList<Object>();
|
||||
private RenameList<FileEntry> filesList = new RenameList<FileEntry>();
|
||||
|
||||
private MatchAction matchAction = new MatchAction(namesList, filesList);
|
||||
private MatchAction matchAction = new MatchAction(namesList.getModel(), filesList.getModel());
|
||||
|
||||
private RenameAction renameAction = new RenameAction(namesList, filesList);
|
||||
|
||||
private SimilarityPanel similarityPanel;
|
||||
|
||||
private ViewPortSynchronizer viewPortSynchroniser;
|
||||
private RenameAction renameAction = new RenameAction(namesList.getModel(), filesList.getModel());
|
||||
|
||||
|
||||
public RenamePanel() {
|
||||
|
@ -65,16 +51,11 @@ public class RenamePanel extends FileBotPanel {
|
|||
namesListComponent.setSelectionModel(selectionModel);
|
||||
filesListComponent.setSelectionModel(selectionModel);
|
||||
|
||||
viewPortSynchroniser = new ViewPortSynchronizer((JViewport) namesListComponent.getParent(), (JViewport) filesListComponent.getParent());
|
||||
|
||||
similarityPanel = new SimilarityPanel(namesListComponent, filesListComponent);
|
||||
|
||||
similarityPanel.setVisible(false);
|
||||
similarityPanel.setMetrics(matchAction.getMetrics());
|
||||
// synchronize viewports
|
||||
new ViewPortSynchronizer((JViewport) namesListComponent.getParent(), (JViewport) filesListComponent.getParent());
|
||||
|
||||
// create Match button
|
||||
JButton matchButton = new JButton(matchAction);
|
||||
matchButton.addMouseListener(new MatchPopupListener());
|
||||
matchButton.setVerticalTextPosition(SwingConstants.BOTTOM);
|
||||
matchButton.setHorizontalTextPosition(SwingConstants.CENTER);
|
||||
|
||||
|
@ -96,123 +77,19 @@ public class RenamePanel extends FileBotPanel {
|
|||
|
||||
add(filesList, "grow");
|
||||
|
||||
namesListComponent.getModel().addListDataListener(repaintOnDataChange);
|
||||
filesListComponent.getModel().addListDataListener(repaintOnDataChange);
|
||||
namesList.getModel().addListEventListener(new RepaintHandler<Object>());
|
||||
filesList.getModel().addListEventListener(new RepaintHandler<FileEntry>());
|
||||
}
|
||||
|
||||
private final ListDataListener repaintOnDataChange = new ListDataListener() {
|
||||
|
||||
private class RepaintHandler<E> implements ListEventListener<E> {
|
||||
|
||||
public void contentsChanged(ListDataEvent e) {
|
||||
repaintBoth();
|
||||
}
|
||||
|
||||
|
||||
public void intervalAdded(ListDataEvent e) {
|
||||
repaintBoth();
|
||||
}
|
||||
|
||||
|
||||
public void intervalRemoved(ListDataEvent e) {
|
||||
repaintBoth();
|
||||
}
|
||||
|
||||
|
||||
private void repaintBoth() {
|
||||
@Override
|
||||
public void listChanged(ListEvent<E> listChanges) {
|
||||
namesList.repaint();
|
||||
filesList.repaint();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
private class MatcherSelectPopup extends JPopupMenu {
|
||||
|
||||
public MatcherSelectPopup() {
|
||||
JMenuItem names2files = new JMenuItem(new SetNames2FilesAction(true));
|
||||
JMenuItem files2names = new JMenuItem(new SetNames2FilesAction(false));
|
||||
|
||||
if (matchAction.isMatchName2File())
|
||||
highlight(names2files);
|
||||
else
|
||||
highlight(files2names);
|
||||
|
||||
add(names2files);
|
||||
add(files2names);
|
||||
|
||||
addSeparator();
|
||||
add(new ToggleSimilarityAction(!similarityPanel.isVisible()));
|
||||
}
|
||||
|
||||
|
||||
public void highlight(JMenuItem item) {
|
||||
item.setFont(item.getFont().deriveFont(Font.BOLD));
|
||||
}
|
||||
|
||||
|
||||
private class SetNames2FilesAction extends AbstractAction {
|
||||
|
||||
private boolean names2files;
|
||||
|
||||
|
||||
public SetNames2FilesAction(boolean names2files) {
|
||||
this.names2files = names2files;
|
||||
|
||||
if (names2files) {
|
||||
putValue(SMALL_ICON, ResourceManager.getIcon("action.match.name2file"));
|
||||
putValue(NAME, MatchAction.MATCH_NAMES_2_FILES_DESCRIPTION);
|
||||
} else {
|
||||
putValue(SMALL_ICON, ResourceManager.getIcon("action.match.file2name"));
|
||||
putValue(NAME, MatchAction.MATCH_FILES_2_NAMES_DESCRIPTION);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
matchAction.setMatchName2File(names2files);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class ToggleSimilarityAction extends AbstractAction {
|
||||
|
||||
private boolean showSimilarityPanel;
|
||||
|
||||
|
||||
public ToggleSimilarityAction(boolean showSimilarityPanel) {
|
||||
this.showSimilarityPanel = showSimilarityPanel;
|
||||
|
||||
if (showSimilarityPanel) {
|
||||
putValue(NAME, "Show Similarity");
|
||||
} else {
|
||||
putValue(NAME, "Hide Similarity");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (showSimilarityPanel) {
|
||||
viewPortSynchroniser.setEnabled(false);
|
||||
similarityPanel.hook();
|
||||
similarityPanel.setVisible(true);
|
||||
} else {
|
||||
similarityPanel.setVisible(false);
|
||||
similarityPanel.unhook();
|
||||
viewPortSynchroniser.setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class MatchPopupListener extends MouseAdapter {
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
MatcherSelectPopup popup = new MatcherSelectPopup();
|
||||
popup.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename;
|
||||
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.GridLayout;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.CompositeSimilarityMetric;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.SimilarityMetric;
|
||||
import net.sourceforge.tuned.ui.notification.SeparatorBorder;
|
||||
|
||||
|
||||
class SimilarityPanel extends Box {
|
||||
|
||||
private JPanel grid = new JPanel(new GridLayout(0, 2, 25, 1));
|
||||
|
||||
private JList nameList;
|
||||
|
||||
private JList fileList;
|
||||
|
||||
private UpdateMetricsListener updateMetricsListener = new UpdateMetricsListener();
|
||||
|
||||
private NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||
|
||||
private List<MetricUpdater> updaterList = new ArrayList<MetricUpdater>();
|
||||
|
||||
private Border labelMarginBorder = BorderFactory.createEmptyBorder(0, 3, 0, 0);
|
||||
|
||||
private Border separatorBorder = new SeparatorBorder(1, new Color(0xACA899), SeparatorBorder.Position.TOP);
|
||||
|
||||
|
||||
public SimilarityPanel(JList nameList, JList fileList) {
|
||||
super(BoxLayout.PAGE_AXIS);
|
||||
|
||||
this.nameList = nameList;
|
||||
this.fileList = fileList;
|
||||
|
||||
numberFormat.setMinimumFractionDigits(2);
|
||||
numberFormat.setMaximumFractionDigits(2);
|
||||
|
||||
Box subBox = Box.createVerticalBox();
|
||||
|
||||
add(subBox);
|
||||
add(Box.createVerticalStrut(15));
|
||||
|
||||
subBox.add(grid);
|
||||
|
||||
subBox.setBorder(BorderFactory.createTitledBorder("Similarity"));
|
||||
|
||||
Border pane = BorderFactory.createLineBorder(Color.LIGHT_GRAY);
|
||||
Border margin = BorderFactory.createEmptyBorder(5, 5, 5, 5);
|
||||
|
||||
grid.setBorder(BorderFactory.createCompoundBorder(pane, margin));
|
||||
grid.setBackground(Color.WHITE);
|
||||
grid.setOpaque(true);
|
||||
}
|
||||
|
||||
|
||||
public void setMetrics(CompositeSimilarityMetric metrics) {
|
||||
grid.removeAll();
|
||||
updaterList.clear();
|
||||
|
||||
for (SimilarityMetric metric : metrics) {
|
||||
JLabel name = new JLabel(metric.getName());
|
||||
name.setToolTipText(metric.getDescription());
|
||||
|
||||
JLabel value = new JLabel();
|
||||
|
||||
name.setBorder(labelMarginBorder);
|
||||
value.setBorder(labelMarginBorder);
|
||||
|
||||
MetricUpdater updater = new MetricUpdater(value, metric);
|
||||
updaterList.add(updater);
|
||||
|
||||
grid.add(name);
|
||||
grid.add(value);
|
||||
}
|
||||
|
||||
JLabel name = new JLabel(metrics.getName());
|
||||
|
||||
JLabel value = new JLabel();
|
||||
|
||||
MetricUpdater updater = new MetricUpdater(value, metrics);
|
||||
updaterList.add(updater);
|
||||
|
||||
Border border = BorderFactory.createCompoundBorder(separatorBorder, labelMarginBorder);
|
||||
name.setBorder(border);
|
||||
value.setBorder(border);
|
||||
|
||||
grid.add(name);
|
||||
grid.add(value);
|
||||
}
|
||||
|
||||
|
||||
public void hook() {
|
||||
updateMetrics();
|
||||
nameList.addListSelectionListener(updateMetricsListener);
|
||||
fileList.addListSelectionListener(updateMetricsListener);
|
||||
}
|
||||
|
||||
|
||||
public void unhook() {
|
||||
nameList.removeListSelectionListener(updateMetricsListener);
|
||||
fileList.removeListSelectionListener(updateMetricsListener);
|
||||
}
|
||||
|
||||
private ListEntry lastListEntryA = null;
|
||||
|
||||
private ListEntry lastListEntryB = null;
|
||||
|
||||
|
||||
public void updateMetrics() {
|
||||
ListEntry a = (ListEntry) nameList.getSelectedValue();
|
||||
ListEntry b = (ListEntry) fileList.getSelectedValue();
|
||||
|
||||
if ((a == lastListEntryA) && (b == lastListEntryB))
|
||||
return;
|
||||
|
||||
lastListEntryA = a;
|
||||
lastListEntryB = b;
|
||||
|
||||
boolean reset = ((a == null) || (b == null));
|
||||
|
||||
for (MetricUpdater updater : updaterList) {
|
||||
if (!reset)
|
||||
updater.update(a, b);
|
||||
else
|
||||
updater.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class UpdateMetricsListener implements ListSelectionListener {
|
||||
|
||||
public void valueChanged(ListSelectionEvent e) {
|
||||
if (e.getValueIsAdjusting())
|
||||
return;
|
||||
|
||||
updateMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class MetricUpdater {
|
||||
|
||||
private JLabel value;
|
||||
|
||||
private SimilarityMetric metric;
|
||||
|
||||
|
||||
public MetricUpdater(JLabel value, SimilarityMetric metric) {
|
||||
this.value = value;
|
||||
this.metric = metric;
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
|
||||
public void update(ListEntry a, ListEntry b) {
|
||||
value.setText(numberFormat.format(metric.getSimilarity(a, b)));
|
||||
}
|
||||
|
||||
|
||||
public void reset() {
|
||||
value.setText(numberFormat.format(0));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename;
|
||||
|
||||
|
||||
public class StringEntry {
|
||||
|
||||
private String value;
|
||||
|
||||
|
||||
public StringEntry(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
}
|
|
@ -25,23 +25,22 @@ import javax.swing.KeyStroke;
|
|||
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import net.sourceforge.filebot.ResourceManager;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.tuned.ui.ArrayListModel;
|
||||
import net.sourceforge.tuned.ui.TunedUtil;
|
||||
|
||||
|
||||
public class ValidateNamesDialog extends JDialog {
|
||||
|
||||
private final Collection<ListEntry> entries;
|
||||
private final Collection<StringEntry> entries;
|
||||
|
||||
private boolean cancelled = true;
|
||||
|
||||
private final ValidateAction validateAction = new ValidateAction();
|
||||
private final ContinueAction continueAction = new ContinueAction();
|
||||
private final CancelAction cancelAction = new CancelAction();
|
||||
protected final Action validateAction = new ValidateAction();
|
||||
protected final Action continueAction = new ContinueAction();
|
||||
protected final Action cancelAction = new CancelAction();
|
||||
|
||||
|
||||
public ValidateNamesDialog(Window owner, Collection<ListEntry> entries) {
|
||||
public ValidateNamesDialog(Window owner, Collection<StringEntry> entries) {
|
||||
super(owner, "Invalid Names", ModalityType.DOCUMENT_MODAL);
|
||||
|
||||
this.entries = entries;
|
||||
|
@ -95,8 +94,8 @@ public class ValidateNamesDialog extends JDialog {
|
|||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
for (ListEntry entry : entries) {
|
||||
entry.setName(validateFileName(entry.getName()));
|
||||
for (StringEntry entry : entries) {
|
||||
entry.setValue(validateFileName(entry.getValue()));
|
||||
}
|
||||
|
||||
setEnabled(false);
|
||||
|
@ -127,7 +126,7 @@ public class ValidateNamesDialog extends JDialog {
|
|||
};
|
||||
|
||||
|
||||
private class CancelAction extends AbstractAction {
|
||||
protected class CancelAction extends AbstractAction {
|
||||
|
||||
public CancelAction() {
|
||||
super("Cancel", ResourceManager.getIcon("dialog.cancel"));
|
||||
|
@ -140,7 +139,7 @@ public class ValidateNamesDialog extends JDialog {
|
|||
};
|
||||
|
||||
|
||||
private static class AlphaButton extends JButton {
|
||||
protected static class AlphaButton extends JButton {
|
||||
|
||||
private float alpha;
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.entry;
|
||||
|
||||
|
||||
public abstract class AbstractFileEntry extends ListEntry {
|
||||
|
||||
public AbstractFileEntry(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
|
||||
public abstract long getLength();
|
||||
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.entry;
|
||||
|
||||
|
||||
public class ListEntry {
|
||||
|
||||
private String name;
|
||||
|
||||
|
||||
public ListEntry(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.entry;
|
||||
|
||||
|
||||
public class StringEntry extends ListEntry {
|
||||
|
||||
public StringEntry(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.entry;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.torrent.Torrent.Entry;
|
||||
import net.sourceforge.tuned.FileUtil;
|
||||
|
||||
|
||||
public class TorrentEntry extends AbstractFileEntry {
|
||||
|
||||
private final Entry entry;
|
||||
|
||||
|
||||
public TorrentEntry(Entry entry) {
|
||||
super(FileUtil.getNameWithoutExtension(entry.getName()));
|
||||
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getLength() {
|
||||
return entry.getLength();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.matcher;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
|
||||
|
||||
public class Match {
|
||||
|
||||
private final ListEntry a;
|
||||
private final ListEntry b;
|
||||
|
||||
|
||||
public Match(ListEntry a, ListEntry b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
|
||||
public ListEntry getA() {
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
public ListEntry getB() {
|
||||
return b;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.matcher;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.SimilarityMetric;
|
||||
|
||||
|
||||
public class Matcher implements Iterator<Match> {
|
||||
|
||||
private final LinkedList<ListEntry> primaryList;
|
||||
private final LinkedList<ListEntry> secondaryList;
|
||||
private final SimilarityMetric similarityMetric;
|
||||
|
||||
|
||||
public Matcher(List<? extends ListEntry> primaryList, List<? extends ListEntry> secondaryList, SimilarityMetric similarityMetric) {
|
||||
this.primaryList = new LinkedList<ListEntry>(primaryList);
|
||||
this.secondaryList = new LinkedList<ListEntry>(secondaryList);
|
||||
this.similarityMetric = similarityMetric;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return remainingMatches() > 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Match next() {
|
||||
ListEntry primaryEntry = primaryList.removeFirst();
|
||||
|
||||
float maxSimilarity = -1;
|
||||
ListEntry mostSimilarSecondaryEntry = null;
|
||||
|
||||
for (ListEntry secondaryEntry : secondaryList) {
|
||||
float similarity = similarityMetric.getSimilarity(primaryEntry, secondaryEntry);
|
||||
|
||||
if (similarity > maxSimilarity) {
|
||||
maxSimilarity = similarity;
|
||||
mostSimilarSecondaryEntry = secondaryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
if (mostSimilarSecondaryEntry != null) {
|
||||
secondaryList.remove(mostSimilarSecondaryEntry);
|
||||
}
|
||||
|
||||
return new Match(primaryEntry, mostSimilarSecondaryEntry);
|
||||
}
|
||||
|
||||
|
||||
public ListEntry getFirstPrimaryEntry() {
|
||||
if (primaryList.isEmpty())
|
||||
return null;
|
||||
|
||||
return primaryList.getFirst();
|
||||
}
|
||||
|
||||
|
||||
public ListEntry getFirstSecondaryEntry() {
|
||||
if (secondaryList.isEmpty())
|
||||
return null;
|
||||
|
||||
return secondaryList.getFirst();
|
||||
}
|
||||
|
||||
|
||||
public int remainingMatches() {
|
||||
return Math.min(primaryList.size(), secondaryList.size());
|
||||
}
|
||||
|
||||
|
||||
public List<ListEntry> getPrimaryList() {
|
||||
return Collections.unmodifiableList(primaryList);
|
||||
}
|
||||
|
||||
|
||||
public List<ListEntry> getSecondaryList() {
|
||||
return Collections.unmodifiableList(secondaryList);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The remove operation is not supported by this implementation of <code>Iterator</code>.
|
||||
*
|
||||
* @throws UnsupportedOperationException if this method is invoked.
|
||||
* @see java.util.Iterator
|
||||
*/
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
|
||||
|
||||
public abstract class AbstractNameSimilarityMetric implements SimilarityMetric {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(ListEntry a, ListEntry b) {
|
||||
return getSimilarity(normalize(a.getName()), normalize(b.getName()));
|
||||
}
|
||||
|
||||
|
||||
protected String normalize(String name) {
|
||||
name = stripChecksum(name);
|
||||
name = normalizeSeparators(name);
|
||||
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
protected String normalizeSeparators(String name) {
|
||||
return name.replaceAll("[\\._ ]+", " ");
|
||||
}
|
||||
|
||||
|
||||
protected String stripChecksum(String name) {
|
||||
return name.replaceAll("\\[\\p{XDigit}{8}\\]", "");
|
||||
}
|
||||
|
||||
|
||||
public abstract float getSimilarity(String a, String b);
|
||||
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
|
||||
|
||||
public class CompositeSimilarityMetric implements SimilarityMetric, Iterable<SimilarityMetric> {
|
||||
|
||||
private List<SimilarityMetric> similarityMetrics;
|
||||
|
||||
|
||||
public CompositeSimilarityMetric(SimilarityMetric... metrics) {
|
||||
similarityMetrics = Arrays.asList(metrics);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float getSimilarity(ListEntry a, ListEntry b) {
|
||||
float similarity = 0;
|
||||
|
||||
for (SimilarityMetric metric : similarityMetrics) {
|
||||
similarity += metric.getSimilarity(a, b) / similarityMetrics.size();
|
||||
}
|
||||
|
||||
return similarity;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Average";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<SimilarityMetric> iterator() {
|
||||
return similarityMetrics.iterator();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.AbstractFileEntry;
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
|
||||
|
||||
public class LengthEqualsMetric implements SimilarityMetric {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(ListEntry a, ListEntry b) {
|
||||
if ((a instanceof AbstractFileEntry) && (b instanceof AbstractFileEntry)) {
|
||||
long lengthA = ((AbstractFileEntry) a).getLength();
|
||||
long lengthB = ((AbstractFileEntry) b).getLength();
|
||||
|
||||
if (lengthA == lengthB)
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Check whether file size is equal or not";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Length";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry;
|
||||
|
||||
|
||||
public interface SimilarityMetric {
|
||||
|
||||
public float getSimilarity(ListEntry a, ListEntry b);
|
||||
|
||||
|
||||
public String getDescription();
|
||||
|
||||
|
||||
public String getName();
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
|
||||
|
||||
import uk.ac.shef.wit.simmetrics.similaritymetrics.AbstractStringMetric;
|
||||
import uk.ac.shef.wit.simmetrics.similaritymetrics.MongeElkan;
|
||||
import uk.ac.shef.wit.simmetrics.tokenisers.TokeniserQGram3Extended;
|
||||
|
||||
|
||||
public class StringSimilarityMetric extends AbstractNameSimilarityMetric {
|
||||
|
||||
private final AbstractStringMetric metric;
|
||||
|
||||
|
||||
public StringSimilarityMetric() {
|
||||
// I have absolutely no clue as to why, but I get a good matching behavior
|
||||
// when using MongeElkan with a QGram3Extended (far from perfect though)
|
||||
metric = new MongeElkan(new TokeniserQGram3Extended());
|
||||
|
||||
//TODO QGram3Extended VS Whitespace (-> normalized values)
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float getSimilarity(String a, String b) {
|
||||
return metric.getSimilarity(a, b);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Similarity of names";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return metric.getShortDescriptionString();
|
||||
}
|
||||
|
||||
}
|
|
@ -12,12 +12,20 @@ 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;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public abstract class FileTransferablePolicy extends TransferablePolicy {
|
||||
|
||||
/**
|
||||
* Pattern that will match Windows (\r\n), Unix (\n) and Mac (\r) line separators.
|
||||
*/
|
||||
public static final Pattern LINE_SEPARATOR = Pattern.compile("\r?\n|\r\n?");
|
||||
|
||||
|
||||
@Override
|
||||
public boolean accept(Transferable tr) {
|
||||
List<File> files = getFilesFromTransferable(tr);
|
||||
|
@ -37,19 +45,22 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
|
|||
return (List<File>) tr.getTransferData(DataFlavor.javaFileListFlavor);
|
||||
} else if (tr.isDataFlavorSupported(FileTransferable.uriListFlavor)) {
|
||||
// file URI list flavor
|
||||
String transferString = (String) tr.getTransferData(FileTransferable.uriListFlavor);
|
||||
String transferData = (String) tr.getTransferData(FileTransferable.uriListFlavor);
|
||||
|
||||
String lines[] = transferString.split("\r?\n");
|
||||
ArrayList<File> files = new ArrayList<File>(lines.length);
|
||||
Scanner scanner = new Scanner(transferData).useDelimiter(LINE_SEPARATOR);
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.startsWith("#")) {
|
||||
// the line is a comment (as per the RFC 2483)
|
||||
ArrayList<File> files = new ArrayList<File>();
|
||||
|
||||
while (scanner.hasNext()) {
|
||||
String uri = scanner.next();
|
||||
|
||||
if (uri.startsWith("#")) {
|
||||
// the line is a comment (as per RFC 2483)
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
File file = new File(new URI(line));
|
||||
File file = new File(new URI(uri));
|
||||
|
||||
if (!file.exists())
|
||||
throw new FileNotFoundException(file.toString());
|
||||
|
@ -57,7 +68,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
|
|||
files.add(file);
|
||||
} catch (Exception e) {
|
||||
// URISyntaxException, IllegalArgumentException, FileNotFoundException
|
||||
Logger.getLogger("global").log(Level.WARNING, "Invalid file url: " + line);
|
||||
Logger.getLogger("global").log(Level.WARNING, "Invalid file uri: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +90,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
|
|||
public void handleTransferable(Transferable tr, TransferAction action) {
|
||||
List<File> files = getFilesFromTransferable(tr);
|
||||
|
||||
if (action != TransferAction.ADD) {
|
||||
if (action == TransferAction.PUT) {
|
||||
clear();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.transfer;
|
||||
|
||||
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public abstract class StringTransferablePolicy extends TransferablePolicy {
|
||||
|
||||
@Override
|
||||
public boolean accept(Transferable tr) {
|
||||
return tr.isDataFlavorSupported(DataFlavor.stringFlavor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleTransferable(Transferable tr, TransferAction action) {
|
||||
String string;
|
||||
|
||||
try {
|
||||
string = (String) tr.getTransferData(DataFlavor.stringFlavor);
|
||||
} catch (UnsupportedFlavorException e) {
|
||||
// should no happen
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
// should no happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (action != TransferAction.ADD)
|
||||
clear();
|
||||
|
||||
load(string);
|
||||
}
|
||||
|
||||
|
||||
protected void clear() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected abstract void load(String string);
|
||||
|
||||
}
|
|
@ -70,16 +70,17 @@ public class Episode implements Serializable {
|
|||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder(40);
|
||||
|
||||
sb.append(showName + " - ");
|
||||
sb.append(showName);
|
||||
sb.append(" - ");
|
||||
|
||||
if (seasonNumber != null)
|
||||
sb.append(seasonNumber + "x");
|
||||
|
||||
sb.append(episodeNumber);
|
||||
|
||||
sb.append(" - " + title);
|
||||
sb.append(" - ");
|
||||
sb.append(title);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,12 +2,8 @@
|
|||
package net.sourceforge.tuned.ui;
|
||||
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowListener;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Action;
|
||||
|
@ -26,33 +22,31 @@ public class ProgressDialog extends JDialog {
|
|||
private final JProgressBar progressBar = new JProgressBar(0, 100);
|
||||
private final JLabel iconLabel = new JLabel();
|
||||
private final JLabel headerLabel = new JLabel();
|
||||
private final JLabel noteLabel = new JLabel();
|
||||
|
||||
private final JButton cancelButton;
|
||||
|
||||
private boolean cancelled = false;
|
||||
private final Cancellable cancellable;
|
||||
|
||||
|
||||
public ProgressDialog(Window owner) {
|
||||
public ProgressDialog(Window owner, Cancellable cancellable) {
|
||||
super(owner, ModalityType.DOCUMENT_MODAL);
|
||||
|
||||
cancelButton = new JButton(cancelAction);
|
||||
this.cancellable = cancellable;
|
||||
|
||||
addWindowListener(closeListener);
|
||||
// disable window close button
|
||||
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
|
||||
|
||||
headerLabel.setFont(headerLabel.getFont().deriveFont(Font.BOLD));
|
||||
headerLabel.setFont(headerLabel.getFont().deriveFont(18f));
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setStringPainted(true);
|
||||
|
||||
JPanel c = (JPanel) getContentPane();
|
||||
|
||||
c.setLayout(new MigLayout("insets panel, fill"));
|
||||
c.setLayout(new MigLayout("insets dialog, nogrid, fill"));
|
||||
|
||||
c.add(iconLabel, "spany 2, grow 0 0, gap right 1mm");
|
||||
c.add(headerLabel, "align left, wmax 70%, grow 100 0, wrap");
|
||||
c.add(noteLabel, "align left, wmax 70%, grow 100 0, wrap");
|
||||
c.add(progressBar, "spanx 2, gap top unrel, gap bottom unrel, grow, wrap");
|
||||
c.add(iconLabel, "h pref!, w pref!");
|
||||
c.add(headerLabel, "gap 3mm, wrap paragraph");
|
||||
c.add(progressBar, "grow, wrap paragraph");
|
||||
|
||||
c.add(cancelButton, "spanx 2, align center");
|
||||
c.add(new JButton(cancelAction), "align center");
|
||||
|
||||
setSize(240, 155);
|
||||
|
||||
|
@ -60,22 +54,19 @@ public class ProgressDialog extends JDialog {
|
|||
}
|
||||
|
||||
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
|
||||
public void setIcon(Icon icon) {
|
||||
iconLabel.setIcon(icon);
|
||||
}
|
||||
|
||||
|
||||
public void setNote(String text) {
|
||||
noteLabel.setText(text);
|
||||
progressBar.setString(text);
|
||||
}
|
||||
|
||||
|
||||
public void setHeader(String text) {
|
||||
@Override
|
||||
public void setTitle(String text) {
|
||||
super.setTitle(text);
|
||||
headerLabel.setText(text);
|
||||
}
|
||||
|
||||
|
@ -85,32 +76,26 @@ public class ProgressDialog extends JDialog {
|
|||
}
|
||||
|
||||
|
||||
public JButton getCancelButton() {
|
||||
return cancelButton;
|
||||
}
|
||||
|
||||
|
||||
public void close() {
|
||||
setVisible(false);
|
||||
dispose();
|
||||
}
|
||||
|
||||
private final Action cancelAction = new AbstractAction("Cancel") {
|
||||
protected final Action cancelAction = new AbstractAction("Cancel") {
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
cancelled = true;
|
||||
close();
|
||||
cancellable.cancel();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private final WindowListener closeListener = new WindowAdapter() {
|
||||
|
||||
public static interface Cancellable {
|
||||
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
cancelAction.actionPerformed(null);
|
||||
}
|
||||
};
|
||||
boolean isCancelled();
|
||||
|
||||
|
||||
boolean cancel();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
|
||||
package net.sourceforge.tuned.ui;
|
||||
|
||||
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.SwingWorker;
|
||||
import javax.swing.Timer;
|
||||
|
||||
|
||||
public class SwingWorkerProgressMonitor {
|
||||
|
||||
public static final String PROPERTY_TITLE = "title";
|
||||
public static final String PROPERTY_NOTE = "note";
|
||||
public static final String PROPERTY_PROGRESS_STRING = "progress string";
|
||||
|
||||
private final SwingWorker<?, ?> worker;
|
||||
private final ProgressDialog progressDialog;
|
||||
|
||||
private int millisToPopup = 2000;
|
||||
|
||||
|
||||
public SwingWorkerProgressMonitor(Window owner, SwingWorker<?, ?> worker, Icon progressDialogIcon) {
|
||||
this.worker = worker;
|
||||
|
||||
progressDialog = new ProgressDialog(owner);
|
||||
progressDialog.setIcon(progressDialogIcon);
|
||||
|
||||
worker.addPropertyChangeListener(listener);
|
||||
|
||||
progressDialog.getCancelButton().addActionListener(cancelListener);
|
||||
}
|
||||
|
||||
|
||||
public ProgressDialog getProgressDialog() {
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
|
||||
public void setMillisToPopup(int millisToPopup) {
|
||||
this.millisToPopup = millisToPopup;
|
||||
}
|
||||
|
||||
|
||||
public int getMillisToPopup() {
|
||||
return millisToPopup;
|
||||
}
|
||||
|
||||
private final SwingWorkerPropertyChangeAdapter listener = new SwingWorkerPropertyChangeAdapter() {
|
||||
|
||||
private Timer popupTimer = null;
|
||||
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (evt.getPropertyName().equals(PROPERTY_PROGRESS_STRING))
|
||||
progressString(evt);
|
||||
else if (evt.getPropertyName().equals(PROPERTY_NOTE))
|
||||
note(evt);
|
||||
else if (evt.getPropertyName().equals(PROPERTY_TITLE))
|
||||
title(evt);
|
||||
else
|
||||
super.propertyChange(evt);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void started(PropertyChangeEvent evt) {
|
||||
popupTimer = TunedUtil.invokeLater(millisToPopup, new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!worker.isDone() && !progressDialog.isVisible()) {
|
||||
progressDialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void done(PropertyChangeEvent evt) {
|
||||
if (popupTimer != null) {
|
||||
popupTimer.stop();
|
||||
}
|
||||
|
||||
progressDialog.close();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void progress(PropertyChangeEvent evt) {
|
||||
progressDialog.getProgressBar().setValue((Integer) evt.getNewValue());
|
||||
}
|
||||
|
||||
|
||||
protected void progressString(PropertyChangeEvent evt) {
|
||||
progressDialog.getProgressBar().setString(evt.getNewValue().toString());
|
||||
}
|
||||
|
||||
|
||||
protected void note(PropertyChangeEvent evt) {
|
||||
progressDialog.setNote(evt.getNewValue().toString());
|
||||
}
|
||||
|
||||
|
||||
protected void title(PropertyChangeEvent evt) {
|
||||
String title = evt.getNewValue().toString();
|
||||
|
||||
progressDialog.setHeader(title);
|
||||
progressDialog.setTitle(title);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private final ActionListener cancelListener = new ActionListener() {
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
worker.cancel(false);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
package net.sourceforge.filebot;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.MatcherTestSuite;
|
||||
import net.sourceforge.filebot.similarity.SimilarityTestSuite;
|
||||
import net.sourceforge.filebot.web.WebTestSuite;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
|
@ -11,7 +11,7 @@ import org.junit.runners.Suite.SuiteClasses;
|
|||
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@SuiteClasses( { MatcherTestSuite.class, WebTestSuite.class, ArgumentBeanTest.class })
|
||||
@SuiteClasses( { SimilarityTestSuite.class, WebTestSuite.class, ArgumentBeanTest.class })
|
||||
public class FileBotTestSuite {
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
public class NameSimilarityMetricTest {
|
||||
|
||||
private static NameSimilarityMetric metric = new NameSimilarityMetric();
|
||||
|
||||
|
||||
@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"));
|
||||
|
||||
// remove checksum
|
||||
assertEquals(1, metric.getSimilarity("test", "test [EF62DF13]"));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -60,7 +60,7 @@ public class NumericSimilarityMetricTest {
|
|||
return TestUtil.asParameters(matches.keySet());
|
||||
}
|
||||
|
||||
private String normalizedName;
|
||||
private final String normalizedName;
|
||||
|
||||
|
||||
public NumericSimilarityMetricTest(String normalizedName) {
|
||||
|
@ -77,18 +77,20 @@ public class NumericSimilarityMetricTest {
|
|||
|
||||
|
||||
public String getBestMatch(String value, Collection<String> testdata) {
|
||||
float maxSimilarity = -1;
|
||||
double maxSimilarity = -1;
|
||||
String mostSimilar = null;
|
||||
|
||||
for (String comparisonValue : testdata) {
|
||||
float similarity = metric.getSimilarity(value, comparisonValue);
|
||||
for (String current : testdata) {
|
||||
double similarity = metric.getSimilarity(value, current);
|
||||
|
||||
if (similarity > maxSimilarity) {
|
||||
maxSimilarity = similarity;
|
||||
mostSimilar = comparisonValue;
|
||||
mostSimilar = current;
|
||||
}
|
||||
}
|
||||
|
||||
// System.out.println(String.format("[%f, %s, %s]", maxSimilarity, value, mostSimilar));
|
||||
|
||||
return mostSimilar;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
public class SeasonEpisodeSimilarityMetricTest {
|
||||
|
||||
private static SeasonEpisodeSimilarityMetric metric = new SeasonEpisodeSimilarityMetric();
|
||||
|
||||
|
||||
@Test
|
||||
public void getSimilarity() {
|
||||
// single pattern match, single episode match
|
||||
assertEquals(1.0, metric.getSimilarity("1x01", "s01e01"));
|
||||
|
||||
// multiple pattern matches, single episode match
|
||||
assertEquals(1.0, metric.getSimilarity("1x02a", "101 102 103"));
|
||||
|
||||
// multiple pattern matches, no episode match
|
||||
assertEquals(0.0, metric.getSimilarity("1x03b", "104 105 106"));
|
||||
|
||||
// no pattern match, no episode match
|
||||
assertEquals(0.0, metric.getSimilarity("abc", "xyz"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void fallbackMetric() {
|
||||
assertEquals(1.0, metric.getSimilarity("1x01", "sno=1, eno=1"));
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
import org.junit.runners.Suite.SuiteClasses;
|
||||
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@SuiteClasses( { NameSimilarityMetricTest.class, NumericSimilarityMetricTest.class, SeasonEpisodeSimilarityMetricTest.class })
|
||||
public class SimilarityTestSuite {
|
||||
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename;
|
||||
|
||||
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.AbstractNameSimilarityMetricTest;
|
||||
import net.sourceforge.filebot.ui.panel.rename.metric.NumericSimilarityMetricTest;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
import org.junit.runners.Suite.SuiteClasses;
|
||||
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@SuiteClasses( { AbstractNameSimilarityMetricTest.class, NumericSimilarityMetricTest.class })
|
||||
public class MatcherTestSuite {
|
||||
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
|
||||
package net.sourceforge.filebot.ui.panel.rename.metric;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import net.sourceforge.tuned.TestUtil;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class AbstractNameSimilarityMetricTest {
|
||||
|
||||
private static final BasicNameSimilarityMetric metric = new BasicNameSimilarityMetric();
|
||||
|
||||
|
||||
@Parameters
|
||||
public static Collection<Object[]> createParameters() {
|
||||
Map<String, String> matches = new LinkedHashMap<String, String>();
|
||||
|
||||
// normalize separators
|
||||
matches.put("test s01e01 first", "test.S01E01.First");
|
||||
matches.put("test s01e02 second", "test_S01E02_Second");
|
||||
matches.put("test s01e03 third", "__test__S01E03__Third__");
|
||||
matches.put("test s01e04 four", "test s01e04 four");
|
||||
|
||||
// strip checksum
|
||||
matches.put("test", "test [EF62DF13]");
|
||||
|
||||
// lower-case
|
||||
matches.put("the a-team", "The A-Team");
|
||||
|
||||
return TestUtil.asParameters(matches.entrySet());
|
||||
}
|
||||
|
||||
private Entry<String, String> entry;
|
||||
|
||||
|
||||
public AbstractNameSimilarityMetricTest(Entry<String, String> entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void normalize() {
|
||||
String normalizedName = entry.getKey();
|
||||
String unnormalizedName = entry.getValue();
|
||||
|
||||
assertEquals(normalizedName, metric.normalize(unnormalizedName));
|
||||
}
|
||||
|
||||
|
||||
private static class BasicNameSimilarityMetric extends AbstractNameSimilarityMetric {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(String a, String b) {
|
||||
return a.equals(b) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Equals";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Equals";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue