diff --git a/source/net/filebot/subtitle/SubtitleMetrics.java b/source/net/filebot/subtitle/SubtitleMetrics.java new file mode 100644 index 00000000..009ab15b --- /dev/null +++ b/source/net/filebot/subtitle/SubtitleMetrics.java @@ -0,0 +1,156 @@ +package net.filebot.subtitle; + +import static java.util.Collections.*; +import static net.filebot.media.MediaDetection.*; +import static net.filebot.similarity.EpisodeMetrics.*; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.filebot.mediainfo.MediaInfo; +import net.filebot.mediainfo.MediaInfo.StreamKind; +import net.filebot.similarity.CrossPropertyMetric; +import net.filebot.similarity.EpisodeMetrics; +import net.filebot.similarity.MetricAvg; +import net.filebot.similarity.MetricCascade; +import net.filebot.similarity.NameSimilarityMetric; +import net.filebot.similarity.NumericSimilarityMetric; +import net.filebot.similarity.SimilarityMetric; +import net.filebot.web.OpenSubtitlesSubtitleDescriptor; +import net.filebot.web.SubtitleDescriptor; + +public enum SubtitleMetrics implements SimilarityMetric { + + // subtitle verification metric specifically excluding SxE mismatches + AbsoluteSeasonEpisode(new SimilarityMetric() { + + @Override + public float getSimilarity(Object o1, Object o2) { + float f = SeasonEpisode.getSimilarity(o1, o2); + if (f == 0 && (getEpisodeIdentifier(o1.toString(), true) == null) == (getEpisodeIdentifier(o2.toString(), true) == null)) { + return 0; + } + return f < 1 ? -1 : 1; + } + }), + + DiskNumber(new NumericSimilarityMetric() { + + private final Pattern CDNO = Pattern.compile("(?:CD|DISK)(\\d+)", Pattern.CASE_INSENSITIVE); + + @Override + public float getSimilarity(Object o1, Object o2) { + int c1 = getDiskNumber(o1); + int c2 = getDiskNumber(o2); + + if (c1 == 0 && c2 == 0) // undefined + return 0; + + return c1 == c2 ? 1 : -1; // positive or negative match + } + + public int getDiskNumber(Object o) { + int cd = 0; + Matcher matcher = CDNO.matcher(o.toString()); + while (matcher.find()) { + cd = Integer.parseInt(matcher.group(1)); + } + return cd; + } + }), + + VideoProperties(new CrossPropertyMetric() { + + private final String FPS = "FPS"; + private final String SECONDS = "SECS"; + + public float getSimilarity(Object o1, Object o2) { + return o1 instanceof SubtitleDescriptor ? super.getSimilarity(o1, o2) : super.getSimilarity(o2, o1); // make sure that SubtitleDescriptor is o1 + }; + + protected Map getProperties(Object object) { + if (object instanceof OpenSubtitlesSubtitleDescriptor) { + return getSubtitleProperties((OpenSubtitlesSubtitleDescriptor) object); + } else if (object instanceof File) { + return getVideoProperties((File) object); + } + return emptyMap(); + }; + + private Map getSubtitleProperties(OpenSubtitlesSubtitleDescriptor subtitle) { + Map props = new HashMap(); + + try { + float fps = Math.round(subtitle.getMovieFPS()); // round because most FPS values in the database are bad anyway + if (fps > 0) { + props.put(FPS, fps); + } + long seconds = (long) Math.floor(subtitle.getMovieTimeMS() / (double) 1000); + if (seconds > 0) { + props.put(SECONDS, seconds); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return props; + } + + private final Map> mediaInfoCache = new WeakHashMap>(64); + + private Map getVideoProperties(File file) { + synchronized (mediaInfoCache) { + return mediaInfoCache.computeIfAbsent(file, (f) -> { + try { + Map props = new HashMap(); + + MediaInfo mediaInfo = new MediaInfo(); + if (mediaInfo.open(file)) { + float fps = Math.round(Float.parseFloat(mediaInfo.get(StreamKind.Video, 0, "FrameRate"))); + if (fps > 0) { + props.put(FPS, fps); + } + long seconds = (long) Math.floor(Long.parseLong(mediaInfo.get(StreamKind.Video, 0, "Duration")) / (double) 1000); + if (seconds > 0) { + props.put(SECONDS, seconds); + } + } + + return props; + } catch (Exception e) { + return emptyMap(); + } + }); + } + } + }); + + // inner metric + private final SimilarityMetric metric; + + private SubtitleMetrics(SimilarityMetric metric) { + this.metric = metric; + } + + @Override + public float getSimilarity(Object o1, Object o2) { + return metric.getSimilarity(o1, o2); + } + + public static SimilarityMetric[] defaultSequence() { + return new SimilarityMetric[] { EpisodeFunnel, EpisodeBalancer, NameSubstringSequence, new MetricCascade(NameSubstringSequence, Name), Numeric, FileName, DiskNumber, VideoProperties, new NameSimilarityMetric() }; + } + + public static SimilarityMetric verificationMetric() { + return EpisodeMetrics.verificationMetric(); + } + + public static SimilarityMetric sanityMetric() { + return new MetricCascade(AbsoluteSeasonEpisode, AirDate, new MetricAvg(NameSubstringSequence, Name), getMovieMatchMetric()); + } + +} diff --git a/source/net/filebot/subtitle/SubtitleUtilities.java b/source/net/filebot/subtitle/SubtitleUtilities.java index e22089cf..cc999e80 100644 --- a/source/net/filebot/subtitle/SubtitleUtilities.java +++ b/source/net/filebot/subtitle/SubtitleUtilities.java @@ -4,7 +4,6 @@ import static java.lang.Math.*; import static java.util.Collections.*; import static net.filebot.MediaTypes.*; import static net.filebot.media.MediaDetection.*; -import static net.filebot.similarity.EpisodeMetrics.*; import static net.filebot.similarity.Normalization.*; import static net.filebot.util.FileUtilities.*; @@ -35,7 +34,6 @@ import net.filebot.similarity.EpisodeMetrics; import net.filebot.similarity.Match; import net.filebot.similarity.Matcher; import net.filebot.similarity.MetricAvg; -import net.filebot.similarity.MetricCascade; import net.filebot.similarity.NameSimilarityMetric; import net.filebot.similarity.SequenceMatchSimilarity; import net.filebot.similarity.SimilarityMetric; @@ -139,11 +137,11 @@ public final class SubtitleUtilities { } // add other possible matches to the options - SimilarityMetric sanity = EpisodeMetrics.verificationMetric(); + SimilarityMetric sanity = SubtitleMetrics.verificationMetric(); float minMatchSimilarity = strict ? 0.9f : 0.6f; // first match everything as best as possible, then filter possibly bad matches - for (Entry it : matchSubtitles(files, subtitles, false).entrySet()) { + for (Entry it : matchSubtitles(files, subtitles).entrySet()) { if (sanity.getSimilarity(it.getKey(), it.getValue()) >= minMatchSimilarity) { subtitlesByFile.get(it.getKey()).add(it.getValue()); } @@ -178,31 +176,20 @@ public final class SubtitleUtilities { return subtitlesByFile; } - public static Map matchSubtitles(Collection files, Collection subtitles, boolean strict) throws InterruptedException { + public static Map matchSubtitles(Collection files, Collection subtitles) throws InterruptedException { Map subtitleByVideo = new LinkedHashMap(); // optimize for generic media <-> subtitle matching - SimilarityMetric[] metrics = new SimilarityMetric[] { EpisodeFunnel, EpisodeBalancer, NameSubstringSequence, new MetricCascade(NameSubstringSequence, Name), Numeric, new NameSimilarityMetric() }; - - // subtitle verification metric specifically excluding SxE mismatches - SimilarityMetric absoluteSeasonEpisode = new SimilarityMetric() { - - @Override - public float getSimilarity(Object o1, Object o2) { - float f = SeasonEpisode.getSimilarity(o1, o2); - if (f == 0 && (getEpisodeIdentifier(o1.toString(), true) == null) == (getEpisodeIdentifier(o2.toString(), true) == null)) { - return 0; - } - return f < 1 ? -1 : 1; - } - }; - SimilarityMetric sanity = new MetricCascade(absoluteSeasonEpisode, AirDate, new MetricAvg(NameSubstringSequence, Name), getMovieMatchMetric()); + SimilarityMetric[] metrics = SubtitleMetrics.defaultSequence(); // first match everything as best as possible, then filter possibly bad matches Matcher matcher = new Matcher(files, subtitles, false, metrics); + SimilarityMetric sanity = SubtitleMetrics.sanityMetric(); + float minSanitySimilarity = 0.1f; + for (Match it : matcher.match()) { - if (sanity.getSimilarity(it.getValue(), it.getCandidate()) >= (strict ? 0.9f : 0.6f)) { + if (sanity.getSimilarity(it.getValue(), it.getCandidate()) >= minSanitySimilarity) { subtitleByVideo.put(it.getValue(), it.getCandidate()); } } @@ -250,7 +237,7 @@ public final class SubtitleUtilities { } try { - return matchSubtitles(singleton(file), subtitles, strict).entrySet().iterator().next().getValue(); + return matchSubtitles(singleton(file), subtitles).entrySet().iterator().next().getValue(); } catch (NoSuchElementException e) { return null; } catch (InterruptedException e) { diff --git a/source/net/filebot/web/OpenSubtitlesSubtitleDescriptor.java b/source/net/filebot/web/OpenSubtitlesSubtitleDescriptor.java index 46eeca27..603ffa35 100644 --- a/source/net/filebot/web/OpenSubtitlesSubtitleDescriptor.java +++ b/source/net/filebot/web/OpenSubtitlesSubtitleDescriptor.java @@ -96,6 +96,22 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor, Seri return Integer.parseInt(getProperty(Property.QueryNumber)); } + public float getMovieFPS() { + return Float.parseFloat(getProperty(Property.MovieFPS)); + } + + public long getMovieTimeMS() { + return Long.parseLong(getProperty(Property.MovieTimeMS)); + } + + public int getSubActualCD() { + return Integer.parseInt(getProperty(Property.SubActualCD)); + } + + public int getSubSumCD() { + return Integer.parseInt(getProperty(Property.SubSumCD)); + } + @Override public ByteBuffer fetch() throws Exception { URL resource = new URL(getProperty(Property.SubDownloadLink));