* fix for alias-aware matching
This commit is contained in:
parent
56424aafe1
commit
bd136671ff
|
@ -540,7 +540,7 @@ public class MediaDetection {
|
|||
try {
|
||||
Movie movie = (Movie) xattr.getObject();
|
||||
if (movie != null) {
|
||||
options.add(new Movie(movie)); // normalize as movie object
|
||||
options.add(movie.clone()); // normalize as movie object
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// can't read meta attributes => ignore
|
||||
|
|
|
@ -303,6 +303,8 @@ public enum EpisodeMetrics implements SimilarityMetric {
|
|||
}
|
||||
}),
|
||||
|
||||
NameBalancer(new MetricCascade(NameSubstringSequence, Name)),
|
||||
|
||||
// Match by generic name similarity (absolute)
|
||||
SeriesName(new NameSimilarityMetric() {
|
||||
|
||||
|
@ -539,7 +541,11 @@ public enum EpisodeMetrics implements SimilarityMetric {
|
|||
public float getSimilarity(Object o1, Object o2) {
|
||||
float r1 = getRating(o1);
|
||||
float r2 = getRating(o2);
|
||||
return max(r1, r2) >= 0.4 ? 1 : min(r1, r2) < 0 ? -1 : 0;
|
||||
|
||||
if (r1 < 0 || r2 < 0)
|
||||
return -1;
|
||||
|
||||
return max(r1, r2);
|
||||
}
|
||||
|
||||
private final Map<String, SeriesInfo> seriesInfoCache = new HashMap<String, SeriesInfo>();
|
||||
|
@ -561,12 +567,16 @@ public enum EpisodeMetrics implements SimilarityMetric {
|
|||
}
|
||||
|
||||
if (seriesInfo != null) {
|
||||
if (seriesInfo.getRatingCount() > 0) {
|
||||
float rating = max(0, seriesInfo.getRating().floatValue());
|
||||
return seriesInfo.getRatingCount() >= 15 ? rating : 0; // PENALIZE SHOWS WITH FEW RATINGS
|
||||
} else {
|
||||
return -1; // BIG PENALTY FOR SHOWS WITH 0 RATINGS
|
||||
if (seriesInfo.getRatingCount() >= 100) {
|
||||
return (float) floor(seriesInfo.getRating() / 3) + 1; // BOOST POPULAR SHOWS
|
||||
}
|
||||
if (seriesInfo.getRatingCount() >= 10) {
|
||||
return (float) floor(seriesInfo.getRating() / 3); // PUT INTO 3 GROUPS
|
||||
}
|
||||
if (seriesInfo.getRatingCount() >= 1) {
|
||||
return 0; // PENALIZE SHOWS WITH FEW RATINGS
|
||||
}
|
||||
return -1; // BIG PENALTY FOR SHOWS WITH 0 RATINGS
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -642,7 +652,7 @@ public enum EpisodeMetrics implements SimilarityMetric {
|
|||
|
||||
// ignore everything else
|
||||
return emptyMap();
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
@ -706,9 +716,9 @@ public enum EpisodeMetrics implements SimilarityMetric {
|
|||
// 7 pass: prefer episodes that were aired closer to the last modified date of the file
|
||||
// 8 pass: resolve remaining collisions via absolute string similarity
|
||||
if (includeFileMetrics) {
|
||||
return new SimilarityMetric[] { FileSize, new MetricCascade(FileName, EpisodeFunnel), EpisodeBalancer, AirDate, MetaAttributes, SubstringFields, new MetricCascade(NameSubstringSequence, Name), Numeric, NumericSequence, SeriesName, RegionHint, SeriesRating, TimeStamp, AbsolutePath };
|
||||
return new SimilarityMetric[] { FileSize, new MetricCascade(FileName, EpisodeFunnel), EpisodeBalancer, AirDate, MetaAttributes, SubstringFields, NameBalancer, Numeric, NumericSequence, SeriesName, RegionHint, SeriesRating, TimeStamp, AbsolutePath };
|
||||
} else {
|
||||
return new SimilarityMetric[] { EpisodeFunnel, EpisodeBalancer, AirDate, MetaAttributes, SubstringFields, new MetricCascade(NameSubstringSequence, Name), Numeric, NumericSequence, SeriesName, RegionHint, SeriesRating, TimeStamp, AbsolutePath };
|
||||
return new SimilarityMetric[] { EpisodeFunnel, EpisodeBalancer, AirDate, MetaAttributes, SubstringFields, NameBalancer, Numeric, NumericSequence, SeriesName, RegionHint, SeriesRating, TimeStamp, AbsolutePath };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
package net.sourceforge.filebot.similarity;
|
||||
|
||||
|
||||
import static java.util.Collections.*;
|
||||
|
||||
import java.util.AbstractList;
|
||||
|
@ -20,77 +18,71 @@ import java.util.Set;
|
|||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
|
||||
public class Matcher<V, C> {
|
||||
|
||||
|
||||
protected final List<V> values;
|
||||
protected final List<C> candidates;
|
||||
|
||||
|
||||
protected final boolean strict;
|
||||
protected final SimilarityMetric[] metrics;
|
||||
|
||||
|
||||
protected final DisjointMatchCollection<V, C> disjointMatchCollection;
|
||||
|
||||
|
||||
|
||||
public Matcher(Collection<? extends V> values, Collection<? extends C> candidates, boolean strict, SimilarityMetric[] metrics) {
|
||||
this.values = new LinkedList<V>(values);
|
||||
this.candidates = new LinkedList<C>(candidates);
|
||||
|
||||
|
||||
this.strict = strict;
|
||||
this.metrics = metrics.clone();
|
||||
|
||||
|
||||
this.disjointMatchCollection = new DisjointMatchCollection<V, C>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public synchronized List<Match<V, C>> match() throws InterruptedException {
|
||||
// list of all combinations of values and candidates
|
||||
List<Match<V, C>> possibleMatches = new ArrayList<Match<V, C>>(values.size() * candidates.size());
|
||||
|
||||
|
||||
// populate with all possible matches
|
||||
for (V value : values) {
|
||||
for (C candidate : candidates) {
|
||||
possibleMatches.add(new Match<V, C>(value, candidate));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// match recursively
|
||||
deepMatch(possibleMatches, 0);
|
||||
|
||||
|
||||
// restore order according to the given values
|
||||
List<Match<V, C>> result = new ArrayList<Match<V, C>>();
|
||||
|
||||
|
||||
for (V value : values) {
|
||||
Match<V, C> match = disjointMatchCollection.getByValue(value);
|
||||
|
||||
|
||||
if (match != null) {
|
||||
result.add(match);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// remove matched objects
|
||||
for (Match<V, C> match : result) {
|
||||
values.remove(match.getValue());
|
||||
candidates.remove(match.getCandidate());
|
||||
}
|
||||
|
||||
|
||||
// clear collected matches
|
||||
disjointMatchCollection.clear();
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public synchronized List<V> remainingValues() {
|
||||
return Collections.unmodifiableList(values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public synchronized List<C> remainingCandidates() {
|
||||
return Collections.unmodifiableList(candidates);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected void deepMatch(Collection<Match<V, C>> possibleMatches, int level) throws InterruptedException {
|
||||
if (level >= metrics.length || possibleMatches.isEmpty()) {
|
||||
// add the first possible match if non-strict, otherwise ignore ambiguous matches
|
||||
|
@ -98,7 +90,7 @@ public class Matcher<V, C> {
|
|||
// order alphabetically to get more predictable matching (when no matching is possible anymore)
|
||||
List<Match<V, C>> rest = new ArrayList<Match<V, C>>(possibleMatches);
|
||||
sort(rest, new Comparator<Match<V, C>>() {
|
||||
|
||||
|
||||
@Override
|
||||
public int compare(Match<V, C> o1, Match<V, C> o2) {
|
||||
return o1.toString().compareToIgnoreCase(o2.toString());
|
||||
|
@ -106,97 +98,94 @@ public class Matcher<V, C> {
|
|||
});
|
||||
disjointMatchCollection.addAll(rest);
|
||||
}
|
||||
|
||||
|
||||
// no further refinement possible
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (Set<Match<V, C>> matchesWithEqualSimilarity : mapBySimilarity(possibleMatches, metrics[level]).values()) {
|
||||
// some matches may already be unique
|
||||
List<Match<V, C>> disjointMatches = disjointMatches(matchesWithEqualSimilarity);
|
||||
|
||||
|
||||
if (!disjointMatches.isEmpty()) {
|
||||
// collect disjoint matches
|
||||
disjointMatchCollection.addAll(disjointMatches);
|
||||
|
||||
|
||||
// no need for further matching
|
||||
matchesWithEqualSimilarity.removeAll(disjointMatches);
|
||||
}
|
||||
|
||||
|
||||
// remove invalid matches
|
||||
removeCollected(matchesWithEqualSimilarity);
|
||||
|
||||
|
||||
// matches may be ambiguous, more refined matching required
|
||||
deepMatch(matchesWithEqualSimilarity, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected void removeCollected(Collection<Match<V, C>> matches) {
|
||||
for (Iterator<Match<V, C>> iterator = matches.iterator(); iterator.hasNext();) {
|
||||
if (!disjointMatchCollection.disjoint(iterator.next()))
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected SortedMap<Float, Set<Match<V, C>>> mapBySimilarity(Collection<Match<V, C>> possibleMatches, SimilarityMetric metric) throws InterruptedException {
|
||||
// map sorted by similarity descending
|
||||
SortedMap<Float, Set<Match<V, C>>> similarityMap = new TreeMap<Float, Set<Match<V, C>>>(Collections.reverseOrder());
|
||||
|
||||
|
||||
// use metric on all matches
|
||||
for (Match<V, C> possibleMatch : possibleMatches) {
|
||||
float similarity = metric.getSimilarity(possibleMatch.getValue(), possibleMatch.getCandidate());
|
||||
|
||||
|
||||
// DEBUG
|
||||
// System.out.format("%s: %.04f: %s%n", metric, similarity, possibleMatch);
|
||||
|
||||
|
||||
Set<Match<V, C>> matchSet = similarityMap.get(similarity);
|
||||
if (matchSet == null) {
|
||||
matchSet = new LinkedHashSet<Match<V, C>>();
|
||||
similarityMap.put(similarity, matchSet);
|
||||
}
|
||||
matchSet.add(possibleMatch);
|
||||
|
||||
|
||||
// unwind this thread if we have been interrupted
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return similarityMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected List<Match<V, C>> disjointMatches(Collection<Match<V, C>> collection) {
|
||||
Map<V, List<Match<V, C>>> matchesByValue = new HashMap<V, List<Match<V, C>>>();
|
||||
Map<C, List<Match<V, C>>> matchesByCandidate = new HashMap<C, List<Match<V, C>>>();
|
||||
|
||||
|
||||
// map matches by value and candidate respectively
|
||||
for (Match<V, C> match : collection) {
|
||||
List<Match<V, C>> matchListForValue = matchesByValue.get(match.getValue());
|
||||
List<Match<V, C>> matchListForCandidate = matchesByCandidate.get(match.getCandidate());
|
||||
|
||||
|
||||
// create list if necessary
|
||||
if (matchListForValue == null) {
|
||||
matchListForValue = new ArrayList<Match<V, C>>();
|
||||
matchesByValue.put(match.getValue(), matchListForValue);
|
||||
}
|
||||
|
||||
|
||||
// create list if necessary
|
||||
if (matchListForCandidate == null) {
|
||||
matchListForCandidate = new ArrayList<Match<V, C>>();
|
||||
matchesByCandidate.put(match.getCandidate(), matchListForCandidate);
|
||||
}
|
||||
|
||||
|
||||
// add match to both lists
|
||||
matchListForValue.add(match);
|
||||
matchListForCandidate.add(match);
|
||||
}
|
||||
|
||||
|
||||
// collect disjoint matches
|
||||
List<Match<V, C>> disjointMatches = new ArrayList<Match<V, C>>();
|
||||
|
||||
|
||||
for (List<Match<V, C>> matchListForValue : matchesByValue.values()) {
|
||||
// check if match is the only element in both lists
|
||||
if (matchListForValue.size() == 1 && matchListForValue.equals(matchesByCandidate.get(matchListForValue.get(0).getCandidate()))) {
|
||||
|
@ -204,66 +193,58 @@ public class Matcher<V, C> {
|
|||
disjointMatches.add(matchListForValue.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return disjointMatches;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected static class DisjointMatchCollection<V, C> extends AbstractList<Match<V, C>> {
|
||||
|
||||
|
||||
private final List<Match<V, C>> matches = new ArrayList<Match<V, C>>();
|
||||
|
||||
|
||||
private final Map<V, Match<V, C>> values = new IdentityHashMap<V, Match<V, C>>();
|
||||
private final Map<C, Match<V, C>> candidates = new IdentityHashMap<C, Match<V, C>>();
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean add(Match<V, C> match) {
|
||||
if (disjoint(match)) {
|
||||
values.put(match.getValue(), match);
|
||||
candidates.put(match.getCandidate(), match);
|
||||
|
||||
|
||||
return matches.add(match);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean disjoint(Match<V, C> match) {
|
||||
return !values.containsKey(match.getValue()) && !candidates.containsKey(match.getCandidate());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Match<V, C> getByValue(V value) {
|
||||
return values.get(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Match<V, C> getByCandidate(C candidate) {
|
||||
return candidates.get(candidate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Match<V, C> get(int index) {
|
||||
return matches.get(index);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return matches.size();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
matches.clear();
|
||||
values.clear();
|
||||
candidates.clear();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -16,10 +16,6 @@ public class Movie extends SearchResult {
|
|||
// used by serializer
|
||||
}
|
||||
|
||||
public Movie(Movie obj) {
|
||||
this(obj.name, obj.aliasNames, obj.year, obj.imdbId, obj.tmdbId);
|
||||
}
|
||||
|
||||
public Movie(String name, int year, int imdbId, int tmdbId) {
|
||||
this(name, new String[0], year, imdbId, tmdbId);
|
||||
}
|
||||
|
@ -90,7 +86,7 @@ public class Movie extends SearchResult {
|
|||
|
||||
@Override
|
||||
public Movie clone() {
|
||||
return new Movie(this);
|
||||
return new Movie(name, aliasNames, year, imdbId, tmdbId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -42,7 +42,7 @@ public class TheTVDBSearchResult extends SearchResult {
|
|||
|
||||
@Override
|
||||
public TheTVDBSearchResult clone() {
|
||||
return new TheTVDBSearchResult(name, seriesId);
|
||||
return new TheTVDBSearchResult(name, aliasNames, seriesId);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue