* improved matching of episodes with episode number > 99

* refactoring
This commit is contained in:
Reinhard Pointner 2009-07-23 14:25:43 +00:00
parent 64f1cd7040
commit a500aacf80
10 changed files with 113 additions and 73 deletions

View File

@ -4,7 +4,10 @@ package net.sourceforge.filebot.similarity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -24,7 +27,19 @@ public class SeasonEpisodeMatcher {
patterns[1] = new SeasonEpisodePattern("(?<!\\p{Alnum})(\\d{1,2})[x\\.](\\d{1,3})(?!\\p{Digit})");
// match patterns like 01, 102, 1003 (enclosed in separators)
patterns[2] = new SeasonEpisodePattern("(?<=^|[\\._ ])([0-1]?\\d?)(\\d{2})(?=[\\._ ]|$)");
patterns[2] = new SeasonEpisodePattern("(?<=^|[\\._ ])([0-1]?\\d?)(\\d{2})(?=[\\._ ]|$)") {
@Override
protected Collection<SxE> process(MatchResult match) {
// interpret match as season and episode
SxE seasonEpisode = new SxE(match.group(1), match.group(2));
// interpret match as episode number only
SxE episodeOnly = new SxE(null, match.group(1) + match.group(2));
return Arrays.asList(seasonEpisode, episodeOnly);
}
};
}
@ -49,9 +64,9 @@ public class SeasonEpisodeMatcher {
}
public int find(CharSequence name) {
public int find(CharSequence name, int fromIndex) {
for (SeasonEpisodePattern pattern : patterns) {
int index = pattern.find(name);
int index = pattern.find(name, fromIndex);
if (index >= 0) {
// current pattern did match
@ -120,40 +135,35 @@ public class SeasonEpisodeMatcher {
protected final Pattern pattern;
protected final int seasonGroup;
protected final int episodeGroup;
public SeasonEpisodePattern(String pattern) {
this(Pattern.compile(pattern), 1, 2);
this.pattern = Pattern.compile(pattern);
}
public SeasonEpisodePattern(Pattern pattern, int seasonGroup, int episodeGroup) {
this.pattern = pattern;
this.seasonGroup = seasonGroup;
this.episodeGroup = episodeGroup;
protected Collection<SxE> process(MatchResult match) {
return Collections.singleton(new SxE(match.group(1), match.group(2)));
}
public List<SxE> match(CharSequence name) {
// name will probably contain no more than one match, but may contain more
List<SxE> matches = new ArrayList<SxE>(1);
// name will probably contain no more than two matches
List<SxE> matches = new ArrayList<SxE>(2);
Matcher matcher = pattern.matcher(name);
while (matcher.find()) {
matches.add(new SxE(matcher.group(seasonGroup), matcher.group(episodeGroup)));
matches.addAll(process(matcher));
}
return matches;
}
public int find(CharSequence name) {
public int find(CharSequence name, int fromIndex) {
Matcher matcher = pattern.matcher(name);
if (matcher.find())
if (matcher.find(fromIndex))
return matcher.start();
return -1;

View File

@ -152,7 +152,7 @@ public class SeriesNameMatcher {
* episode pattern, or null if there is no such pattern
*/
public String matchBySeasonEpisodePattern(String name) {
int seasonEpisodePosition = seasonEpisodeMatcher.find(name);
int seasonEpisodePosition = seasonEpisodeMatcher.find(name, 0);
if (seasonEpisodePosition > 0) {
// series name ends at the first season episode pattern

View File

@ -11,9 +11,11 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -39,10 +41,10 @@ import net.sourceforge.tuned.FileUtilities;
class AutoFetchEpisodeListMatcher extends SwingWorker<List<Match<File, Episode>>, Void> {
private final List<File> files;
private final EpisodeListProvider provider;
private final List<File> files;
private final List<SimilarityMetric> metrics;
@ -124,20 +126,20 @@ class AutoFetchEpisodeListMatcher extends SwingWorker<List<Match<File, Episode>>
}
protected List<Episode> fetchEpisodeList(Collection<String> seriesNames) throws Exception {
List<Callable<Collection<Episode>>> tasks = new ArrayList<Callable<Collection<Episode>>>();
protected Set<Episode> fetchEpisodeSet(Collection<String> seriesNames) throws Exception {
List<Callable<List<Episode>>> tasks = new ArrayList<Callable<List<Episode>>>();
// detect series names and create episode list fetch tasks
for (final String seriesName : seriesNames) {
tasks.add(new Callable<Collection<Episode>>() {
for (final String query : seriesNames) {
tasks.add(new Callable<List<Episode>>() {
@Override
public Collection<Episode> call() throws Exception {
List<SearchResult> results = provider.search(seriesName);
public List<Episode> call() throws Exception {
List<SearchResult> results = provider.search(query);
// select search result
if (results.size() > 0) {
SearchResult selectedSearchResult = selectSearchResult(seriesName, results);
SearchResult selectedSearchResult = selectSearchResult(query, results);
if (selectedSearchResult != null) {
return provider.getEpisodeList(selectedSearchResult);
@ -150,17 +152,22 @@ class AutoFetchEpisodeListMatcher extends SwingWorker<List<Match<File, Episode>>
}
// fetch episode lists concurrently
List<Episode> episodes = new ArrayList<Episode>();
ExecutorService executor = Executors.newCachedThreadPool();
for (Future<Collection<Episode>> future : executor.invokeAll(tasks)) {
episodes.addAll(future.get());
try {
// merge all episodes
Set<Episode> episodes = new LinkedHashSet<Episode>();
for (Future<List<Episode>> future : executor.invokeAll(tasks)) {
episodes.addAll(future.get());
}
// all background workers have finished
return episodes;
} finally {
// destroy background threads
executor.shutdown();
}
// destroy background threads
executor.shutdown();
return episodes;
}
@ -171,7 +178,7 @@ class AutoFetchEpisodeListMatcher extends SwingWorker<List<Match<File, Episode>>
List<File> mediaFiles = FileUtilities.filter(files, VIDEO_FILES, SUBTITLE_FILES);
// detect series name and fetch episode list
List<Episode> episodes = fetchEpisodeList(detectSeriesNames(mediaFiles));
Set<Episode> episodes = fetchEpisodeSet(detectSeriesNames(mediaFiles));
List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
@ -221,4 +228,5 @@ class AutoFetchEpisodeListMatcher extends SwingWorker<List<Match<File, Episode>>
return map;
}
}

View File

@ -49,8 +49,9 @@ class EpisodeExpressionFormatter implements MatchFormatter {
String result = format.format(new EpisodeFormatBindingBean(episode, mediaFile)).trim();
// if result is empty, check for script exceptions
if (result.isEmpty() && format.caughtScriptException() != null)
if (result.isEmpty() && format.caughtScriptException() != null) {
throw format.caughtScriptException();
}
return result;
}

View File

@ -12,7 +12,7 @@ class FileNameFormatter implements MatchFormatter {
private final boolean preserveExtension;
public FileNameFormatter(boolean preserveExtension) {
this.preserveExtension = preserveExtension;
}
@ -34,18 +34,16 @@ class FileNameFormatter implements MatchFormatter {
public String format(Match<?, ?> match) {
if (match.getValue() instanceof File) {
File file = (File) match.getValue();
return preserveExtension ? FileUtilities.getName(file) : file.getName();
}
if (match.getValue() instanceof AbstractFile) {
AbstractFile file = (AbstractFile) match.getValue();
return preserveExtension ? FileUtilities.getNameWithoutExtension(file.getName()) : file.getName();
}
// type not supported
throw new IllegalArgumentException("Type not supported");
// cannot format value
throw new IllegalArgumentException("Illegal value: " + match.getValue());
}
}

View File

@ -133,6 +133,9 @@ class MatchAction extends AbstractAction {
public void actionPerformed(ActionEvent evt) {
if (model.names().isEmpty() || model.files().isEmpty())
return;
JComponent eventSource = (JComponent) evt.getSource();
SwingUtilities.getRoot(eventSource).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

View File

@ -69,7 +69,7 @@ class RenameAction extends AbstractAction {
iterator.remove();
} else {
// failed to revert rename operation
Logger.getLogger("ui").severe(String.format("Failed to revert file: \"%s\".", mapping.getValue()));
Logger.getLogger("ui").severe("Failed to revert file: " + mapping.getValue());
}
}
}
@ -90,16 +90,16 @@ class RenameAction extends AbstractAction {
}
private File rename(File file, String name) throws IOException {
private File rename(File file, String path) throws IOException {
// same folder, different name
File destination = new File(file.getParentFile(), name);
File destination = new File(file.getParentFile(), path);
// name may be a relative path, so we can't use file.getParentFile()
File destinationFolder = destination.getParentFile();
// create parent folder if necessary
if (!destinationFolder.isDirectory()) {
if (!destinationFolder.mkdirs()) {
throw new IOException("Failed to create folder: " + destinationFolder);
}
if (!destinationFolder.isDirectory() && !destinationFolder.mkdirs()) {
throw new IOException("Failed to create folder: " + destinationFolder);
}
if (!file.renameTo(destination)) {

View File

@ -3,6 +3,7 @@ package net.sourceforge.filebot.web;
import java.io.Serializable;
import java.util.Arrays;
public class Episode implements Serializable {
@ -31,29 +32,11 @@ public class Episode implements Serializable {
}
public Integer getEpisodeNumber() {
try {
return Integer.valueOf(episode);
} catch (NumberFormatException e) {
return null;
}
}
public String getSeason() {
return season;
}
public Integer getSeasonNumber() {
try {
return Integer.valueOf(season);
} catch (NumberFormatException e) {
return null;
}
}
public String getSeriesName() {
return seriesName;
}
@ -64,6 +47,31 @@ public class Episode implements Serializable {
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Episode) {
Episode other = (Episode) obj;
return equals(season, other.season) && equals(episode, other.episode) && equals(seriesName, other.seriesName) && equals(title, other.title);
}
return false;
}
private boolean equals(Object o1, Object o2) {
if (o1 == null || o2 == null)
return o1 == o2;
return o1.equals(o2);
}
@Override
public int hashCode() {
return Arrays.hashCode(new Object[] { seriesName, season, episode, title });
}
@Override
public String toString() {
return EpisodeFormat.getInstance().format(this);

View File

@ -22,8 +22,15 @@ public class EpisodeFormat extends Format {
public StringBuffer format(Object obj, StringBuffer sb, FieldPosition pos) {
Episode episode = (Episode) obj;
// try to format episode number, or use episode "number" string as is
String episodeNumber = (episode.getEpisodeNumber() != null ? String.format("%02d", episode.getEpisodeNumber()) : episode.getEpisode());
// episode number is most likely a number but could also be some kind of special identifier (e.g. Special)
String episodeNumber = episode.getEpisode();
// try to format episode number, if possible
try {
episodeNumber = String.format("%02d", Integer.parseInt(episodeNumber));
} catch (NumberFormatException e) {
// ignore
}
// series name should not be empty or null
sb.append(episode.getSeriesName());

View File

@ -2,18 +2,20 @@
package net.sourceforge.filebot.similarity;
import static java.util.Arrays.*;
import static net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE.*;
import static org.junit.Assert.*;
import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE;
import org.junit.Test;
import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE;
public class SeasonEpisodeMatcherTest {
private static SeasonEpisodeMatcher matcher = new SeasonEpisodeMatcher();
@Test
public void patternPrecedence() {
// S01E01 pattern has highest precedence
@ -34,7 +36,7 @@ public class SeasonEpisodeMatcherTest {
// test high values
assertEquals(new SxE(12, 345), matcher.match("Test - 12x345 - High Values").get(0));
// test lookahead and lookbehind
// test look-ahead and look-behind
assertEquals(new SxE(1, 3), matcher.match("Test_-_103_[1280x720]").get(0));
}
@ -64,11 +66,14 @@ public class SeasonEpisodeMatcherTest {
// test high values
assertEquals(new SxE(10, 1), matcher.match("[Test]_1001_High_Values").get(0));
// first two digits <= 29
// test season digits <= 19
assertEquals(null, matcher.match("The 4400"));
// test lookbehind
// test look-behind
assertEquals(null, matcher.match("720p"));
// test ambiguous match processing
assertEquals(asList(new SxE(1, 1), new SxE(UNDEFINED, 101)), matcher.match("Test.101"));
}
}