diff --git a/fw/script.png b/fw/script.png new file mode 100644 index 00000000..ff39a27d Binary files /dev/null and b/fw/script.png differ diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index a28b7b2a..5bcb8019 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -18,6 +18,8 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -45,6 +47,7 @@ import net.sourceforge.filebot.hash.VerificationFileWriter; import net.sourceforge.filebot.similarity.EpisodeMetrics; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Matcher; +import net.sourceforge.filebot.similarity.MetricCascade; import net.sourceforge.filebot.similarity.NameSimilarityMetric; import net.sourceforge.filebot.similarity.SeriesNameMatcher; import net.sourceforge.filebot.similarity.SimilarityMetric; @@ -122,21 +125,9 @@ public class CmdlineOperations implements CmdlineInterface { public List renameSeries(Collection files, String query, ExpressionFormat format, EpisodeListProvider db, Locale locale, boolean strict) throws Exception { CLILogger.config(format("Rename episodes using [%s]", db.getName())); List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); - Collection seriesNames; // auto-detect series name if not given - if (query == null) { - seriesNames = new SeriesNameMatcher().matchAll(mediaFiles.toArray(new File[0])); - - if (seriesNames.isEmpty() || (strict && seriesNames.size() > 1)) { - throw new Exception("Unable to auto-select series name: " + seriesNames); - } - - query = seriesNames.iterator().next(); - CLILogger.config("Auto-detected series name: " + seriesNames); - } else { - seriesNames = singleton(query); - } + Collection seriesNames = (query == null) ? detectQuery(mediaFiles, strict) : singleton(query); // fetch episode data Set episodes = fetchEpisodeSet(db, seriesNames, locale, strict); @@ -146,11 +137,11 @@ public class CmdlineOperations implements CmdlineInterface { } // similarity metrics for matching - SimilarityMetric[] sequence = strict ? StrictEpisodeMetrics.defaultSequence() : EpisodeMetrics.defaultSequence(false); + SimilarityMetric[] sequence = strict ? StrictEpisodeMetrics.defaultSequence(false) : EpisodeMetrics.defaultSequence(false); List> matches = new ArrayList>(); - matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, sequence)); - matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, sequence)); + matches.addAll(matchEpisodes(filter(mediaFiles, VIDEO_FILES), episodes, sequence)); + matches.addAll(matchEpisodes(filter(mediaFiles, SUBTITLE_FILES), episodes, sequence)); if (matches.isEmpty()) { throw new RuntimeException("Unable to match files to episode data"); @@ -179,6 +170,19 @@ public class CmdlineOperations implements CmdlineInterface { } + private List> matchEpisodes(Collection files, Collection episodes, SimilarityMetric[] sequence) throws Exception { + // always use strict fail-fast matcher + Matcher matcher = new Matcher(files, episodes, true, sequence); + List> matches = matcher.match(); + + for (File failedMatch : matcher.remainingValues()) { + CLILogger.warning("No matching episode: " + failedMatch.getName()); + } + + return matches; + } + + private Set fetchEpisodeSet(final EpisodeListProvider db, final Collection names, final Locale locale, final boolean strict) throws Exception { List>> tasks = new ArrayList>>(); @@ -302,132 +306,6 @@ public class CmdlineOperations implements CmdlineInterface { } - @Override - public List getSubtitles(Collection files, String query, String languageName, String output, String csn) throws Exception { - Language language = getLanguage(languageName); - Charset outputEncoding = (csn != null) ? Charset.forName(csn) : null; - - // match movie hashes online - Set remainingVideos = new TreeSet(filter(files, VIDEO_FILES)); - List downloadedSubtitles = new ArrayList(); - - if (remainingVideos.isEmpty()) { - throw new IllegalArgumentException("No video files: " + files); - } - - SubtitleFormat outputFormat = null; - if (output != null) { - outputFormat = getSubtitleFormatByName(output); - - // when rewriting subtitles to target format an encoding must be defined, default to UTF-8 - if (outputEncoding == null) { - outputEncoding = Charset.forName("UTF-8"); - } - } - - // lookup subtitles by hash - for (VideoHashSubtitleService service : WebServices.getVideoHashSubtitleServices()) { - if (remainingVideos.isEmpty()) { - break; - } - - CLILogger.fine("Looking up subtitles by filehash via " + service.getName()); - - for (Entry> it : service.getSubtitleList(remainingVideos.toArray(new File[0]), language.getName()).entrySet()) { - if (it.getValue() != null && it.getValue().size() > 0) { - // auto-select first element if there are multiple hash matches for the same video files - File subtitle = fetchSubtitle(it.getValue().get(0), it.getKey(), outputFormat, outputEncoding); - Analytics.trackEvent(service.getName(), "DownloadSubtitle", it.getValue().get(0).getLanguageName(), 1); - - // download complete, cross this video off the list - remainingVideos.remove(it.getKey()); - downloadedSubtitles.add(subtitle); - } - } - } - - // lookup subtitles by query and filename - if (query != null && remainingVideos.size() > 0) { - for (SubtitleProvider service : WebServices.getSubtitleProviders()) { - if (remainingVideos.isEmpty()) { - break; - } - - try { - CLILogger.fine(format("Searching for [%s] at [%s]", query, service.getName())); - SearchResult searchResult = selectSearchResult(query, service.search(query), false); - - CLILogger.config(format("Retrieving subtitles for [%s]", searchResult.getName())); - List subtitles = service.getSubtitleList(searchResult, language.getName()); - - for (File video : remainingVideos.toArray(new File[0])) { - for (SubtitleDescriptor descriptor : subtitles) { - if (isDerived(descriptor.getName(), video)) { - File subtitle = fetchSubtitle(descriptor, video, outputFormat, outputEncoding); - Analytics.trackEvent(service.getName(), "DownloadSubtitle", descriptor.getLanguageName(), 1); - - // download complete, cross this video off the list - remainingVideos.remove(video); - downloadedSubtitles.add(subtitle); - break; - } - } - } - } catch (Exception e) { - CLILogger.warning(e.getMessage()); - } - } - } - - // no subtitles for remaining video files - for (File video : remainingVideos) { - CLILogger.warning("No matching subtitles found: " + video); - } - - Analytics.trackEvent("CLI", "Download", "Subtitle", downloadedSubtitles.size()); - return downloadedSubtitles; - } - - - private File fetchSubtitle(SubtitleDescriptor descriptor, File movieFile, SubtitleFormat outputFormat, Charset outputEncoding) throws Exception { - // fetch subtitle archive - CLILogger.info(format("Fetching [%s.%s]", descriptor.getName(), descriptor.getType())); - ByteBuffer downloadedData = descriptor.fetch(); - - // extract subtitles from archive - ArchiveType type = ArchiveType.forName(descriptor.getType()); - MemoryFile subtitleFile; - - if (type != ArchiveType.UNDEFINED) { - // extract subtitle from archive - subtitleFile = type.fromData(downloadedData).iterator().next(); - } else { - // assume that the fetched data is the subtitle - subtitleFile = new MemoryFile(descriptor.getName() + "." + descriptor.getType(), downloadedData); - } - - // subtitle filename is based on movie filename - String name = getName(movieFile); - String ext = getExtension(subtitleFile.getName()); - ByteBuffer data = subtitleFile.getData(); - - if (outputFormat != null || outputEncoding != null) { - if (outputFormat != null) { - ext = outputFormat.getFilter().extension(); // adjust extension of the output file - } - - CLILogger.finest(format("Export [%s] as: %s / %s", subtitleFile.getName(), outputFormat, outputEncoding.displayName(Locale.ROOT))); - data = exportSubtitles(subtitleFile, outputFormat, 0, outputEncoding); - } - - File destination = new File(movieFile.getParentFile(), name + "." + ext); - CLILogger.config(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName())); - - writeFile(data, destination); - return destination; - } - - private List renameAll(Map renameMap) throws Exception { // rename files final List> renameLog = new ArrayList>(); @@ -484,20 +362,192 @@ public class CmdlineOperations implements CmdlineInterface { } - private List> match(Collection files, Collection episodes, SimilarityMetric[] sequence) throws Exception { - // always use strict fail-fast matcher - Matcher matcher = new Matcher(files, episodes, true, sequence); - List> matches = matcher.match(); + @Override + public List getSubtitles(Collection files, String query, String languageName, String output, String csn) throws Exception { + final Language language = getLanguage(languageName); - for (File failedMatch : matcher.remainingValues()) { - CLILogger.warning("No matching episode: " + failedMatch.getName()); + // when rewriting subtitles to target format an encoding must be defined, default to UTF-8 + final Charset outputEncoding = (csn != null) ? Charset.forName(csn) : (output != null) ? Charset.forName("UTF-8") : null; + final SubtitleFormat outputFormat = (output != null) ? getSubtitleFormatByName(output) : null; + + // try to find subtitles for each video file + SubtitleCollector collector = new SubtitleCollector(filter(files, VIDEO_FILES)); + + if (collector.isComplete()) { + throw new IllegalArgumentException("No video files: " + files); } - return matches; + // lookup subtitles by hash + for (VideoHashSubtitleService service : WebServices.getVideoHashSubtitleServices()) { + if (collector.isComplete()) { + break; + } + + try { + collector.addAll(service.getName(), lookupSubtitleByHash(service, language, collector.remainingVideos())); + } catch (RuntimeException e) { + CLILogger.warning(format("Lookup by hash failed: " + e.getMessage())); + } + } + + // lookup subtitles via text search + if (!collector.isComplete()) { + // auto-detect search query + Collection querySet = (query == null) ? detectQuery(filter(files, VIDEO_FILES), false) : singleton(query); + + for (SubtitleProvider service : WebServices.getSubtitleProviders()) { + if (collector.isComplete()) { + break; + } + + try { + collector.addAll(service.getName(), lookupSubtitleByFileName(service, querySet, language, collector.remainingVideos())); + } catch (RuntimeException e) { + CLILogger.warning(format("Search for [%s] failed: %s", query, e.getMessage())); + } + } + } + + // no subtitles for remaining video files + for (File it : collector.remainingVideos()) { + CLILogger.warning("No matching subtitles found: " + it); + } + + // download subtitles in order + Map> downloadQueue = new TreeMap>(); + for (final Entry> source : collector.subtitlesBySource().entrySet()) { + for (final Entry descriptor : source.getValue().entrySet()) { + downloadQueue.put(descriptor.getKey(), new Callable() { + + @Override + public File call() throws Exception { + Analytics.trackEvent(source.getKey(), "DownloadSubtitle", descriptor.getValue().getLanguageName(), 1); + return fetchSubtitle(descriptor.getValue(), descriptor.getKey(), outputFormat, outputEncoding); + } + }); + } + } + + // parallel download + List subtitleFiles = new ArrayList(); + ExecutorService executor = Executors.newFixedThreadPool(4); + + try { + for (Future it : executor.invokeAll(downloadQueue.values())) { + subtitleFiles.add(it.get()); + } + } finally { + executor.shutdownNow(); + } + + Analytics.trackEvent("CLI", "Download", "Subtitle", subtitleFiles.size()); + return subtitleFiles; } - private SearchResult selectSearchResult(String query, Iterable searchResults, boolean strict) throws IllegalArgumentException { + private File fetchSubtitle(SubtitleDescriptor descriptor, File movieFile, SubtitleFormat outputFormat, Charset outputEncoding) throws Exception { + // fetch subtitle archive + CLILogger.info(format("Fetching [%s]", descriptor.getPath())); + ByteBuffer downloadedData = descriptor.fetch(); + + // extract subtitles from archive + ArchiveType type = ArchiveType.forName(descriptor.getType()); + MemoryFile subtitleFile; + + if (type != ArchiveType.UNDEFINED) { + // extract subtitle from archive + subtitleFile = type.fromData(downloadedData).iterator().next(); + } else { + // assume that the fetched data is the subtitle + subtitleFile = new MemoryFile(descriptor.getPath(), downloadedData); + } + + // subtitle filename is based on movie filename + String name = getName(movieFile); + String ext = getExtension(subtitleFile.getName()); + ByteBuffer data = subtitleFile.getData(); + + if (outputFormat != null || outputEncoding != null) { + if (outputFormat != null) { + ext = outputFormat.getFilter().extension(); // adjust extension of the output file + } + + CLILogger.finest(format("Export [%s] as: %s / %s", subtitleFile.getName(), outputFormat, outputEncoding.displayName(Locale.ROOT))); + data = exportSubtitles(subtitleFile, outputFormat, 0, outputEncoding); + } + + File destination = new File(movieFile.getParentFile(), name + "." + ext); + CLILogger.config(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName())); + + writeFile(data, destination); + return destination; + } + + + private Map lookupSubtitleByHash(VideoHashSubtitleService service, Language language, Collection videoFiles) throws Exception { + Map subtitleByVideo = new HashMap(videoFiles.size()); + CLILogger.fine("Looking up subtitles by filehash via " + service.getName()); + + for (Entry> it : service.getSubtitleList(videoFiles.toArray(new File[0]), language.getName()).entrySet()) { + if (it.getValue() != null && it.getValue().size() > 0) { + CLILogger.finest(format("Matched [%s] to [%s]", it.getKey().getName(), it.getValue().get(0).getName())); + subtitleByVideo.put(it.getKey(), it.getValue().get(0)); + } + } + + return subtitleByVideo; + } + + + private Map lookupSubtitleByFileName(SubtitleProvider service, Collection querySet, Language language, Collection videoFiles) throws Exception { + Map subtitleByVideo = new HashMap(); + + // search for and automatically select movie / show entry + Set resultSet = new HashSet(); + for (String query : querySet) { + CLILogger.fine(format("Searching for [%s] at [%s]", query, service.getName())); + resultSet.addAll(findProbableMatches(query, service.search(query))); + } + + // fetch subtitles for all shows / movies and match against video files + if (resultSet.size() > 0) { + List subtitles = new ArrayList(); + + for (SearchResult it : resultSet) { + List list = service.getSubtitleList(it, language.getName()); + CLILogger.config(format("Found %d subtitles for [%s] at [%s]", list.size(), it.toString(), service.getName())); + subtitles.addAll(list); + } + + // first match everything as best as possible, then filter possibly bad matches + Matcher matcher = new Matcher(videoFiles, subtitles, false, EpisodeMetrics.defaultSequence(true)); + SimilarityMetric sanity = new MetricCascade(StrictEpisodeMetrics.defaultSequence(true)); + + for (Match it : matcher.match()) { + if (sanity.getSimilarity(it.getValue(), it.getCandidate()) >= 1) { + CLILogger.finest(format("Matched [%s] to [%s]", it.getValue().getName(), it.getCandidate().getName())); + subtitleByVideo.put(it.getValue(), it.getCandidate()); + } + } + } + + return subtitleByVideo; + } + + + private Collection detectQuery(Collection mediaFiles, boolean strict) { + Collection names = new SeriesNameMatcher().matchAll(mediaFiles.toArray(new File[0])); + + if (names.isEmpty() || (strict && names.size() > 1)) { + throw new IllegalArgumentException("Unable to auto-select query: " + names); + } + + CLILogger.config("Auto-detected query: " + names); + return names; + } + + + private Collection findProbableMatches(String query, Iterable searchResults) { // auto-select most probable search result Map probableMatches = new TreeMap(String.CASE_INSENSITIVE_ORDER); @@ -507,18 +557,83 @@ public class CmdlineOperations implements CmdlineInterface { // find probable matches using name similarity > 0.9 for (SearchResult result : searchResults) { if (metric.getSimilarity(query, result.getName()) > 0.9) { - if (!probableMatches.containsKey(result.getName())) { - probableMatches.put(result.getName(), result); + if (!probableMatches.containsKey(result.toString())) { + probableMatches.put(result.toString(), result); } } } + return probableMatches.values(); + } + + + private SearchResult selectSearchResult(String query, Iterable searchResults, boolean strict) { + Collection probableMatches = findProbableMatches(query, searchResults); + if (probableMatches.isEmpty() || (strict && probableMatches.size() != 1)) { - throw new IllegalArgumentException("Failed to auto-select search result: " + probableMatches.values()); + throw new IllegalArgumentException("Failed to auto-select search result: " + probableMatches); } // return first and only value - return probableMatches.values().iterator().next(); + return probableMatches.iterator().next(); + } + + + private Language getLanguage(String lang) { + // try to look up by language code + Language language = Language.getLanguage(lang); + + if (language == null) { + // try too look up by language name + language = Language.getLanguageByName(lang); + + if (language == null) { + // unable to lookup language + throw new IllegalArgumentException("Illegal language code: " + lang); + } + } + + return language; + } + + + private class SubtitleCollector { + + private final Map> collection = new HashMap>(); + private final Set remainingVideos = new TreeSet(); + + + public SubtitleCollector(Collection videoFiles) { + remainingVideos.addAll(videoFiles); + } + + + public void addAll(String source, Map subtitles) { + remainingVideos.removeAll(subtitles.keySet()); + + Map subtitlesBySource = collection.get(source); + if (subtitlesBySource == null) { + subtitlesBySource = new TreeMap(); + collection.put(source, subtitlesBySource); + } + + subtitlesBySource.putAll(subtitles); + } + + + public Map> subtitlesBySource() { + return collection; + } + + + public Collection remainingVideos() { + return remainingVideos; + } + + + public boolean isComplete() { + return remainingVideos.size() == 0; + } } @@ -659,22 +774,4 @@ public class CmdlineOperations implements CmdlineInterface { return format.format(new MediaBindingBean(file, file)); } - - private Language getLanguage(String lang) { - // try to look up by language code - Language language = Language.getLanguage(lang); - - if (language == null) { - // try too look up by language name - language = Language.getLanguageByName(lang); - - if (language == null) { - // unable to lookup language - throw new IllegalArgumentException("Illegal language code: " + lang); - } - } - - return language; - } - } diff --git a/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java b/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java index f5ee7682..01f61b7c 100644 --- a/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java +++ b/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java @@ -14,7 +14,7 @@ import java.util.Map; import java.util.WeakHashMap; import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE; -import net.sourceforge.filebot.vfs.AbstractFile; +import net.sourceforge.filebot.vfs.FileInfo; import net.sourceforge.filebot.web.Date; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Movie; @@ -213,12 +213,25 @@ public enum EpisodeMetrics implements SimilarityMetric { @Override protected long getLength(Object object) { - if (object instanceof AbstractFile) { - return ((AbstractFile) object).getLength(); + if (object instanceof FileInfo) { + return ((FileInfo) object).getLength(); } return super.getLength(object); } + }), + + // Match by common words at the beginning of both files + FileName(new FileNameMetric() { + + @Override + protected String getFileName(Object object) { + if (object instanceof File || object instanceof FileInfo) { + return normalizeObject(object); + } + + return null; + } }); // inner metric @@ -242,8 +255,8 @@ public enum EpisodeMetrics implements SimilarityMetric { // use name without extension if (object instanceof File) { name = getName((File) object); - } else if (object instanceof AbstractFile) { - name = getNameWithoutExtension(((AbstractFile) object).getName()); + } else if (object instanceof FileInfo) { + name = ((FileInfo) object).getName(); } // remove checksums, any [...] or (...) @@ -264,7 +277,7 @@ public enum EpisodeMetrics implements SimilarityMetric { // 4. pass: match by generic name similarity (slow, but most matches will have been determined in second pass) // 5. pass: match by generic numeric similarity if (includeFileMetrics) { - return new SimilarityMetric[] { FileSize, EpisodeFunnel, EpisodeBalancer, SubstringFields, Name, Numeric }; + return new SimilarityMetric[] { FileSize, new MetricCascade(FileName, EpisodeFunnel), EpisodeBalancer, SubstringFields, Name, Numeric }; } else { return new SimilarityMetric[] { EpisodeFunnel, EpisodeBalancer, SubstringFields, Name, Numeric }; } diff --git a/source/net/sourceforge/filebot/similarity/FileNameMetric.java b/source/net/sourceforge/filebot/similarity/FileNameMetric.java new file mode 100644 index 00000000..fd1ae0d1 --- /dev/null +++ b/source/net/sourceforge/filebot/similarity/FileNameMetric.java @@ -0,0 +1,35 @@ + +package net.sourceforge.filebot.similarity; + + +import static net.sourceforge.tuned.FileUtilities.*; + +import java.io.File; + + +public class FileNameMetric implements SimilarityMetric { + + @Override + public float getSimilarity(Object o1, Object o2) { + String s1 = getFileName(o1); + if (s1 == null || s1.isEmpty()) + return 0; + + String s2 = getFileName(o2); + if (s2 == null || s2.isEmpty()) + return 0; + + return s1.startsWith(s2) || s2.startsWith(s1) ? 1 : 0; + } + + + protected String getFileName(Object object) { + if (object instanceof File) { + // name without extension normalized to lower-case + return getName((File) object).trim().toLowerCase(); + } + + return null; + } + +} diff --git a/source/net/sourceforge/filebot/similarity/FileSizeMetric.java b/source/net/sourceforge/filebot/similarity/FileSizeMetric.java index 1db9bea3..f28c2c38 100644 --- a/source/net/sourceforge/filebot/similarity/FileSizeMetric.java +++ b/source/net/sourceforge/filebot/similarity/FileSizeMetric.java @@ -20,9 +20,9 @@ public class FileSizeMetric implements SimilarityMetric { } - protected long getLength(Object o) { - if (o instanceof File) { - return ((File) o).length(); + protected long getLength(Object object) { + if (object instanceof File) { + return ((File) object).length(); } return -1; diff --git a/source/net/sourceforge/filebot/similarity/StrictEpisodeMetrics.java b/source/net/sourceforge/filebot/similarity/StrictEpisodeMetrics.java index 1c772222..8ad78c39 100644 --- a/source/net/sourceforge/filebot/similarity/StrictEpisodeMetrics.java +++ b/source/net/sourceforge/filebot/similarity/StrictEpisodeMetrics.java @@ -7,6 +7,7 @@ import static java.lang.Math.*; public enum StrictEpisodeMetrics implements SimilarityMetric { + FileName(EpisodeMetrics.FileName, 1), // only allow 0 or 1 EpisodeIdentifier(EpisodeMetrics.EpisodeIdentifier, 1), // only allow 0 or 1 SubstringFields(EpisodeMetrics.SubstringFields, 2), // allow 0 or .5 or 1 Name(EpisodeMetrics.Name, 2); // allow 0 or .5 or 1 @@ -28,8 +29,13 @@ public enum StrictEpisodeMetrics implements SimilarityMetric { } - public static SimilarityMetric[] defaultSequence() { + public static SimilarityMetric[] defaultSequence(boolean includeFileMetrics) { // use SEI for matching and SN for excluding false positives - return new SimilarityMetric[] { StrictEpisodeMetrics.EpisodeIdentifier, StrictEpisodeMetrics.SubstringFields, StrictEpisodeMetrics.Name }; + if (includeFileMetrics) { + return new SimilarityMetric[] { FileName, EpisodeIdentifier, SubstringFields, Name }; + } else { + return new SimilarityMetric[] { EpisodeIdentifier, SubstringFields, Name }; + } } + } diff --git a/source/net/sourceforge/filebot/similarity/SubstringMetric.java b/source/net/sourceforge/filebot/similarity/SubstringMetric.java index f31c3edd..9da24f8b 100644 --- a/source/net/sourceforge/filebot/similarity/SubstringMetric.java +++ b/source/net/sourceforge/filebot/similarity/SubstringMetric.java @@ -7,11 +7,14 @@ public class SubstringMetric implements SimilarityMetric { @Override public float getSimilarity(Object o1, Object o2) { String s1 = normalize(o1); - String s2 = normalize(o2); - String pri = s1.length() > s2.length() ? s1 : s2; - String sub = s1.length() > s2.length() ? s2 : s1; + if (s1 == null || s1.isEmpty()) + return 0; - return sub.length() > 0 && pri.contains(sub) ? 1 : 0; + String s2 = normalize(o2); + if (s2 == null || s2.isEmpty()) + return 0; + + return s1.contains(s2) || s2.contains(s1) ? 1 : 0; } diff --git a/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java b/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java index e9eff17e..cc9113af 100644 --- a/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java +++ b/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java @@ -86,26 +86,6 @@ public final class SubtitleUtilities { } - public static boolean isDerived(String subtitle, File video) { - return isDerived(subtitle, getName(video)); - } - - - public static boolean isDerived(String derivate, String base) { - if (derivate.equalsIgnoreCase(base)) - return true; - - while (getExtension(derivate) != null) { - derivate = getNameWithoutExtension(derivate); - - if (derivate.equalsIgnoreCase(base)) - return true; - } - - return false; - } - - public static SubtitleFormat getSubtitleFormat(File file) { for (SubtitleFormat it : SubtitleFormat.values()) { if (it.getFilter().accept(file)) diff --git a/source/net/sourceforge/filebot/ui/rename/FileNameFormatter.java b/source/net/sourceforge/filebot/ui/rename/FileNameFormatter.java index 7ee3d212..c78ac778 100644 --- a/source/net/sourceforge/filebot/ui/rename/FileNameFormatter.java +++ b/source/net/sourceforge/filebot/ui/rename/FileNameFormatter.java @@ -5,7 +5,7 @@ package net.sourceforge.filebot.ui.rename; import java.io.File; import net.sourceforge.filebot.similarity.Match; -import net.sourceforge.filebot.vfs.AbstractFile; +import net.sourceforge.filebot.vfs.FileInfo; import net.sourceforge.tuned.FileUtilities; @@ -21,7 +21,7 @@ class FileNameFormatter implements MatchFormatter { @Override public boolean canFormat(Match match) { - return match.getValue() instanceof File || match.getValue() instanceof AbstractFile; + return match.getValue() instanceof File || match.getValue() instanceof FileInfo; } @@ -38,9 +38,9 @@ class FileNameFormatter implements MatchFormatter { 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(); + if (match.getValue() instanceof FileInfo) { + FileInfo file = (FileInfo) match.getValue(); + return preserveExtension ? file.getName() : file.getPath(); } // cannot format value diff --git a/source/net/sourceforge/filebot/ui/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/rename/NamesListTransferablePolicy.java index 9bb4eb58..a3148ac6 100644 --- a/source/net/sourceforge/filebot/ui/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/rename/NamesListTransferablePolicy.java @@ -22,7 +22,7 @@ import net.sourceforge.filebot.hash.VerificationFileReader; import net.sourceforge.filebot.torrent.Torrent; import net.sourceforge.filebot.ui.transfer.ArrayTransferable; import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy; -import net.sourceforge.filebot.vfs.AbstractFile; +import net.sourceforge.filebot.vfs.SimpleFileInfo; import net.sourceforge.filebot.web.Episode; import net.sourceforge.tuned.FastFile; @@ -146,7 +146,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy { try { while (parser.hasNext()) { - values.add(new AbstractFile(parser.next().getKey().getName(), -1)); + values.add(new SimpleFileInfo(parser.next().getKey().getName(), -1)); } } finally { parser.close(); @@ -160,7 +160,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy { Torrent torrent = new Torrent(file); for (Torrent.Entry entry : torrent.getFiles()) { - values.add(new AbstractFile(entry.getName(), entry.getLength())); + values.add(new SimpleFileInfo(entry.getName(), entry.getLength())); } } } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java index 2ba99551..c5f0acb6 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java @@ -167,7 +167,7 @@ public class SubtitlePackage { if (archiveType == ArchiveType.UNDEFINED) { // cannot extract files from archive - return singletonList(new MemoryFile(subtitle.getName() + '.' + subtitle.getType(), data)); + return singletonList(new MemoryFile(subtitle.getPath(), data)); } // extract contents of the archive diff --git a/source/net/sourceforge/filebot/vfs/AbstractFile.java b/source/net/sourceforge/filebot/vfs/AbstractFile.java deleted file mode 100644 index 1c1d932e..00000000 --- a/source/net/sourceforge/filebot/vfs/AbstractFile.java +++ /dev/null @@ -1,32 +0,0 @@ - -package net.sourceforge.filebot.vfs; - - -public class AbstractFile { - - private final String name; - private final long length; - - - public AbstractFile(String name, long length) { - this.name = name; - this.length = length; - } - - - public String getName() { - return name; - } - - - public long getLength() { - return length; - } - - - @Override - public String toString() { - return getName(); - } - -} diff --git a/source/net/sourceforge/filebot/vfs/FileInfo.java b/source/net/sourceforge/filebot/vfs/FileInfo.java new file mode 100644 index 00000000..c917b5f2 --- /dev/null +++ b/source/net/sourceforge/filebot/vfs/FileInfo.java @@ -0,0 +1,18 @@ + +package net.sourceforge.filebot.vfs; + + +public interface FileInfo { + + public String getPath(); + + + public String getName(); + + + public String getType(); + + + public long getLength(); + +} diff --git a/source/net/sourceforge/filebot/vfs/SimpleFileInfo.java b/source/net/sourceforge/filebot/vfs/SimpleFileInfo.java new file mode 100644 index 00000000..c9d1efa7 --- /dev/null +++ b/source/net/sourceforge/filebot/vfs/SimpleFileInfo.java @@ -0,0 +1,66 @@ + +package net.sourceforge.filebot.vfs; + + +import static net.sourceforge.tuned.FileUtilities.*; + +import java.util.Arrays; + + +public class SimpleFileInfo implements FileInfo { + + private final String path; + private final long length; + + + public SimpleFileInfo(String path, long length) { + this.path = path; + this.length = length; + } + + + @Override + public String getPath() { + return path; + } + + + public String getName() { + return getNameWithoutExtension(path); + } + + + @Override + public String getType() { + return getExtension(path); + } + + + public long getLength() { + return length; + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof FileInfo) { + FileInfo other = (FileInfo) obj; + return other.getLength() == getLength() && other.getPath().equals(getPath()); + } + + return false; + } + + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] { getPath(), getLength() }); + } + + + @Override + public String toString() { + return getPath(); + } + +} diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java index b17a38b3..722bd6cc 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java @@ -87,6 +87,12 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { } + @Override + public String getPath() { + return getProperty(Property.SubFileName); + } + + @Override public String getName() { return FileUtilities.getNameWithoutExtension(getProperty(Property.SubFileName)); @@ -105,8 +111,9 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { } - public int getSize() { - return Integer.parseInt(getProperty(Property.SubSize)); + @Override + public long getLength() { + return Long.parseLong(getProperty(Property.SubSize)); } @@ -126,7 +133,7 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { InputStream stream = new GZIPInputStream(resource.openStream()); try { - ByteBufferOutputStream buffer = new ByteBufferOutputStream(getSize()); + ByteBufferOutputStream buffer = new ByteBufferOutputStream(getLength()); // read all buffer.transferFully(stream); diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java b/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java index 168ba2cc..8ed769c0 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java @@ -141,7 +141,7 @@ public class OpenSubtitlesXmlRpc { if (!matcher.find()) throw new IllegalArgumentException("Illegal title"); - String name = matcher.group(1).trim(); + String name = matcher.group(1).replaceAll("\"", "").trim(); int year = Integer.parseInt(matcher.group(2)); movies.add(new Movie(name, year, Integer.parseInt(imdbid))); diff --git a/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java index 93027d16..919d9060 100644 --- a/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java @@ -83,6 +83,12 @@ public class SublightSubtitleDescriptor implements SubtitleDescriptor { } + @Override + public long getLength() { + return subtitle.getSize(); + } + + @Override public ByteBuffer fetch() throws Exception { byte[] archive = source.getZipArchive(subtitle); @@ -94,7 +100,7 @@ public class SublightSubtitleDescriptor implements SubtitleDescriptor { // move to subtitle entry ZipEntry entry = stream.getNextEntry(); - ByteBufferOutputStream buffer = new ByteBufferOutputStream((int) entry.getSize()); + ByteBufferOutputStream buffer = new ByteBufferOutputStream(entry.getSize()); // read subtitle data buffer.transferFully(stream); @@ -107,6 +113,12 @@ public class SublightSubtitleDescriptor implements SubtitleDescriptor { } + @Override + public String getPath() { + return String.format("%s.%s", getName(), getType()); + } + + @Override public String toString() { return String.format("%s [%s]", getName(), getLanguageName()); diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java index 2aee84ad..245bb192 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.swing.Icon; @@ -60,12 +61,17 @@ public class SubsceneSubtitleClient implements SubtitleProvider { List nodes = selectNodes("id('filmSearch')/A", dom); List searchResults = new ArrayList(nodes.size()); + Pattern titleSuffixPattern = Pattern.compile("\\s-\\s([^-]+)[(](\\d{4})[)]$"); + for (Node node : nodes) { String title = getTextContent(node); String href = getAttribute("href", node); + // simplified name for easy matching + String shortName = titleSuffixPattern.matcher(title).replaceFirst(""); + try { - searchResults.add(new HyperLink(title, new URL("http", host, href))); + searchResults.add(new SubsceneSearchResult(shortName, title, new URL("http", host, href))); } catch (MalformedURLException e) { Logger.getLogger(getClass().getName()).log(Level.WARNING, "Invalid href: " + href, e); } @@ -184,4 +190,28 @@ public class SubsceneSubtitleClient implements SubtitleProvider { return ((HyperLink) searchResult).getURI(); } + + public static class SubsceneSearchResult extends HyperLink { + + private String shortName; + + + public SubsceneSearchResult(String shortName, String title, URL url) { + super(title, url); + this.shortName = shortName; + } + + + @Override + public String getName() { + return shortName; + } + + + @Override + public String toString() { + return super.getName(); + } + } + } diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java index 738557ad..21b636cd 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java @@ -85,6 +85,18 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor { } + @Override + public String getPath() { + return String.format("%s.%s", getName(), getType()); + } + + + @Override + public long getLength() { + return -1; + } + + @Override public String toString() { return String.format("%s [%s]", getName(), getLanguageName()); diff --git a/source/net/sourceforge/filebot/web/SubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubtitleDescriptor.java index 088f81ac..14f82013 100644 --- a/source/net/sourceforge/filebot/web/SubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SubtitleDescriptor.java @@ -4,8 +4,10 @@ package net.sourceforge.filebot.web; import java.nio.ByteBuffer; +import net.sourceforge.filebot.vfs.FileInfo; -public interface SubtitleDescriptor { + +public interface SubtitleDescriptor extends FileInfo { String getName(); diff --git a/source/net/sourceforge/tuned/ByteBufferOutputStream.java b/source/net/sourceforge/tuned/ByteBufferOutputStream.java index 2abdc9fa..f7d7f978 100644 --- a/source/net/sourceforge/tuned/ByteBufferOutputStream.java +++ b/source/net/sourceforge/tuned/ByteBufferOutputStream.java @@ -17,6 +17,11 @@ public class ByteBufferOutputStream extends OutputStream { private final float loadFactor; + public ByteBufferOutputStream(long initialCapacity) { + this((int) initialCapacity); + } + + public ByteBufferOutputStream(int initialCapacity) { this(initialCapacity, 1.0f); } diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index 349399e2..f6631880 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -223,6 +223,31 @@ public final class FileUtilities { } + public static boolean isDerived(String derivate, File prime) { + String base = getName(prime).trim().toLowerCase(); + derivate = derivate.trim().toLowerCase(); + return derivate.startsWith(base); + } + + + public static boolean isDerivedByExtension(String derivate, File prime) { + String base = getName(prime).trim().toLowerCase(); + derivate = derivate.trim().toLowerCase(); + + if (derivate.equals(base)) + return true; + + while (derivate.length() > base.length() && getExtension(derivate) != null) { + derivate = getNameWithoutExtension(derivate); + + if (derivate.equals(base)) + return true; + } + + return false; + } + + public static boolean containsOnly(Iterable files, FileFilter filter) { for (File file : files) { if (!filter.accept(file)) diff --git a/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java b/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java index 432e2834..18f11a38 100644 --- a/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java +++ b/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java @@ -12,6 +12,8 @@ import java.util.Map; import org.junit.BeforeClass; import org.junit.Test; +import net.sourceforge.filebot.web.SubsceneSubtitleClient.SubsceneSearchResult; + public class SubsceneSubtitleClientTest { @@ -28,8 +30,8 @@ public class SubsceneSubtitleClientTest { @BeforeClass public static void setUpBeforeClass() throws Exception { - twinpeaksSearchResult = new HyperLink("Twin Peaks - First Season (1990)", new URL("http://subscene.com/twin-peaks--first-season/subtitles-32482.aspx")); - lostSearchResult = new HyperLink("Lost - Fourth Season (2008)", new URL("http://subscene.com/Lost-Fourth-Season/subtitles-70963.aspx")); + twinpeaksSearchResult = new SubsceneSearchResult("Twin Peaks", "Twin Peaks - First Season (1990)", new URL("http://subscene.com/twin-peaks--first-season/subtitles-32482.aspx")); + lostSearchResult = new SubsceneSearchResult("Lost", "Lost - Fourth Season (2008)", new URL("http://subscene.com/Lost-Fourth-Season/subtitles-70963.aspx")); } @@ -40,22 +42,21 @@ public class SubsceneSubtitleClientTest { public void search() throws Exception { List results = subscene.search("twin peaks"); - HyperLink result = (HyperLink) results.get(1); - - assertEquals(twinpeaksSearchResult.getName(), result.getName()); + SubsceneSearchResult result = (SubsceneSearchResult) results.get(1); + assertEquals(twinpeaksSearchResult.toString(), result.toString()); assertEquals(twinpeaksSearchResult.getURL().toString(), result.getURL().toString()); + assertEquals(twinpeaksSearchResult.getName(), result.getName()); } @Test public void searchResultPageRedirect() throws Exception { List results = subscene.search("firefly"); - assertEquals(2, results.size()); - HyperLink result = (HyperLink) results.get(0); - - assertEquals("Firefly - The Complete Series (2002)", result.getName()); + SubsceneSearchResult result = (SubsceneSearchResult) results.get(0); + assertEquals("Firefly - The Complete Series (2002)", result.toString()); + assertEquals("Firefly", result.getName()); assertEquals("http://subscene.com/Firefly-The-Complete-Series/subtitles-20008.aspx", result.getURL().toString()); } @@ -63,11 +64,9 @@ public class SubsceneSubtitleClientTest { @Test public void getSubtitleListSearchResult() throws Exception { List subtitleList = subscene.getSubtitleList(twinpeaksSearchResult, "Italian"); - assertEquals(1, subtitleList.size()); SubtitleDescriptor subtitle = subtitleList.get(0); - assertEquals("Twin Peaks - First Season", subtitle.getName()); assertEquals("Italian", subtitle.getLanguageName()); assertEquals("zip", subtitle.getType()); @@ -104,7 +103,6 @@ public class SubsceneSubtitleClientTest { public void downloadSubtitleArchive() throws Exception { SearchResult selectedResult = subscene.search("firefly").get(0); SubtitleDescriptor subtitleDescriptor = subscene.getSubtitleList(selectedResult, "English").get(1); - assertEquals("Firefly - The Complete Series", subtitleDescriptor.getName()); ByteBuffer archive = subtitleDescriptor.fetch();