From c217d06eeb6aa00e53d024d2521b68d411662fef Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Sun, 11 Jan 2009 21:23:03 +0000 Subject: [PATCH] * 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 --- fw/action.match.file2name.png | Bin 25426 -> 0 bytes fw/action.match.name2file.png | Bin 24967 -> 0 bytes fw/action.match.png | Bin 0 -> 26839 bytes .../net/sourceforge/filebot/FileBotUtil.java | 5 + source/net/sourceforge/filebot/Main.java | 13 +- .../resources/action.match.file2name.png | Bin 1371 -> 0 bytes .../resources/action.match.name2file.png | Bin 1374 -> 0 bytes .../filebot/resources/action.match.png | Bin 0 -> 1573 bytes .../similarity/LengthEqualsMetric.java | 49 ++++ .../sourceforge/filebot/similarity/Match.java | 44 ++++ .../filebot/similarity/Matcher.java | 237 ++++++++++++++++++ .../similarity/NameSimilarityMetric.java | 56 +++++ .../NumericSimilarityMetric.java | 39 ++- .../SeasonEpisodeSimilarityMetric.java | 171 +++++++++++++ .../filebot/similarity/SimilarityMetric.java | 15 ++ .../sourceforge/filebot/torrent/Torrent.java | 8 +- .../filebot/ui/AbstractSearchPanel.java | 8 +- .../filebot/ui/panel/analyze/FileTree.java | 2 - .../filebot/ui/panel/analyze/SplitTool.java | 9 +- .../filebot/ui/panel/analyze/Tool.java | 14 +- .../filebot/ui/panel/analyze/TypeTool.java | 9 +- .../filebot/ui/panel/list/ListPanel.java | 2 +- .../ui/panel/rename/AbstractFileEntry.java | 32 +++ .../panel/rename/{entry => }/FileEntry.java | 23 +- .../rename/FilesListTransferablePolicy.java | 6 +- .../filebot/ui/panel/rename/MatchAction.java | 206 +++++++-------- .../rename/NamesListTransferablePolicy.java | 119 ++++----- .../filebot/ui/panel/rename/RenameAction.java | 83 +++--- .../filebot/ui/panel/rename/RenameList.java | 3 +- .../panel/rename/RenameListCellRenderer.java | 2 +- .../filebot/ui/panel/rename/RenamePanel.java | 149 +---------- .../ui/panel/rename/SimilarityPanel.java | 183 -------------- .../filebot/ui/panel/rename/StringEntry.java | 30 +++ .../ui/panel/rename/ValidateNamesDialog.java | 19 +- .../panel/rename/entry/AbstractFileEntry.java | 14 -- .../ui/panel/rename/entry/ListEntry.java | 29 --- .../ui/panel/rename/entry/StringEntry.java | 11 - .../ui/panel/rename/entry/TorrentEntry.java | 26 -- .../ui/panel/rename/matcher/Match.java | 29 --- .../ui/panel/rename/matcher/Matcher.java | 99 -------- .../metric/AbstractNameSimilarityMetric.java | 36 --- .../metric/CompositeSimilarityMetric.java | 51 ---- .../rename/metric/LengthEqualsMetric.java | 36 --- .../panel/rename/metric/SimilarityMetric.java | 17 -- .../rename/metric/StringSimilarityMetric.java | 41 --- .../ui/transfer/FileTransferablePolicy.java | 29 ++- .../ui/transfer/StringTransferablePolicy.java | 47 ---- .../net/sourceforge/filebot/web/Episode.java | 7 +- .../sourceforge/tuned/ui/ProgressDialog.java | 65 ++--- .../tuned/ui/SwingWorkerProgressMonitor.java | 129 ---------- .../sourceforge/filebot/FileBotTestSuite.java | 4 +- .../similarity/NameSimilarityMetricTest.java | 27 ++ .../NumericSimilarityMetricTest.java | 14 +- .../SeasonEpisodeSimilarityMetricTest.java | 93 +++++++ .../similarity/SimilarityTestSuite.java | 14 ++ .../ui/panel/rename/MatcherTestSuite.java | 17 -- .../AbstractNameSimilarityMetricTest.java | 83 ------ 57 files changed, 1140 insertions(+), 1314 deletions(-) delete mode 100644 fw/action.match.file2name.png delete mode 100644 fw/action.match.name2file.png create mode 100644 fw/action.match.png delete mode 100644 source/net/sourceforge/filebot/resources/action.match.file2name.png delete mode 100644 source/net/sourceforge/filebot/resources/action.match.name2file.png create mode 100644 source/net/sourceforge/filebot/resources/action.match.png create mode 100644 source/net/sourceforge/filebot/similarity/LengthEqualsMetric.java create mode 100644 source/net/sourceforge/filebot/similarity/Match.java create mode 100644 source/net/sourceforge/filebot/similarity/Matcher.java create mode 100644 source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java rename source/net/sourceforge/filebot/{ui/panel/rename/metric => similarity}/NumericSimilarityMetric.java (58%) create mode 100644 source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java create mode 100644 source/net/sourceforge/filebot/similarity/SimilarityMetric.java create mode 100644 source/net/sourceforge/filebot/ui/panel/rename/AbstractFileEntry.java rename source/net/sourceforge/filebot/ui/panel/rename/{entry => }/FileEntry.java (63%) delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/SimilarityPanel.java create mode 100644 source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/entry/AbstractFileEntry.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/entry/ListEntry.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/entry/StringEntry.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/entry/TorrentEntry.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/matcher/Match.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/matcher/Matcher.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetric.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/metric/CompositeSimilarityMetric.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/metric/LengthEqualsMetric.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/metric/SimilarityMetric.java delete mode 100644 source/net/sourceforge/filebot/ui/panel/rename/metric/StringSimilarityMetric.java delete mode 100644 source/net/sourceforge/filebot/ui/transfer/StringTransferablePolicy.java delete mode 100644 source/net/sourceforge/tuned/ui/SwingWorkerProgressMonitor.java create mode 100644 test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java rename test/net/sourceforge/filebot/{ui/panel/rename/metric => similarity}/NumericSimilarityMetricTest.java (88%) create mode 100644 test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java create mode 100644 test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java delete mode 100644 test/net/sourceforge/filebot/ui/panel/rename/MatcherTestSuite.java delete mode 100644 test/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetricTest.java diff --git a/fw/action.match.file2name.png b/fw/action.match.file2name.png deleted file mode 100644 index f5726518e681ff5024fd0a88abf408260318862a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25426 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 R@0(o=9j&LDRqA$8{{<3Ua3KHy diff --git a/fw/action.match.name2file.png b/fw/action.match.name2file.png deleted file mode 100644 index 9297b1b9dc5f0ff5a88cad4aa5b5bac5ebfa38bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24967 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 diff --git a/fw/action.match.png b/fw/action.match.png new file mode 100644 index 0000000000000000000000000000000000000000..faafd8a508fa15847bdb0ba6ca24e18af7f93b03 GIT binary patch literal 26839 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�s)`@$tub>Y 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 YAD4fRD&qX|-&|c)`R&VUMa%I21@)it=>Px# literal 0 HcmV?d00001 diff --git a/source/net/sourceforge/filebot/FileBotUtil.java b/source/net/sourceforge/filebot/FileBotUtil.java index f78cd352..94957192 100644 --- a/source/net/sourceforge/filebot/FileBotUtil.java +++ b/source/net/sourceforge/filebot/FileBotUtil.java @@ -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 TORRENT_FILE_EXTENSIONS = unmodifiableList("torrent"); public static final List SFV_FILE_EXTENSIONS = unmodifiableList("sfv"); public static final List LIST_FILE_EXTENSIONS = unmodifiableList("txt", "list", ""); diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index d14fd32c..2e6c365e 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -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); } diff --git a/source/net/sourceforge/filebot/resources/action.match.file2name.png b/source/net/sourceforge/filebot/resources/action.match.file2name.png deleted file mode 100644 index a49c912b691909796a0afc5eb9fcca8275e4f73a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1371 zcmV-h1*H0kP)WdJZWFEBSRFgH(DW!(S( z1h7d&K~zYIwU%pa6jc<*|L5MB-Pvy0K9+WO8)yL&9+D7BXpzt@2mygl5I>M0Xb3Se z81aP=Ni-@U1|uX;)PM$z;)4%-u-VEG|Q@66jUQKwl3;PN)5 zA+s{10l-Nue@^wJ>Gi(a(gpXH-oB7u7ea5iedxrp&Ts8zrPtUU9W8S4>Z8yN7m1`p zWy7wo-6<2%uFQuNfVSk35(}BOt%2^7xqm(fC{0Y2)3osuiyC~t7OuFru;(ZLnE<*M zbq)gDI@gNHRTEW9-j-w5i7%_QrE7lXd|xgl7$CtEj~-J(G(xGY^f6`BA#<_X7R$AE9p|mn2gI;y7&OLcU%gR)&^*Q*<`u|EIeB@v`y#vVj5Yf7A;3K;nd&4Zqwu(Gjw($$=;>E4}^-DKAO`b!> zWVyjqgMsf?g4t=147iY0ii*S>2-2ZLfQWFAtY; z526dl=}0n#1EC8bredhgIfZa+KJqvp01)FovhE~$v=RIakvF+IqX3Wrl6qTLYE`6G zqy!SSQx==oxe?kx15#29sw>Di1xR;ToXWbjl2iXMWGXpZr z+g&!nRDjDSaExP#U&I=#DgOQ3O938z1}xWgvW)A@Sychq+23U*lSIE!3B?5FZ5Umd*54bO~noZ~nKk8CdCzkLh*` zaDLPAJMN~knrLM_2+n($#{_Qe3i@49Xn#}fzct_!!K?~g;2Q9;&A`~002ovPDHLkV1l-DbdLZ4 diff --git a/source/net/sourceforge/filebot/resources/action.match.name2file.png b/source/net/sourceforge/filebot/resources/action.match.name2file.png deleted file mode 100644 index de0c1fa2551bb2a12a6350f8607dfb625e687661..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1374 zcmV-k1)=(hP)WdJZWFEBSRFgH(DW!(S( z1hYv*K~zYIwU%pa6jc<*|L4riZue12+l4NzRKaM(h~brPyEK4Gh>eN*!KgtZffOz1|=x$nTtpgV*=*mOkBhb9Y;a{ob2rHi5@nijOl$cYvpmyY^RgEVhJXRFahlFWNJweo zfQ~p68~^}AY=~&-GyqN90OD|pCdYezUfB`&`~8ss=m0SG$I^G_cq*5=$Ti4lkbx;Q z$s@!tz#t+B7`mhjL<9x{X`2oPk%rcuvlReF2H-vB+~z8CEj`9fa9M7+l^d>PKJvu{ zkL`iZc^Ji`M&j$bq)V4KW|qfbQ4hL|R3pLw43!!t6`+v@AABf_mcb7{eC&gPYyTXy zYlG(escLL&%Y7F;wGD`DtR4;!ukT(%CjBK<#@kMv82}P2NHQR{!0BWp#6}1qgtQP0 z1=wJeNeL#Jcj7iKhlBHm`2_&u``f1l=ao0QR~?yYZkm0}yRjL4wSmEqRrlP2=9&<5 zx#MB?BWjQ3B!?qC$8k%+KBBK@lRK`NE?e1Q@^b7e?aKK!{L>Iv{A92x9JAEElbto@ znyP~Z8ynGG8|()V5d;!5DK6$>X1QG-jNr0;p>VfOPRb01g4m-Mt4n@_xeQvd4^rJ&LsD(R~UAWs1%KyFDrH-VyJ zhih(;HQJ@6rj!A6Cu}+R>y>rk71Q6$P5}UTUeOTQlG^kheOki-(hBrv^qq+l&Q;d( z%WeAhQ#*Lv#6?gF&=ru(k+M?#Q0eQfMiMjJF&Io>0*DBage`}D>)94wKJ{f9xE)Vh ztf6&B3L-dpJdRgi)a{7Vws%Hk5Wv9fhXQ158M{(&v0%0*0*_80N;kMI^36H(Lw(@) znczUSFDwidb1IO5+*QbkI0eCss8Xo!^P<`v!Du6uaIYm~b^RIE|HH*_6{Q!SFaX5> z4XFla7N-G=lx+b}umw13dQjzxz!FGHL+dj9^7VUkwG+{>gr0lnF3sbgM{4-tD3=db z7Hn5R3D20dmc#~>0*MWYN2AnS|Ac*^<%{u=gEI08M6EN#_8D>H4mMPZn7G%h0I)Xu z%$}*4eb~)fb3c^VpaffEUFW6_Ag!%x^%(%q3!_~cc1UwG8aI5 z3jTk2y$%r(nekC@CL!vFvP07*qoM6N<$f{6lh;s5{u diff --git a/source/net/sourceforge/filebot/resources/action.match.png b/source/net/sourceforge/filebot/resources/action.match.png new file mode 100644 index 0000000000000000000000000000000000000000..f91890642bfbad9064d84a7eaeec620d2d62820c GIT binary patch literal 1573 zcmV+=2HN?FP)w(EZ*psMAUL&X(s%#> z1$s$DK~zYIt(9$Tl~*0WfB)z0-rIX`Un~u@a0`W&t}Wf5ms00MTwwvmn`|K(OoYXV z(-^W3>I}Gru+7BqWk@n$GREYC>0<205=I1P7P5sp!6gn$Sh5Y8@zz^td*7b>yqx2Q zI*q*yXyQrEm-B!7opb&#XswybX7`Nxr%&vhKKZ=uNO$dnht|%Vsy~A%tjc{2E1&54 z?)x)qnqHeMt4}r`dT4gfnaaI$R!+s|Jp(iyPH*4Tx~czrOIFP}Km2j+WVwT#>RqsC z)~2=%{Y^*G+y8F>vY{&4b@b7F>zjM`Zs}Pbi$^Wak9`(S*7HMxY4JE)dX~r5&+XmY zb@b7FvY{$^H~-ch>2}tqmTq2ia9LCH>Tk?jKq<_V&6lvnlF7O%2<1k07(3##t|Lt{ z7W+wJbM-u9XZ;V=3s;Ax3{Zby(L>9-zxhgUQ+sRs^y%CxWG0eEB~Z#N(^twQ?-lTa z9FAeIq_L5>HcIT6i>{{5= zuw!9k9Z5@b-OWrWYK;$s*o2xgfKa1LL4mA{AtKm7AVR{iS=KZwS!p@1cD}T(<)eKk zcj-abF9WE4Vg8JT+rR#Dd#Y;Pf|(VBs=%n1Lx~_Ld~H(HJ|Yo(KlCO8_1%ilh|Ewe;!4B)EOFN^_-M7ts=EAlA0L*@V@#6VSbB@fPk?5FLYcS^C zM9c|8`Qnqb5UPN{s6~Z-Srw=-3_azMQ)8Hz_#z+^&BoM1sGzyt06V_Ujy9f3?e5t+ z^z6Xjte#!Z%XLom58yi-5pBOhUpB+0>=hTc4 zQ595}4%!iH3p`wWT>=ACs0jlb#oV%FQg{^PIv1TQhy`MRV@Gg^LMW7cjjLRI!=;$1 zW{Lgnp~Wqad)6EddgYqYVf&ST&lFi`A&Xwl@y;y|V4Vz;A{v0DU?;Tq(Vq62ENz zRp5R0v$pPAh$$3Jp~{|kZLOHNS*Y<<)-Fpz<=Qw3BMJ%o-w=kNm4QZKdGN_V4I}4f z?Zc><3V23{yzj!Zgz+rmC4%QNi%2%elUWPY3^$UX$B@?C@l}+qPnT zwC}W}Ha=Xq5_vWF&gF*}F8B(rYhZ^yB1N>yl~sXOIf;oTVh9kWCTa4I^En27Kj$yk zjy6A`hx#tvF4mXM0zK4!DIwi!Pu;x#MR)VXAEdu(5eW@YpvyZkhG+^3;--md`V71q z@Lpf%Z{?u-l-B))FN<@>X_(U=6o75kmK7gou73FIudT^T+%^fru#CW0N{gc?1ukdb zvh_zN9(cO+yOVo3aHj}&0r+ACubtW>8&_S-eEz{p4{pde)YX)a`O0X3T~NIHhs2dj zr@Mcw|9Je(d+~iw006rG#GCS1`UcnZ8&&n*t;url^R|(*ujewC{u+H;zjge~z54wZ X2#cQu5`*&f00000NkvXXu0mjf2($k5 literal 0 HcmV?d00001 diff --git a/source/net/sourceforge/filebot/similarity/LengthEqualsMetric.java b/source/net/sourceforge/filebot/similarity/LengthEqualsMetric.java new file mode 100644 index 00000000..4474ebec --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/LengthEqualsMetric.java @@ -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(); + } + +} diff --git a/source/net/sourceforge/filebot/similarity/Match.java b/source/net/sourceforge/filebot/similarity/Match.java new file mode 100644 index 00000000..25f4ed37 --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/Match.java @@ -0,0 +1,44 @@ + +package net.sourceforge.filebot.similarity; + + +public class Match { + + 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 + * identity equality test. + * + * @param match a match + * @return Returns true 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); + } + +} diff --git a/source/net/sourceforge/filebot/similarity/Matcher.java b/source/net/sourceforge/filebot/similarity/Matcher.java new file mode 100644 index 00000000..e0a7df64 --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/Matcher.java @@ -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 { + + private final List values; + private final List candidates; + + private final List metrics; + + private final DisjointMatchCollection disjointMatchCollection; + + + public Matcher(Collection values, Collection candidates, Collection metrics) { + this.values = new LinkedList(values); + this.candidates = new LinkedList(candidates); + + this.metrics = new ArrayList(metrics); + + this.disjointMatchCollection = new DisjointMatchCollection(); + } + + + public synchronized List> match() throws InterruptedException { + + // list of all combinations of values and candidates + List> possibleMatches = new ArrayList>(values.size() * candidates.size()); + + // populate with all possible matches + for (V value : values) { + for (C candidate : candidates) { + possibleMatches.add(new Match(value, candidate)); + } + } + + // match recursively + match(possibleMatches, 0); + + // restore order according to the given values + List> result = new ArrayList>(); + + for (V value : values) { + Match match = disjointMatchCollection.getByValue(value); + + if (match != null) { + result.add(match); + } + } + + // remove matched objects + for (Match match : result) { + values.remove(match.getValue()); + candidates.remove(match.getCandidate()); + } + + // clear collected matches + disjointMatchCollection.clear(); + + return result; + } + + + public List remainingValues() { + return Collections.unmodifiableList(values); + } + + + public List remainingCandidates() { + return Collections.unmodifiableList(candidates); + } + + + protected void match(Collection> possibleMatches, int level) throws InterruptedException { + if (level >= metrics.size() || possibleMatches.isEmpty()) { + // no further refinement possible + disjointMatchCollection.addAll(possibleMatches); + return; + } + + for (List> matchesWithEqualSimilarity : mapBySimilarity(possibleMatches, metrics.get(level)).values()) { + // some matches may already be unique + List> 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> matches) { + for (Iterator> iterator = matches.iterator(); iterator.hasNext();) { + if (!disjointMatchCollection.disjoint(iterator.next())) + iterator.remove(); + } + } + + + protected SortedMap>> mapBySimilarity(Collection> possibleMatches, SimilarityMetric metric) throws InterruptedException { + // map sorted by similarity descending + SortedMap>> similarityMap = new TreeMap>>(Collections.reverseOrder()); + + // use metric on all matches + for (Match possibleMatch : possibleMatches) { + float similarity = metric.getSimilarity(possibleMatch.getValue(), possibleMatch.getCandidate()); + + List> list = similarityMap.get(similarity); + + if (list == null) { + list = new ArrayList>(); + 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> disjointMatches(Collection> collection) { + List> disjointMatches = new ArrayList>(); + + for (Match m1 : collection) { + boolean disjoint = true; + + for (Match 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 extends AbstractList> { + + private final List> matches; + + private final Map> values; + private final Map> candidates; + + + public DisjointMatchCollection() { + matches = new ArrayList>(); + values = new IdentityHashMap>(); + candidates = new IdentityHashMap>(); + } + + + @Override + public boolean add(Match match) { + if (disjoint(match)) { + values.put(match.getValue(), match); + candidates.put(match.getCandidate(), match); + + return matches.add(match); + } + + return false; + } + + + public boolean disjoint(Match match) { + return !values.containsKey(match.getValue()) && !candidates.containsKey(match.getCandidate()); + } + + + public Match getByValue(V value) { + return values.get(value); + } + + + public Match getByCandidate(C candidate) { + return candidates.get(candidate); + } + + + @Override + public Match get(int index) { + return matches.get(index); + } + + + @Override + public int size() { + return matches.size(); + } + + + @Override + public void clear() { + matches.clear(); + values.clear(); + candidates.clear(); + } + + } + +} diff --git a/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java b/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java new file mode 100644 index 00000000..093692f8 --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/NameSimilarityMetric.java @@ -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(); + } +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/metric/NumericSimilarityMetric.java b/source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java similarity index 58% rename from source/net/sourceforge/filebot/ui/panel/rename/metric/NumericSimilarityMetric.java rename to source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java index ebb4e90b..93b92379 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/metric/NumericSimilarityMetric.java +++ b/source/net/sourceforge/filebot/similarity/NumericSimilarityMetric.java @@ -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 tokens = new ArrayList(); 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; diff --git a/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java b/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java new file mode 100644 index 00000000..b217da0e --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetric.java @@ -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("(? sxeVector1 = match(normalize(o1)); + List 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 match(String name) { + for (SeasonEpisodePattern pattern : patterns) { + List match = pattern.match(name); + + if (!match.isEmpty()) { + // current pattern did match + return match; + } + } + + return null; + } + + + protected String normalize(Object object) { + return object.toString(); + } + + + @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 match(String name) { + // name will probably contain no more than one match, but may contain more + List matches = new ArrayList(1); + + Matcher matcher = pattern.matcher(name); + + while (matcher.find()) { + matches.add(new SxE(matcher.group(seasonGroup), matcher.group(episodeGroup))); + } + + return matches; + } + } + +} diff --git a/source/net/sourceforge/filebot/similarity/SimilarityMetric.java b/source/net/sourceforge/filebot/similarity/SimilarityMetric.java new file mode 100644 index 00000000..b95f3c5a --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/SimilarityMetric.java @@ -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(); + +} diff --git a/source/net/sourceforge/filebot/torrent/Torrent.java b/source/net/sourceforge/filebot/torrent/Torrent.java index 47f966be..5e5a597d 100644 --- a/source/net/sourceforge/filebot/torrent/Torrent.java +++ b/source/net/sourceforge/filebot/torrent/Torrent.java @@ -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; } diff --git a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java index 8a64f4bf..2d5470ce 100644 --- a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java +++ b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java @@ -184,8 +184,7 @@ public abstract class AbstractSearchPanel 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 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 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(); diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/FileTree.java b/source/net/sourceforge/filebot/ui/panel/analyze/FileTree.java index c6c5f36b..898abf7e 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/FileTree.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/FileTree.java @@ -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); } } } diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/SplitTool.java b/source/net/sourceforge/filebot/ui/panel/analyze/SplitTool.java index ca114353..6f1241ca 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/SplitTool.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/SplitTool.java @@ -75,7 +75,7 @@ public class SplitTool extends Tool 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 implements ChangeListener { List remainder = new ArrayList(50); long totalSize = 0; - for (Iterator iterator = sourceModel.fileIterator(); iterator.hasNext() && !cancellable.isCancelled();) { + for (Iterator iterator = sourceModel.fileIterator(); iterator.hasNext();) { File file = iterator.next(); long fileSize = file.length(); @@ -108,6 +108,11 @@ public class SplitTool extends Tool implements ChangeListener { totalSize += fileSize; currentPart.add(file); + + // unwind thread, if we have been cancelled + if (Thread.interrupted()) { + throw new InterruptedException(); + } } if (!currentPart.isEmpty()) { diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java b/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java index 434cd20c..4b2861c7 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/Tool.java @@ -32,7 +32,7 @@ abstract class Tool 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 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 implements Cancellable { + private class UpdateModelTask extends SwingWorker { private final FolderNode sourceModel; @@ -67,7 +67,7 @@ abstract class Tool 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 extends JComponent { } } - - protected static interface Cancellable { - - boolean isCancelled(); - } - protected FolderNode createStatisticsNode(String name, List files) { FolderNode folder = new FolderNode(null, files.size()); diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java b/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java index f4b38133..bcbf6eb1 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/TypeTool.java @@ -41,10 +41,10 @@ public class TypeTool extends Tool { @Override - protected TreeModel createModelInBackground(FolderNode sourceModel, Cancellable cancellable) { + protected TreeModel createModelInBackground(FolderNode sourceModel) throws InterruptedException { TreeMap> map = new TreeMap>(); - for (Iterator iterator = sourceModel.fileIterator(); iterator.hasNext() && !cancellable.isCancelled();) { + for (Iterator iterator = sourceModel.fileIterator(); iterator.hasNext();) { File file = iterator.next(); String extension = FileUtil.getExtension(file); @@ -62,6 +62,11 @@ public class TypeTool extends Tool { for (Entry> 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); diff --git a/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java b/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java index fc9e9656..2ee7b3e3 100644 --- a/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/list/ListPanel.java @@ -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; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/AbstractFileEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/AbstractFileEntry.java new file mode 100644 index 00000000..80668aa0 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/AbstractFileEntry.java @@ -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(); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/entry/FileEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java similarity index 63% rename from source/net/sourceforge/filebot/ui/panel/rename/entry/FileEntry.java rename to source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java index f4d8c9ef..89c5c74e 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/entry/FileEntry.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FileEntry.java @@ -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; + } + } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java index caf1d594..389e2101 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java @@ -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 model; + private final List model; - public FilesListTransferablePolicy(EventList model) { + public FilesListTransferablePolicy(List model) { this.model = model; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java index c6399d78..692fedcb 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java @@ -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 namesModel; + private final List filesModel; - private final RenameList namesList; - private final RenameList 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 namesList, RenameList filesList) { - super("Match"); + public MatchAction(List namesModel, List 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 primaryList = (RenameList) (matchName2File ? namesList : filesList); - RenameList secondaryList = (RenameList) (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, Void> { + protected static class BackgroundMatcher extends SwingWorker>, Void> implements Cancellable { - private final RenameList primaryList; - private final RenameList secondaryList; + private final List namesModel; + private final List filesModel; - private final Matcher matcher; + private final Matcher matcher; - public BackgroundMatcher(RenameList primaryList, RenameList secondaryList, SimilarityMetric similarityMetric) { - this.primaryList = primaryList; - this.secondaryList = secondaryList; + public BackgroundMatcher(List namesModel, List filesModel, List metrics) { + this.namesModel = namesModel; + this.filesModel = filesModel; - matcher = new Matcher(primaryList.getEntries(), secondaryList.getEntries(), similarityMetric); + this.matcher = new Matcher(namesModel, filesModel, metrics); } @Override - protected List doInBackground() throws Exception { - - firePropertyChange(SwingWorkerProgressMonitor.PROPERTY_TITLE, null, "Matching..."); - - int total = matcher.remainingMatches(); - - List matches = new ArrayList(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> doInBackground() throws Exception { + return matcher.match(); } @@ -162,23 +144,29 @@ class MatchAction extends AbstractAction { return; try { - List matches = get(); + List> 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 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); + } + } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java index 7eb7da14..84b3bed3 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java @@ -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 list; - - private final TextPolicy textPolicy = new TextPolicy(); + private final RenameList list; - public NamesListTransferablePolicy(RenameList list) { + public NamesListTransferablePolicy(RenameList 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 entries) { - List invalidEntries = new ArrayList(); + protected void submit(List entries) { + List invalidEntries = new ArrayList(); - 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 entries = new ArrayList(); + + 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 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 files) { try { - List entries = new ArrayList(); + List entries = new ArrayList(); 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 files) { try { - List entries = new ArrayList(); + List entries = new ArrayList(); 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 entries = new ArrayList(); - - String[] lines = string.split("\r?\n"); - - for (String line : lines) { - - if (!line.isEmpty()) - entries.add(new StringEntry(line)); - } - - if (!entries.isEmpty()) { - submit(entries); - } - } - - } - } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java index 4fa85a7e..5570480b 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java @@ -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 namesList; - private final RenameList filesList; + private final List namesModel; + private final List filesModel; - public RenameAction(RenameList namesList, RenameList filesList) { + public RenameAction(List namesModel, List 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 nameEntries = namesList.getEntries(); - List fileEntries = filesList.getEntries(); + public void actionPerformed(ActionEvent evt) { - int minLength = Math.min(nameEntries.size(), fileEntries.size()); + Deque> renameMatches = new ArrayDeque>(); + Deque> revertMatches = new ArrayDeque>(); - int i = 0; - int errors = 0; + Iterator names = namesModel.iterator(); + Iterator 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(source, target)); + } + + try { + int renameCount = renameMatches.size(); - if (f.renameTo(newFile)) { - filesList.getModel().remove(fileEntry); - } else { - errors++; + for (Match 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 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(); } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java index fe46bf1f..3204c571 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java @@ -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 extends FileBotList { +class RenameList extends FileBotList { private JButton loadButton = new JButton(); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java index 53209348..692ecbaa 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java @@ -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; diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java index 410a7c0d..53379d2c 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java @@ -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 namesList = new RenameList(); + private RenameList namesList = new RenameList(); private RenameList filesList = new RenameList(); - 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()); + filesList.getModel().addListEventListener(new RepaintHandler()); } - private final ListDataListener repaintOnDataChange = new ListDataListener() { + + private class RepaintHandler implements ListEventListener { - 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 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()); - } - } - } - } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/SimilarityPanel.java b/source/net/sourceforge/filebot/ui/panel/rename/SimilarityPanel.java deleted file mode 100644 index ebcd5b55..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/SimilarityPanel.java +++ /dev/null @@ -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 updaterList = new ArrayList(); - - 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)); - } - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java new file mode 100644 index 00000000..2160e434 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/StringEntry.java @@ -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(); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java index df8d4e41..4eaf30ce 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java @@ -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 entries; + private final Collection 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 entries) { + public ValidateNamesDialog(Window owner, Collection 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; diff --git a/source/net/sourceforge/filebot/ui/panel/rename/entry/AbstractFileEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/entry/AbstractFileEntry.java deleted file mode 100644 index 0ae8bb5c..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/entry/AbstractFileEntry.java +++ /dev/null @@ -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(); - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/entry/ListEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/entry/ListEntry.java deleted file mode 100644 index 532c86a5..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/entry/ListEntry.java +++ /dev/null @@ -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(); - } -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/entry/StringEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/entry/StringEntry.java deleted file mode 100644 index 5ed4418d..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/entry/StringEntry.java +++ /dev/null @@ -1,11 +0,0 @@ - -package net.sourceforge.filebot.ui.panel.rename.entry; - - -public class StringEntry extends ListEntry { - - public StringEntry(String string) { - super(string); - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/entry/TorrentEntry.java b/source/net/sourceforge/filebot/ui/panel/rename/entry/TorrentEntry.java deleted file mode 100644 index 494a3d9c..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/entry/TorrentEntry.java +++ /dev/null @@ -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(); - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/matcher/Match.java b/source/net/sourceforge/filebot/ui/panel/rename/matcher/Match.java deleted file mode 100644 index 0e4bbe44..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/matcher/Match.java +++ /dev/null @@ -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; - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/matcher/Matcher.java b/source/net/sourceforge/filebot/ui/panel/rename/matcher/Matcher.java deleted file mode 100644 index 8e3be765..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/matcher/Matcher.java +++ /dev/null @@ -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 { - - private final LinkedList primaryList; - private final LinkedList secondaryList; - private final SimilarityMetric similarityMetric; - - - public Matcher(List primaryList, List secondaryList, SimilarityMetric similarityMetric) { - this.primaryList = new LinkedList(primaryList); - this.secondaryList = new LinkedList(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 getPrimaryList() { - return Collections.unmodifiableList(primaryList); - } - - - public List getSecondaryList() { - return Collections.unmodifiableList(secondaryList); - } - - - /** - * The remove operation is not supported by this implementation of Iterator. - * - * @throws UnsupportedOperationException if this method is invoked. - * @see java.util.Iterator - */ - @Override - public void remove() { - throw new UnsupportedOperationException(); - } -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetric.java b/source/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetric.java deleted file mode 100644 index 6c565171..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetric.java +++ /dev/null @@ -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); - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/metric/CompositeSimilarityMetric.java b/source/net/sourceforge/filebot/ui/panel/rename/metric/CompositeSimilarityMetric.java deleted file mode 100644 index fe5a61a9..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/metric/CompositeSimilarityMetric.java +++ /dev/null @@ -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 { - - private List 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 iterator() { - return similarityMetrics.iterator(); - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/metric/LengthEqualsMetric.java b/source/net/sourceforge/filebot/ui/panel/rename/metric/LengthEqualsMetric.java deleted file mode 100644 index a3f24a6c..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/metric/LengthEqualsMetric.java +++ /dev/null @@ -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"; - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/metric/SimilarityMetric.java b/source/net/sourceforge/filebot/ui/panel/rename/metric/SimilarityMetric.java deleted file mode 100644 index 45b07bf8..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/metric/SimilarityMetric.java +++ /dev/null @@ -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(); -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/metric/StringSimilarityMetric.java b/source/net/sourceforge/filebot/ui/panel/rename/metric/StringSimilarityMetric.java deleted file mode 100644 index 896022d4..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/metric/StringSimilarityMetric.java +++ /dev/null @@ -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(); - } - -} diff --git a/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java b/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java index cc617a7a..79a77cbb 100644 --- a/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/transfer/FileTransferablePolicy.java @@ -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 files = getFilesFromTransferable(tr); @@ -37,19 +45,22 @@ public abstract class FileTransferablePolicy extends TransferablePolicy { return (List) 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 files = new ArrayList(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 files = new ArrayList(); + + 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 files = getFilesFromTransferable(tr); - if (action != TransferAction.ADD) { + if (action == TransferAction.PUT) { clear(); } diff --git a/source/net/sourceforge/filebot/ui/transfer/StringTransferablePolicy.java b/source/net/sourceforge/filebot/ui/transfer/StringTransferablePolicy.java deleted file mode 100644 index 5b600b3e..00000000 --- a/source/net/sourceforge/filebot/ui/transfer/StringTransferablePolicy.java +++ /dev/null @@ -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); - -} diff --git a/source/net/sourceforge/filebot/web/Episode.java b/source/net/sourceforge/filebot/web/Episode.java index 9da5bc52..edb0679a 100644 --- a/source/net/sourceforge/filebot/web/Episode.java +++ b/source/net/sourceforge/filebot/web/Episode.java @@ -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(); } - } diff --git a/source/net/sourceforge/tuned/ui/ProgressDialog.java b/source/net/sourceforge/tuned/ui/ProgressDialog.java index b78b6b72..afa584ed 100644 --- a/source/net/sourceforge/tuned/ui/ProgressDialog.java +++ b/source/net/sourceforge/tuned/ui/ProgressDialog.java @@ -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(); + } } diff --git a/source/net/sourceforge/tuned/ui/SwingWorkerProgressMonitor.java b/source/net/sourceforge/tuned/ui/SwingWorkerProgressMonitor.java deleted file mode 100644 index ab0c4e68..00000000 --- a/source/net/sourceforge/tuned/ui/SwingWorkerProgressMonitor.java +++ /dev/null @@ -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); - } - - }; - -} diff --git a/test/net/sourceforge/filebot/FileBotTestSuite.java b/test/net/sourceforge/filebot/FileBotTestSuite.java index ec025e18..a1f612ed 100644 --- a/test/net/sourceforge/filebot/FileBotTestSuite.java +++ b/test/net/sourceforge/filebot/FileBotTestSuite.java @@ -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 { } diff --git a/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java b/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java new file mode 100644 index 00000000..70beb974 --- /dev/null +++ b/test/net/sourceforge/filebot/similarity/NameSimilarityMetricTest.java @@ -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]")); + } + +} diff --git a/test/net/sourceforge/filebot/ui/panel/rename/metric/NumericSimilarityMetricTest.java b/test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java similarity index 88% rename from test/net/sourceforge/filebot/ui/panel/rename/metric/NumericSimilarityMetricTest.java rename to test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java index c9251421..38824e66 100644 --- a/test/net/sourceforge/filebot/ui/panel/rename/metric/NumericSimilarityMetricTest.java +++ b/test/net/sourceforge/filebot/similarity/NumericSimilarityMetricTest.java @@ -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 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; } } diff --git a/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java b/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java new file mode 100644 index 00000000..e1fa5f98 --- /dev/null +++ b/test/net/sourceforge/filebot/similarity/SeasonEpisodeSimilarityMetricTest.java @@ -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")); + } + +} diff --git a/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java b/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java new file mode 100644 index 00000000..ac7975ef --- /dev/null +++ b/test/net/sourceforge/filebot/similarity/SimilarityTestSuite.java @@ -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 { + +} diff --git a/test/net/sourceforge/filebot/ui/panel/rename/MatcherTestSuite.java b/test/net/sourceforge/filebot/ui/panel/rename/MatcherTestSuite.java deleted file mode 100644 index 2a8b3a7f..00000000 --- a/test/net/sourceforge/filebot/ui/panel/rename/MatcherTestSuite.java +++ /dev/null @@ -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 { - -} diff --git a/test/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetricTest.java b/test/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetricTest.java deleted file mode 100644 index 33670f8c..00000000 --- a/test/net/sourceforge/filebot/ui/panel/rename/metric/AbstractNameSimilarityMetricTest.java +++ /dev/null @@ -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 createParameters() { - Map matches = new LinkedHashMap(); - - // 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 entry; - - - public AbstractNameSimilarityMetricTest(Entry 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"; - } - - } - -}