diff --git a/source/net/sourceforge/filebot/similarity/EpisodeMatcher.java b/source/net/sourceforge/filebot/similarity/EpisodeMatcher.java index fe72f058..9e97a212 100644 --- a/source/net/sourceforge/filebot/similarity/EpisodeMatcher.java +++ b/source/net/sourceforge/filebot/similarity/EpisodeMatcher.java @@ -1,7 +1,5 @@ - package net.sourceforge.filebot.similarity; - import static java.util.Collections.*; import java.io.File; @@ -19,15 +17,13 @@ import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.MultiEpisode; - public class EpisodeMatcher extends Matcher { - + public EpisodeMatcher(Collection values, Collection candidates, boolean strict) { // use strict matcher as to force a result from the final top similarity set super(values, candidates, strict, strict ? StrictEpisodeMetrics.defaultSequence(false) : EpisodeMetrics.defaultSequence(false)); } - - + @Override protected void deepMatch(Collection> possibleMatches, int level) throws InterruptedException { Map> episodeSets = new IdentityHashMap>(); @@ -39,7 +35,7 @@ public class EpisodeMatcher extends Matcher { } episodes.add((Episode) it.getCandidate()); } - + Map> episodeIdentifierSets = new IdentityHashMap>(); for (Entry> it : episodeSets.entrySet()) { Set sxe = new HashSet(it.getValue().size()); @@ -48,16 +44,16 @@ public class EpisodeMatcher extends Matcher { } episodeIdentifierSets.put(it.getKey(), sxe); } - + boolean modified = false; for (Match it : possibleMatches) { File file = it.getValue(); Set uniqueFiles = parseEpisodeIdentifer(file); Set uniqueEpisodes = episodeIdentifierSets.get(file); - + if (uniqueFiles.equals(uniqueEpisodes)) { Episode[] episodes = episodeSets.get(file).toArray(new Episode[0]); - + if (isMultiEpisode(episodes)) { MultiEpisode episode = new MultiEpisode(episodes); disjointMatchCollection.add(new Match(file, episode)); @@ -65,57 +61,55 @@ public class EpisodeMatcher extends Matcher { } } } - + if (modified) { removeCollected(possibleMatches); } - + super.deepMatch(possibleMatches, level); - + } - - private final SeasonEpisodeMatcher seasonEpisodeMatcher = new SeasonEpisodeMatcher(SeasonEpisodeMatcher.DEFAULT_SANITY, true); + + private final SeasonEpisodeMatcher seasonEpisodeMatcher = new SeasonEpisodeMatcher(SeasonEpisodeMatcher.DEFAULT_SANITY, false); private final Map> transformCache = synchronizedMap(new HashMap>(64, 4)); - - + private Set parseEpisodeIdentifer(File file) { Set result = transformCache.get(file); if (result != null) { return result; } - + List sxe = seasonEpisodeMatcher.match(file.getName()); if (sxe != null) { result = new HashSet(sxe); } else { result = emptySet(); } - + transformCache.put(file, result); return result; } - - + private boolean isMultiEpisode(Episode[] episodes) { // check episode sequence integrity Integer seqIndex = null; for (Episode ep : episodes) { if (seqIndex != null && !ep.getEpisode().equals(seqIndex + 1)) return false; - + seqIndex = ep.getEpisode(); } - + // check drill-down integrity String seriesName = null; for (Episode ep : episodes) { if (seriesName != null && !seriesName.equals(ep.getSeriesName())) return false; - + seriesName = ep.getSeriesName(); } - + return true; } - + } diff --git a/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java b/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java index 22387dce..42cde733 100644 --- a/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java +++ b/source/net/sourceforge/filebot/similarity/SeasonEpisodeMatcher.java @@ -1,8 +1,5 @@ - package net.sourceforge.filebot.similarity; - -import static java.util.Arrays.*; import static java.util.Collections.*; import static java.util.regex.Pattern.*; @@ -10,30 +7,30 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Scanner; +import java.util.Set; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; - public class SeasonEpisodeMatcher { - + public static final SeasonEpisodeFilter DEFAULT_SANITY = new SeasonEpisodeFilter(50, 50, 1000); - + private SeasonEpisodePattern[] patterns; private Pattern seasonPattern; - - + public SeasonEpisodeMatcher(SeasonEpisodeFilter sanity, boolean strict) { patterns = new SeasonEpisodePattern[5]; - + // match patterns like Season 01 Episode 02, ... patterns[0] = new SeasonEpisodePattern(null, "(? process(MatchResult match) { List matches = new ArrayList(2); @@ -44,10 +41,10 @@ public class SeasonEpisodeMatcher { return matches; } }; - + // match patterns like 1x01, 1.02, ..., 1x01a, 10x01, 10.02, ... 1x01-02-03-04, 1x01x02x03x04 ... patterns[2] = new SeasonEpisodePattern(sanity, "(? process(MatchResult match) { List matches = new ArrayList(2); @@ -58,54 +55,61 @@ public class SeasonEpisodeMatcher { return matches; } }; - + // match patterns like ep1, ep.1, ... patterns[3] = new SeasonEpisodePattern(sanity, "(? process(MatchResult match) { // regex doesn't match season return singleton(new SxE(null, match.group(1))); } }; - - // match patterns like 01, 102, 1003 (enclosed in separators) - patterns[4] = new SeasonEpisodePattern(sanity, "(? process(MatchResult match) { + Set sxe = new LinkedHashSet(2); + // interpret match as season and episode - SxE seasonEpisode = new SxE(match.group(1), match.group(2)); - - // interpret match as episode number only - SxE absoluteEpisode = new SxE(null, match.group(1) + match.group(2)); - + for (int i = 2; i <= match.groupCount(); i++) { + if (match.group(i) != null) { + sxe.add(new SxE(match.group(1), match.group(i))); + } + } + + // interpret match both ways, as SxE match as well as episode number only match if it's not an double episode + if (sxe.size() < 2) { + sxe.add(new SxE(null, match.group(1) + match.group(2))); + } + // return both matches, unless they are one and the same - return seasonEpisode.equals(absoluteEpisode) ? singleton(seasonEpisode) : asList(seasonEpisode, absoluteEpisode); + return sxe; } }; - + // only use S00E00 and SxE pattern in strict mode if (strict) { patterns = new SeasonEpisodePattern[] { patterns[0], patterns[1], patterns[2] }; } - + // season folder pattern for complementing partial sxe info from filename seasonPattern = compile("Season[-._ ]?(\\d{1,2})", CASE_INSENSITIVE | UNICODE_CASE); } - - + /** * 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 + * @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 */ public List match(CharSequence name) { for (SeasonEpisodePattern pattern : patterns) { List match = pattern.match(name); - + if (!match.isEmpty()) { // current pattern did match return match; @@ -113,12 +117,11 @@ public class SeasonEpisodeMatcher { } return null; } - - + public List match(File file) { for (SeasonEpisodePattern pattern : patterns) { List match = pattern.match(file.getName()); - + if (!match.isEmpty()) { // current pattern did match for (int i = 0; i < match.size(); i++) { @@ -134,57 +137,51 @@ public class SeasonEpisodeMatcher { } return null; } - - + public int find(CharSequence name, int fromIndex) { for (SeasonEpisodePattern pattern : patterns) { int index = pattern.find(name, fromIndex); - + if (index >= 0) { // current pattern did match return index; } } - + return -1; } - - + public Matcher matcher(CharSequence name) { for (SeasonEpisodePattern pattern : patterns) { Matcher matcher = pattern.matcher(name); - - // check if current pattern matches + + // check if current pattern matches if (matcher.find()) { // reset matcher state return matcher.reset(); } } - + return null; } - - + public static class SxE { - + public static final int UNDEFINED = -1; - + public final int season; public final int episode; - - + public SxE(Integer season, Integer episode) { this.season = season != null ? season : UNDEFINED; this.episode = episode != null ? episode : UNDEFINED; } - - + public SxE(String season, String episode) { this.season = parse(season); this.episode = parse(episode); } - - + protected int parse(String number) { try { return Integer.parseInt(number); @@ -192,80 +189,69 @@ public class SeasonEpisodeMatcher { return UNDEFINED; } } - - + @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 int hashCode() { return Arrays.hashCode(new Object[] { season, episode }); } - - + @Override public String toString() { return season >= 0 ? String.format("%dx%02d", season, episode) : String.format("%02d", episode); } } - - + public static class SeasonEpisodeFilter { - + public final int seasonLimit; public final int seasonEpisodeLimit; public final int absoluteEpisodeLimit; - - + public SeasonEpisodeFilter(int seasonLimit, int seasonEpisodeLimit, int absoluteEpisodeLimit) { this.seasonLimit = seasonLimit; this.seasonEpisodeLimit = seasonEpisodeLimit; this.absoluteEpisodeLimit = absoluteEpisodeLimit; } - - + boolean filter(SxE sxe) { return (sxe.season >= 0 && sxe.season < seasonLimit && sxe.episode < seasonEpisodeLimit) || (sxe.season < 0 && sxe.episode < absoluteEpisodeLimit); } } - - + public static class SeasonEpisodePattern { - + protected final Pattern pattern; protected final SeasonEpisodeFilter sanity; - - + public SeasonEpisodePattern(SeasonEpisodeFilter sanity, String pattern) { this.pattern = Pattern.compile(pattern); this.sanity = sanity; } - - + public Matcher matcher(CharSequence name) { return pattern.matcher(name); } - - + protected Collection process(MatchResult match) { return singleton(new SxE(match.group(1), match.group(2))); } - - + public List match(CharSequence name) { // name will probably contain no more than two matches List matches = new ArrayList(2); - + Matcher matcher = matcher(name); - + while (matcher.find()) { for (SxE value : process(matcher)) { if (sanity == null || sanity.filter(value)) { @@ -273,14 +259,13 @@ public class SeasonEpisodeMatcher { } } } - + return matches; } - - + public int find(CharSequence name, int fromIndex) { Matcher matcher = matcher(name).region(fromIndex, name.length()); - + while (matcher.find()) { for (SxE value : process(matcher)) { if (sanity == null || sanity.filter(value)) { @@ -288,9 +273,9 @@ public class SeasonEpisodeMatcher { } } } - + return -1; } } - + }