* 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:
Reinhard Pointner 2009-01-11 21:23:03 +00:00
parent 54b27e69b7
commit c217d06eeb
57 changed files with 1140 additions and 1314 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

BIN
fw/action.match.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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", "");

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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()) {

View File

@ -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());

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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());
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -1,11 +0,0 @@
package net.sourceforge.filebot.ui.panel.rename.entry;
public class StringEntry extends ListEntry {
public StringEntry(String string) {
super(string);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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";
}
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
};
}

View File

@ -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 {
}

View File

@ -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]"));
}
}

View File

@ -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;
}
}

View File

@ -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"));
}
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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";
}
}
}