mirror of
https://github.com/mitb-archive/filebot
synced 2025-01-11 05:48:01 -05:00
* improved matching of episodes with episode number > 99
* refactoring
This commit is contained in:
parent
64f1cd7040
commit
a500aacf80
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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)) {
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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,17 +90,17 @@ 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()) {
|
||||
if (!destinationFolder.isDirectory() && !destinationFolder.mkdirs()) {
|
||||
throw new IOException("Failed to create folder: " + destinationFolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (!file.renameTo(destination)) {
|
||||
throw new IOException("Failed to rename file: " + file.getName());
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
@ -2,12 +2,14 @@
|
||||
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 {
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user