+ added support for fully automatic SubtitleDescriptor/File subtitle matching to CLI, i.e. match files against subtitle listings

This commit is contained in:
Reinhard Pointner 2011-11-24 17:27:39 +00:00
parent 0de615cd00
commit 8571962e61
23 changed files with 544 additions and 267 deletions

BIN
fw/script.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -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<File> renameSeries(Collection<File> files, String query, ExpressionFormat format, EpisodeListProvider db, Locale locale, boolean strict) throws Exception {
CLILogger.config(format("Rename episodes using [%s]", db.getName()));
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
Collection<String> 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<String> seriesNames = (query == null) ? detectQuery(mediaFiles, strict) : singleton(query);
// fetch episode data
Set<Episode> 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<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
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<Match<File, Episode>> matchEpisodes(Collection<File> files, Collection<Episode> episodes, SimilarityMetric[] sequence) throws Exception {
// always use strict fail-fast matcher
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
List<Match<File, Episode>> matches = matcher.match();
for (File failedMatch : matcher.remainingValues()) {
CLILogger.warning("No matching episode: " + failedMatch.getName());
}
return matches;
}
private Set<Episode> fetchEpisodeSet(final EpisodeListProvider db, final Collection<String> names, final Locale locale, final boolean strict) throws Exception {
List<Callable<List<Episode>>> tasks = new ArrayList<Callable<List<Episode>>>();
@ -302,132 +306,6 @@ public class CmdlineOperations implements CmdlineInterface {
}
@Override
public List<File> getSubtitles(Collection<File> 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<File> remainingVideos = new TreeSet<File>(filter(files, VIDEO_FILES));
List<File> downloadedSubtitles = new ArrayList<File>();
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<File, List<SubtitleDescriptor>> 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<SubtitleDescriptor> 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<File> renameAll(Map<File, File> renameMap) throws Exception {
// rename files
final List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
@ -484,20 +362,192 @@ public class CmdlineOperations implements CmdlineInterface {
}
private List<Match<File, Episode>> match(Collection<File> files, Collection<Episode> episodes, SimilarityMetric[] sequence) throws Exception {
// always use strict fail-fast matcher
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
List<Match<File, Episode>> matches = matcher.match();
@Override
public List<File> getSubtitles(Collection<File> 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<String> 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<File, Callable<File>> downloadQueue = new TreeMap<File, Callable<File>>();
for (final Entry<String, Map<File, SubtitleDescriptor>> source : collector.subtitlesBySource().entrySet()) {
for (final Entry<File, SubtitleDescriptor> descriptor : source.getValue().entrySet()) {
downloadQueue.put(descriptor.getKey(), new Callable<File>() {
@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<File> subtitleFiles = new ArrayList<File>();
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
for (Future<File> 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<? extends SearchResult> 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<File, SubtitleDescriptor> lookupSubtitleByHash(VideoHashSubtitleService service, Language language, Collection<File> videoFiles) throws Exception {
Map<File, SubtitleDescriptor> subtitleByVideo = new HashMap<File, SubtitleDescriptor>(videoFiles.size());
CLILogger.fine("Looking up subtitles by filehash via " + service.getName());
for (Entry<File, List<SubtitleDescriptor>> 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<File, SubtitleDescriptor> lookupSubtitleByFileName(SubtitleProvider service, Collection<String> querySet, Language language, Collection<File> videoFiles) throws Exception {
Map<File, SubtitleDescriptor> subtitleByVideo = new HashMap<File, SubtitleDescriptor>();
// search for and automatically select movie / show entry
Set<SearchResult> resultSet = new HashSet<SearchResult>();
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<SubtitleDescriptor> subtitles = new ArrayList<SubtitleDescriptor>();
for (SearchResult it : resultSet) {
List<SubtitleDescriptor> 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<File, SubtitleDescriptor> matcher = new Matcher<File, SubtitleDescriptor>(videoFiles, subtitles, false, EpisodeMetrics.defaultSequence(true));
SimilarityMetric sanity = new MetricCascade(StrictEpisodeMetrics.defaultSequence(true));
for (Match<File, SubtitleDescriptor> 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<String> detectQuery(Collection<File> mediaFiles, boolean strict) {
Collection<String> 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<SearchResult> findProbableMatches(String query, Iterable<? extends SearchResult> searchResults) {
// auto-select most probable search result
Map<String, SearchResult> probableMatches = new TreeMap<String, SearchResult>(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<? extends SearchResult> searchResults, boolean strict) {
Collection<SearchResult> 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<String, Map<File, SubtitleDescriptor>> collection = new HashMap<String, Map<File, SubtitleDescriptor>>();
private final Set<File> remainingVideos = new TreeSet<File>();
public SubtitleCollector(Collection<File> videoFiles) {
remainingVideos.addAll(videoFiles);
}
public void addAll(String source, Map<File, SubtitleDescriptor> subtitles) {
remainingVideos.removeAll(subtitles.keySet());
Map<File, SubtitleDescriptor> subtitlesBySource = collection.get(source);
if (subtitlesBySource == null) {
subtitlesBySource = new TreeMap<File, SubtitleDescriptor>();
collection.put(source, subtitlesBySource);
}
subtitlesBySource.putAll(subtitles);
}
public Map<String, Map<File, SubtitleDescriptor>> subtitlesBySource() {
return collection;
}
public Collection<File> 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;
}
}

View File

@ -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 };
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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 };
}
}
}

View File

@ -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;
}

View File

@ -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))

View File

@ -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

View File

@ -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()));
}
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -0,0 +1,18 @@
package net.sourceforge.filebot.vfs;
public interface FileInfo {
public String getPath();
public String getName();
public String getType();
public long getLength();
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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)));

View File

@ -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());

View File

@ -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<Node> nodes = selectNodes("id('filmSearch')/A", dom);
List<SearchResult> searchResults = new ArrayList<SearchResult>(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();
}
}
}

View File

@ -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());

View File

@ -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();

View File

@ -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);
}

View File

@ -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<File> files, FileFilter filter) {
for (File file : files) {
if (!filter.accept(file))

View File

@ -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<SearchResult> 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<SearchResult> 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<SubtitleDescriptor> 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();