+ fully-automatic subtitle matching even without hashes

This commit is contained in:
Reinhard Pointner 2011-11-25 18:52:31 +00:00
parent 116262fbea
commit 41c1bcce7b
14 changed files with 466 additions and 175 deletions

View File

@ -63,7 +63,7 @@ public class ArgumentProcessor {
Set<File> files = new LinkedHashSet<File>(args.getFiles(true)); Set<File> files = new LinkedHashSet<File>(args.getFiles(true));
if (args.getSubtitles) { if (args.getSubtitles) {
List<File> subtitles = cli.getSubtitles(files, args.query, args.lang, args.output, args.encoding); List<File> subtitles = cli.getSubtitles(files, args.query, args.lang, args.output, args.encoding, !args.nonStrict);
files.addAll(subtitles); files.addAll(subtitles);
} }

View File

@ -12,7 +12,7 @@ public interface CmdlineInterface {
List<File> rename(Collection<File> files, String query, String format, String db, String lang, boolean strict) throws Exception; List<File> rename(Collection<File> files, String query, String format, String db, String lang, boolean strict) throws Exception;
List<File> getSubtitles(Collection<File> files, String query, String lang, String output, String encoding) throws Exception; List<File> getSubtitles(Collection<File> files, String query, String lang, String output, String encoding, boolean strict) throws Exception;
boolean check(Collection<File> files) throws Exception; boolean check(Collection<File> files) throws Exception;

View File

@ -19,7 +19,6 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
@ -55,7 +54,6 @@ import net.sourceforge.filebot.similarity.StrictEpisodeMetrics;
import net.sourceforge.filebot.subtitle.SubtitleFormat; import net.sourceforge.filebot.subtitle.SubtitleFormat;
import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.Language;
import net.sourceforge.filebot.ui.rename.HistorySpooler; import net.sourceforge.filebot.ui.rename.HistorySpooler;
import net.sourceforge.filebot.vfs.ArchiveType;
import net.sourceforge.filebot.vfs.MemoryFile; import net.sourceforge.filebot.vfs.MemoryFile;
import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.filebot.web.EpisodeFormat;
@ -363,7 +361,7 @@ public class CmdlineOperations implements CmdlineInterface {
@Override @Override
public List<File> getSubtitles(Collection<File> files, String query, String languageName, String output, String csn) throws Exception { public List<File> getSubtitles(Collection<File> files, String query, String languageName, String output, String csn, boolean strict) throws Exception {
final Language language = getLanguage(languageName); final Language language = getLanguage(languageName);
// when rewriting subtitles to target format an encoding must be defined, default to UTF-8 // when rewriting subtitles to target format an encoding must be defined, default to UTF-8
@ -384,14 +382,15 @@ public class CmdlineOperations implements CmdlineInterface {
} }
try { try {
CLILogger.fine("Looking up subtitles by filehash via " + service.getName());
collector.addAll(service.getName(), lookupSubtitleByHash(service, language, collector.remainingVideos())); collector.addAll(service.getName(), lookupSubtitleByHash(service, language, collector.remainingVideos()));
} catch (RuntimeException e) { } catch (RuntimeException e) {
CLILogger.warning(format("Lookup by hash failed: " + e.getMessage())); CLILogger.warning(format("Lookup by hash failed: " + e.getMessage()));
} }
} }
// lookup subtitles via text search // lookup subtitles via text search, only perform hash lookup in strict mode
if (!collector.isComplete()) { if ((query != null || !strict) && !collector.isComplete()) {
// auto-detect search query // auto-detect search query
Collection<String> querySet = (query == null) ? detectQuery(filter(files, VIDEO_FILES), false) : singleton(query); Collection<String> querySet = (query == null) ? detectQuery(filter(files, VIDEO_FILES), false) : singleton(query);
@ -401,6 +400,7 @@ public class CmdlineOperations implements CmdlineInterface {
} }
try { try {
CLILogger.fine(format("Searching for %s at [%s]", querySet.toString(), service.getName()));
collector.addAll(service.getName(), lookupSubtitleByFileName(service, querySet, language, collector.remainingVideos())); collector.addAll(service.getName(), lookupSubtitleByFileName(service, querySet, language, collector.remainingVideos()));
} catch (RuntimeException e) { } catch (RuntimeException e) {
CLILogger.warning(format("Search for [%s] failed: %s", querySet, e.getMessage())); CLILogger.warning(format("Search for [%s] failed: %s", querySet, e.getMessage()));
@ -422,7 +422,7 @@ public class CmdlineOperations implements CmdlineInterface {
@Override @Override
public File call() throws Exception { public File call() throws Exception {
Analytics.trackEvent(source.getKey(), "DownloadSubtitle", descriptor.getValue().getLanguageName(), 1); Analytics.trackEvent(source.getKey(), "DownloadSubtitle", descriptor.getValue().getLanguageName(), 1);
return fetchSubtitle(descriptor.getValue(), descriptor.getKey(), outputFormat, outputEncoding); return downloadSubtitle(descriptor.getValue(), descriptor.getKey(), outputFormat, outputEncoding);
} }
}); });
} }
@ -430,14 +430,17 @@ public class CmdlineOperations implements CmdlineInterface {
// parallel download // parallel download
List<File> subtitleFiles = new ArrayList<File>(); List<File> subtitleFiles = new ArrayList<File>();
ExecutorService executor = Executors.newFixedThreadPool(4);
try { if (downloadQueue.size() > 0) {
for (Future<File> it : executor.invokeAll(downloadQueue.values())) { ExecutorService executor = Executors.newFixedThreadPool(4);
subtitleFiles.add(it.get());
try {
for (Future<File> it : executor.invokeAll(downloadQueue.values())) {
subtitleFiles.add(it.get());
}
} finally {
executor.shutdownNow();
} }
} finally {
executor.shutdownNow();
} }
Analytics.trackEvent("CLI", "Download", "Subtitle", subtitleFiles.size()); Analytics.trackEvent("CLI", "Download", "Subtitle", subtitleFiles.size());
@ -445,26 +448,13 @@ public class CmdlineOperations implements CmdlineInterface {
} }
private File fetchSubtitle(SubtitleDescriptor descriptor, File movieFile, SubtitleFormat outputFormat, Charset outputEncoding) throws Exception { private File downloadSubtitle(SubtitleDescriptor descriptor, File movieFile, SubtitleFormat outputFormat, Charset outputEncoding) throws Exception {
// fetch subtitle archive // fetch subtitle archive
CLILogger.info(format("Fetching [%s]", descriptor.getPath())); CLILogger.info(format("Fetching [%s]", descriptor.getPath()));
ByteBuffer downloadedData = descriptor.fetch(); MemoryFile subtitleFile = fetchSubtitle(descriptor);
// 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 // subtitle filename is based on movie filename
String name = getName(movieFile); String base = getName(movieFile);
String lang = Language.getISO3LanguageCodeByName(descriptor.getLanguageName());
String ext = getExtension(subtitleFile.getName()); String ext = getExtension(subtitleFile.getName());
ByteBuffer data = subtitleFile.getData(); ByteBuffer data = subtitleFile.getData();
@ -477,7 +467,7 @@ public class CmdlineOperations implements CmdlineInterface {
data = exportSubtitles(subtitleFile, outputFormat, 0, outputEncoding); data = exportSubtitles(subtitleFile, outputFormat, 0, outputEncoding);
} }
File destination = new File(movieFile.getParentFile(), String.format("%s.%s.%s", name, lang, ext)); File destination = new File(movieFile.getParentFile(), formatSubtitle(base, descriptor.getLanguageName(), ext));
CLILogger.config(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName())); CLILogger.config(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName()));
writeFile(data, destination); writeFile(data, destination);
@ -487,11 +477,10 @@ public class CmdlineOperations implements CmdlineInterface {
private Map<File, SubtitleDescriptor> lookupSubtitleByHash(VideoHashSubtitleService service, Language language, Collection<File> videoFiles) throws Exception { private Map<File, SubtitleDescriptor> lookupSubtitleByHash(VideoHashSubtitleService service, Language language, Collection<File> videoFiles) throws Exception {
Map<File, SubtitleDescriptor> subtitleByVideo = new HashMap<File, SubtitleDescriptor>(videoFiles.size()); 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()) { for (Entry<File, List<SubtitleDescriptor>> it : service.getSubtitleList(videoFiles.toArray(new File[0]), language.getName()).entrySet()) {
if (it.getValue() != null && it.getValue().size() > 0) { if (it.getValue() != null && it.getValue().size() > 0) {
CLILogger.finest(format("Matched [%s] to [%s]", it.getKey().getName(), it.getValue().get(0).getName())); CLILogger.finest(format("Matched [%s] to [%s] via filehash", it.getKey().getName(), it.getValue().get(0).getName()));
subtitleByVideo.put(it.getKey(), it.getValue().get(0)); subtitleByVideo.put(it.getKey(), it.getValue().get(0));
} }
} }
@ -503,30 +492,18 @@ public class CmdlineOperations implements CmdlineInterface {
private Map<File, SubtitleDescriptor> lookupSubtitleByFileName(SubtitleProvider service, Collection<String> querySet, Language language, Collection<File> videoFiles) throws Exception { 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>(); Map<File, SubtitleDescriptor> subtitleByVideo = new HashMap<File, SubtitleDescriptor>();
// search for and automatically select movie / show entry // search for subtitles
Set<SearchResult> resultSet = new HashSet<SearchResult>(); List<SubtitleDescriptor> subtitles = findSubtitles(service, querySet, language.getName());
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 // match subtitle files to video files
if (resultSet.size() > 0) { if (subtitles.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 // 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)); Matcher<File, SubtitleDescriptor> matcher = new Matcher<File, SubtitleDescriptor>(videoFiles, subtitles, false, EpisodeMetrics.defaultSequence(true));
SimilarityMetric sanity = new MetricCascade(StrictEpisodeMetrics.defaultSequence(true)); SimilarityMetric sanity = new MetricCascade(StrictEpisodeMetrics.defaultSequence(true));
for (Match<File, SubtitleDescriptor> it : matcher.match()) { for (Match<File, SubtitleDescriptor> it : matcher.match()) {
if (sanity.getSimilarity(it.getValue(), it.getCandidate()) >= 1) { if (sanity.getSimilarity(it.getValue(), it.getCandidate()) >= 1) {
CLILogger.finest(format("Matched [%s] to [%s]", it.getValue().getName(), it.getCandidate().getName())); CLILogger.finest(format("Matched [%s] to [%s] via filename", it.getValue().getName(), it.getCandidate().getName()));
subtitleByVideo.put(it.getValue(), it.getCandidate()); subtitleByVideo.put(it.getValue(), it.getCandidate());
} }
} }

View File

@ -32,7 +32,7 @@ def rename(args) { args = _defaults(args)
} }
def getSubtitles(args) { args = _defaults(args) def getSubtitles(args) { args = _defaults(args)
_guarded { _cli.getSubtitles(_files(args), args.query, args.lang, args.output, args.encoding) } _guarded { _cli.getSubtitles(_files(args), args.query, args.lang, args.output, args.encoding, args.strict) }
} }
def check(args) { def check(args) {

View File

@ -3,6 +3,7 @@ package net.sourceforge.filebot.subtitle;
import static java.lang.Math.*; import static java.lang.Math.*;
import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*;
import java.io.File; import java.io.File;
@ -12,14 +13,62 @@ import java.nio.ByteBuffer;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set;
import net.sourceforge.filebot.similarity.NameSimilarityMetric;
import net.sourceforge.filebot.similarity.SimilarityMetric;
import net.sourceforge.filebot.ui.Language;
import net.sourceforge.filebot.vfs.ArchiveType;
import net.sourceforge.filebot.vfs.MemoryFile; import net.sourceforge.filebot.vfs.MemoryFile;
import net.sourceforge.filebot.web.SearchResult;
import net.sourceforge.filebot.web.SubtitleDescriptor;
import net.sourceforge.filebot.web.SubtitleProvider;
public final class SubtitleUtilities { public final class SubtitleUtilities {
public static List<SubtitleDescriptor> findSubtitles(SubtitleProvider service, Collection<String> querySet, String languageName) throws Exception {
List<SubtitleDescriptor> subtitles = new ArrayList<SubtitleDescriptor>();
// search for and automatically select movie / show entry
Set<SearchResult> resultSet = new HashSet<SearchResult>();
for (String query : querySet) {
resultSet.addAll(findProbableMatches(query, service.search(query), 0.9f));
}
// fetch subtitles for all search results
for (SearchResult it : resultSet) {
subtitles.addAll(service.getSubtitleList(it, languageName));
}
return subtitles;
}
protected static Collection<SearchResult> findProbableMatches(String query, Iterable<? extends SearchResult> searchResults, float threshold) {
// auto-select most probable search result
Set<SearchResult> probableMatches = new LinkedHashSet<SearchResult>();
// use name similarity metric
SimilarityMetric metric = new NameSimilarityMetric();
// find probable matches using name similarity > threshold
for (SearchResult result : searchResults) {
if (metric.getSimilarity(query, result.getName()) > threshold) {
probableMatches.add(result);
}
}
return probableMatches;
}
/** /**
* Detect charset and parse subtitle file even if extension is invalid * Detect charset and parse subtitle file even if extension is invalid
*/ */
@ -111,6 +160,50 @@ public final class SubtitleUtilities {
} }
public static String formatSubtitle(String name, String languageName, String type) {
StringBuilder sb = new StringBuilder(name);
if (languageName != null) {
String lang = Language.getISO3LanguageCodeByName(languageName);
if (lang == null) {
// we probably won't get here, but just in case
lang = languageName.replaceAll("\\W", "");
}
sb.append('.').append(lang);
}
if (type != null) {
sb.append('.').append(type);
}
return sb.toString();
}
public static MemoryFile fetchSubtitle(SubtitleDescriptor descriptor) throws Exception {
ByteBuffer data = descriptor.fetch();
// extract subtitles from archive
ArchiveType type = ArchiveType.forName(descriptor.getType());
if (type != ArchiveType.UNKOWN) {
// extract subtitle from archive
Iterator<MemoryFile> it = type.fromData(data).iterator();
while (it.hasNext()) {
MemoryFile entry = it.next();
if (SUBTITLE_FILES.accept(entry.getName())) {
return entry;
}
}
}
// assume that the fetched data is the subtitle
return new MemoryFile(descriptor.getPath(), data);
}
/** /**
* Dummy constructor to prevent instantiation. * Dummy constructor to prevent instantiation.
*/ */

View File

@ -103,8 +103,7 @@ public class Language {
} }
} }
// we won't get here, but just in case return null;
return languageName.replaceAll("\\W", "");
} }

View File

@ -32,8 +32,8 @@ import javax.swing.JDialog;
import javax.swing.JFileChooser; import javax.swing.JFileChooser;
import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.filechooser.FileNameExtensionFilter;
import net.sourceforge.filebot.Analytics;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.web.SubtitleProvider;
import net.sourceforge.filebot.web.VideoHashSubtitleService; import net.sourceforge.filebot.web.VideoHashSubtitleService;
@ -86,7 +86,10 @@ abstract class SubtitleDropTarget extends JButton {
} }
public abstract VideoHashSubtitleService[] getServices(); public abstract VideoHashSubtitleService[] getVideoHashSubtitleServices();
public abstract SubtitleProvider[] getSubtitleProviders();
public abstract String getQueryLanguage(); public abstract String getQueryLanguage();
@ -98,7 +101,11 @@ abstract class SubtitleDropTarget extends JButton {
// initialize download parameters // initialize download parameters
dialog.setVideoFiles(videoFiles.toArray(new File[0])); dialog.setVideoFiles(videoFiles.toArray(new File[0]));
for (VideoHashSubtitleService service : getServices()) { for (VideoHashSubtitleService service : getVideoHashSubtitleServices()) {
dialog.addSubtitleService(service);
}
for (SubtitleProvider service : getSubtitleProviders()) {
dialog.addSubtitleService(service); dialog.addSubtitleService(service);
} }
@ -108,14 +115,12 @@ abstract class SubtitleDropTarget extends JButton {
// initialize window properties // initialize window properties
dialog.setIconImage(getImage(getIcon(DropAction.Download))); dialog.setIconImage(getImage(getIcon(DropAction.Download)));
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.pack(); dialog.setSize(670, 575);
// show dialog // show dialog
dialog.setLocation(getOffsetLocation(dialog.getOwner())); dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setVisible(true); dialog.setVisible(true);
// now it's up to the user
Analytics.trackEvent("GUI", "LookupSubtitleByHash", getQueryLanguage(), videoFiles.size());
return true; return true;
} }

View File

@ -165,7 +165,7 @@ public class SubtitlePackage {
ArchiveType archiveType = ArchiveType.forName(subtitle.getType()); ArchiveType archiveType = ArchiveType.forName(subtitle.getType());
if (archiveType == ArchiveType.UNDEFINED) { if (archiveType == ArchiveType.UNKOWN) {
// cannot extract files from archive // cannot extract files from archive
return singletonList(new MemoryFile(subtitle.getPath(), data)); return singletonList(new MemoryFile(subtitle.getPath(), data));
} }
@ -198,7 +198,7 @@ public class SubtitlePackage {
// check if file is a supported archive // check if file is a supported archive
ArchiveType type = ArchiveType.forName(FileUtilities.getExtension(file.getName())); ArchiveType type = ArchiveType.forName(FileUtilities.getExtension(file.getName()));
if (type != ArchiveType.UNDEFINED) { if (type != ArchiveType.UNKOWN) {
// extract nested archives recursively // extract nested archives recursively
vfs.addAll(extract(type, file.getData())); vfs.addAll(extract(type, file.getData()));
} }

View File

@ -16,7 +16,6 @@ import java.util.List;
import javax.swing.Icon; import javax.swing.Icon;
import net.sourceforge.filebot.Analytics;
import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.WebServices;
import net.sourceforge.filebot.ui.AbstractSearchPanel; import net.sourceforge.filebot.ui.AbstractSearchPanel;
@ -51,11 +50,17 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
private final SubtitleDropTarget dropTarget = new SubtitleDropTarget() { private final SubtitleDropTarget dropTarget = new SubtitleDropTarget() {
@Override @Override
public VideoHashSubtitleService[] getServices() { public VideoHashSubtitleService[] getVideoHashSubtitleServices() {
return WebServices.getVideoHashSubtitleServices(); return WebServices.getVideoHashSubtitleServices();
} }
@Override
public SubtitleProvider[] getSubtitleProviders() {
return WebServices.getSubtitleProviders();
}
@Override @Override
public String getQueryLanguage() { public String getQueryLanguage() {
// use currently selected language for drop target // use currently selected language for drop target
@ -163,7 +168,6 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
packages.add(new SubtitlePackage(request.getProvider(), subtitle)); packages.add(new SubtitlePackage(request.getProvider(), subtitle));
} }
Analytics.trackEvent("GUI", "LookupSubtitleByName", request.getLanguageName(), 1);
return packages; return packages;
} }

View File

@ -4,6 +4,8 @@ package net.sourceforge.filebot.ui.subtitle;
import static javax.swing.BorderFactory.*; import static javax.swing.BorderFactory.*;
import static javax.swing.JOptionPane.*; import static javax.swing.JOptionPane.*;
import static net.sourceforge.filebot.similarity.EpisodeMetrics.*;
import static net.sourceforge.filebot.subtitle.SubtitleUtilities.*;
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*;
import java.awt.Color; import java.awt.Color;
@ -15,7 +17,6 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer;
import java.util.AbstractList; import java.util.AbstractList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -24,7 +25,9 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeSet;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
@ -54,8 +57,17 @@ import javax.swing.table.DefaultTableCellRenderer;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.Analytics;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
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.SeriesNameMatcher;
import net.sourceforge.filebot.similarity.SimilarityMetric;
import net.sourceforge.filebot.similarity.StrictEpisodeMetrics;
import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.Language;
import net.sourceforge.filebot.vfs.MemoryFile;
import net.sourceforge.filebot.web.SubtitleDescriptor; import net.sourceforge.filebot.web.SubtitleDescriptor;
import net.sourceforge.filebot.web.SubtitleProvider;
import net.sourceforge.filebot.web.VideoHashSubtitleService; import net.sourceforge.filebot.web.VideoHashSubtitleService;
import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.ui.AbstractBean; import net.sourceforge.tuned.ui.AbstractBean;
@ -66,8 +78,9 @@ import net.sourceforge.tuned.ui.RoundBorder;
class VideoHashSubtitleDownloadDialog extends JDialog { class VideoHashSubtitleDownloadDialog extends JDialog {
private final JPanel servicePanel = new JPanel(new MigLayout()); private final JPanel hashMatcherServicePanel = createServicePanel(0xFAFAD2); // LightGoldenRodYellow
private final List<VideoHashSubtitleServiceBean> services = new ArrayList<VideoHashSubtitleServiceBean>(); private final JPanel nameMatcherServicePanel = createServicePanel(0xFFEBCD); // BlanchedAlmond
private final List<SubtitleServiceBean> services = new ArrayList<SubtitleServiceBean>();
private final JTable subtitleMappingTable = createTable(); private final JTable subtitleMappingTable = createTable();
@ -81,18 +94,25 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
JComponent content = (JComponent) getContentPane(); JComponent content = (JComponent) getContentPane();
content.setLayout(new MigLayout("fill, insets dialog, nogrid", "", "[fill][pref!]")); content.setLayout(new MigLayout("fill, insets dialog, nogrid", "", "[fill][pref!]"));
servicePanel.setBorder(new RoundBorder());
servicePanel.setOpaque(false);
servicePanel.setBackground(new Color(0xFAFAD2)); // LightGoldenRodYellow
content.add(new JScrollPane(subtitleMappingTable), "grow, wrap"); content.add(new JScrollPane(subtitleMappingTable), "grow, wrap");
content.add(servicePanel, "gap after indent*2"); content.add(hashMatcherServicePanel, "gap after rel");
content.add(nameMatcherServicePanel, "gap after indent*2");
content.add(new JButton(downloadAction), "tag ok"); content.add(new JButton(downloadAction), "tag ok");
content.add(new JButton(finishAction), "tag cancel"); content.add(new JButton(finishAction), "tag cancel");
} }
protected JPanel createServicePanel(int color) {
JPanel panel = new JPanel(new MigLayout("hidemode 3"));
panel.setBorder(new RoundBorder());
panel.setOpaque(false);
panel.setBackground(new Color(color));
panel.setVisible(false);
return panel;
}
protected JTable createTable() { protected JTable createTable() {
JTable table = new JTable(new SubtitleMappingTableModel()); JTable table = new JTable(new SubtitleMappingTableModel());
table.setDefaultRenderer(SubtitleMapping.class, new SubtitleMappingOptionRenderer()); table.setDefaultRenderer(SubtitleMapping.class, new SubtitleMappingOptionRenderer());
@ -134,25 +154,37 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
} }
public void addSubtitleService(final VideoHashSubtitleService service) { public void addSubtitleService(VideoHashSubtitleService service) {
final VideoHashSubtitleServiceBean serviceBean = new VideoHashSubtitleServiceBean(service); addSubtitleService(new VideoHashSubtitleServiceBean(service), hashMatcherServicePanel);
final LinkButton component = new LinkButton(serviceBean.getName(), ResourceManager.getIcon("database.go"), serviceBean.getLink()); }
public void addSubtitleService(SubtitleProvider service) {
addSubtitleService(new SubtitleProviderBean(service), nameMatcherServicePanel);
}
protected void addSubtitleService(final SubtitleServiceBean service, final JPanel servicePanel) {
final LinkButton component = new LinkButton(service.getName(), ResourceManager.getIcon("database"), service.getLink());
component.setVisible(false);
serviceBean.addPropertyChangeListener(new PropertyChangeListener() { service.addPropertyChangeListener(new PropertyChangeListener() {
@Override @Override
public void propertyChange(PropertyChangeEvent evt) { public void propertyChange(PropertyChangeEvent evt) {
if (serviceBean.getState() == StateValue.STARTED) { if (service.getState() == StateValue.STARTED) {
component.setIcon(ResourceManager.getIcon("database.go")); component.setIcon(ResourceManager.getIcon("database.go"));
} else { } else {
component.setIcon(ResourceManager.getIcon(serviceBean.getError() == null ? "database.ok" : "database.error")); component.setIcon(ResourceManager.getIcon(service.getError() == null ? "database.ok" : "database.error"));
} }
component.setToolTipText(serviceBean.getError() == null ? null : serviceBean.getError().getMessage()); servicePanel.setVisible(true);
component.setVisible(true);
component.setToolTipText(service.getError() == null ? null : service.getError().getMessage());
} }
}); });
services.add(serviceBean); services.add(service);
servicePanel.add(component); servicePanel.add(component);
} }
@ -160,38 +192,30 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
public void startQuery(String languageName) { public void startQuery(String languageName) {
final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel();
// query services sequentially QueryTask queryTask = new QueryTask(services, mappingModel.getVideoFiles(), languageName) {
queryService = Executors.newFixedThreadPool(1);
@Override
for (final VideoHashSubtitleServiceBean service : services) { protected void process(List<Map<File, List<SubtitleDescriptorBean>>> sequence) {
QueryTask task = new QueryTask(service, mappingModel.getVideoFiles(), languageName) { for (Map<File, List<SubtitleDescriptorBean>> subtitles : sequence) {
// update subtitle options
@Override for (SubtitleMapping subtitleMapping : mappingModel) {
protected void done() { List<SubtitleDescriptorBean> options = subtitles.get(subtitleMapping.getVideoFile());
try {
Map<File, List<SubtitleDescriptorBean>> subtitles = get();
// update subtitle options if (options != null && options.size() > 0) {
for (SubtitleMapping subtitleMapping : mappingModel) { subtitleMapping.addOptions(options);
List<SubtitleDescriptorBean> options = subtitles.get(subtitleMapping.getVideoFile());
if (options != null && options.size() > 0) {
subtitleMapping.addOptions(options);
}
} }
}
// make subtitle column visible
Analytics.trackEvent(service.getName(), "HashLookup", "Subtitle", subtitles.size()); // number of positive hash lookups // make subtitle column visible
if (subtitles.size() > 0) {
mappingModel.setOptionColumnVisible(true); mappingModel.setOptionColumnVisible(true);
} catch (Exception e) {
Logger.getLogger(VideoHashSubtitleDownloadDialog.class.getName()).log(Level.WARNING, e.getMessage());
} }
} }
}; }
};
// start background worker
queryService.submit(task); ExecutorService executor = Executors.newFixedThreadPool(1);
} executor.submit(queryTask);
} }
@ -244,11 +268,21 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
// collect the subtitles that will be fetched // collect the subtitles that will be fetched
List<DownloadTask> downloadQueue = new ArrayList<DownloadTask>(); List<DownloadTask> downloadQueue = new ArrayList<DownloadTask>();
for (SubtitleMapping mapping : mappingModel) { for (final SubtitleMapping mapping : mappingModel) {
SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption();
if (subtitleBean != null && subtitleBean.getState() == null) { if (subtitleBean != null && subtitleBean.getState() == null) {
downloadQueue.add(new DownloadTask(subtitleBean, mapping.getSubtitleFile())); downloadQueue.add(new DownloadTask(mapping.getVideoFile(), subtitleBean) {
@Override
protected void done() {
try {
mapping.setSubtitleFile(get());
} catch (Exception e) {
Logger.getLogger(VideoHashSubtitleDownloadDialog.class.getName()).log(Level.WARNING, e.getMessage());
}
}
});
} }
} }
@ -257,9 +291,12 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
List<String> existingFiles = new ArrayList<String>(); List<String> existingFiles = new ArrayList<String>();
for (DownloadTask download : downloadQueue) { for (DownloadTask download : downloadQueue) {
if (download.getDestination().exists()) { // target destination may not be known until files are extracted from archives
File target = download.getDestination(null);
if (target != null && target.exists()) {
confirmReplaceDownloadQueue.add(download); confirmReplaceDownloadQueue.add(download);
existingFiles.add(download.getDestination().getName()); existingFiles.add(target.getName());
} }
} }
@ -349,10 +386,13 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
// download in progress // download in progress
setText(subtitleBean.getText()); setText(subtitleBean.getText());
setIcon(ResourceManager.getIcon("action.fetch")); setIcon(ResourceManager.getIcon("action.fetch"));
} else { } else if (mapping.getSubtitleFile() != null) {
// download complete // download complete
setText(mapping.getSubtitleFile().getName()); setText(mapping.getSubtitleFile().getName());
setIcon(ResourceManager.getIcon("status.ok")); setIcon(ResourceManager.getIcon("status.ok"));
} else {
setText(null);
setIcon(null);
} }
return this; return this;
@ -514,7 +554,8 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
private static class SubtitleMapping extends AbstractBean { private static class SubtitleMapping extends AbstractBean {
private final File videoFile; private File videoFile;
private File subtitleFile;
private SubtitleDescriptorBean selectedOption; private SubtitleDescriptorBean selectedOption;
private List<SubtitleDescriptorBean> options = new ArrayList<SubtitleDescriptorBean>(); private List<SubtitleDescriptorBean> options = new ArrayList<SubtitleDescriptorBean>();
@ -531,18 +572,18 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
public File getSubtitleFile() { public File getSubtitleFile() {
if (selectedOption == null) return subtitleFile;
throw new IllegalStateException("Selected option must not be null"); }
String base = FileUtilities.getName(videoFile);
String subtitleName = String.format("%s.%s.%s", base, selectedOption.getLanguage(), selectedOption.getType()); public void setSubtitleFile(File subtitleFile) {
this.subtitleFile = subtitleFile;
return new File(videoFile.getParentFile(), subtitleName); firePropertyChange("subtitleFile", null, this.subtitleFile);
} }
public boolean isEditable() { public boolean isEditable() {
return selectedOption != null && (selectedOption.getState() == null || selectedOption.getError() != null); return subtitleFile == null && selectedOption != null && (selectedOption.getState() == null || selectedOption.getError() != null);
} }
@ -589,21 +630,21 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
private static class SubtitleDescriptorBean extends AbstractBean { private static class SubtitleDescriptorBean extends AbstractBean {
private final SubtitleDescriptor subtitle; private final SubtitleDescriptor descriptor;
private final VideoHashSubtitleServiceBean service; private final SubtitleServiceBean service;
private StateValue state; private StateValue state;
private Exception error; private Exception error;
public SubtitleDescriptorBean(SubtitleDescriptor subtitle, VideoHashSubtitleServiceBean source) { public SubtitleDescriptorBean(SubtitleDescriptor descriptor, SubtitleServiceBean service) {
this.subtitle = subtitle; this.descriptor = descriptor;
this.service = source; this.service = service;
} }
public String getText() { public String getText() {
return String.format("%s.%s.%s", subtitle.getName(), getLanguage(), getType()); return formatSubtitle(descriptor.getName(), getLanguage(), getType());
} }
@ -613,21 +654,21 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
public String getLanguage() { public String getLanguage() {
return Language.getISO3LanguageCodeByName(subtitle.getLanguageName()); return Language.getISO3LanguageCodeByName(descriptor.getLanguageName());
} }
public String getType() { public String getType() {
return subtitle.getType(); return descriptor.getType();
} }
public ByteBuffer fetch() throws Exception { public MemoryFile fetch() throws Exception {
setState(StateValue.STARTED); setState(StateValue.STARTED);
try { try {
ByteBuffer data = subtitle.fetch(); MemoryFile data = fetchSubtitle(descriptor);
Analytics.trackEvent(service.getName(), "DownloadSubtitle", subtitle.getLanguageName(), 1); Analytics.trackEvent(service.getName(), "DownloadSubtitle", descriptor.getLanguageName(), 1);
return data; return data;
} catch (Exception e) { } catch (Exception e) {
@ -665,60 +706,83 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
} }
private static class QueryTask extends SwingWorker<Map<File, List<SubtitleDescriptorBean>>, Void> { private static class QueryTask extends SwingWorker<Collection<File>, Map<File, List<SubtitleDescriptorBean>>> {
private final VideoHashSubtitleServiceBean service; private final Collection<SubtitleServiceBean> services;
private final File[] videoFiles; private final Collection<File> remainingVideos;
private final String languageName; private final String languageName;
public QueryTask(VideoHashSubtitleServiceBean service, Collection<File> videoFiles, String languageName) { public QueryTask(Collection<SubtitleServiceBean> services, Collection<File> videoFiles, String languageName) {
this.service = service; this.services = services;
this.videoFiles = videoFiles.toArray(new File[0]); this.remainingVideos = new TreeSet<File>(videoFiles);
this.languageName = languageName; this.languageName = languageName;
} }
@Override @Override
protected Map<File, List<SubtitleDescriptorBean>> doInBackground() throws Exception { protected Collection<File> doInBackground() throws Exception {
Map<File, List<SubtitleDescriptorBean>> subtitleSet = new HashMap<File, List<SubtitleDescriptorBean>>(); for (SubtitleServiceBean service : services) {
try {
for (final Entry<File, List<SubtitleDescriptor>> result : service.getSubtitleList(videoFiles, languageName).entrySet()) { if (isCancelled())
List<SubtitleDescriptorBean> subtitles = new ArrayList<SubtitleDescriptorBean>(); throw new CancellationException();
// associate subtitles with services Map<File, List<SubtitleDescriptorBean>> subtitleSet = new HashMap<File, List<SubtitleDescriptorBean>>();
for (SubtitleDescriptor subtitleDescriptor : result.getValue()) {
subtitles.add(new SubtitleDescriptorBean(subtitleDescriptor, service)); for (final Entry<File, List<SubtitleDescriptor>> result : service.lookupSubtitles(remainingVideos, languageName).entrySet()) {
List<SubtitleDescriptorBean> subtitles = new ArrayList<SubtitleDescriptorBean>();
// associate subtitles with services
for (SubtitleDescriptor subtitleDescriptor : result.getValue()) {
subtitles.add(new SubtitleDescriptorBean(subtitleDescriptor, service));
}
subtitleSet.put(result.getKey(), subtitles);
}
// only lookup subtitles for remaining videos
for (Entry<File, List<SubtitleDescriptorBean>> it : subtitleSet.entrySet()) {
if (it.getValue() != null && it.getValue().size() > 0) {
remainingVideos.remove(it.getKey());
}
}
publish(subtitleSet);
} catch (Exception e) {
Logger.getLogger(VideoHashSubtitleDownloadDialog.class.getName()).log(Level.SEVERE, e.getMessage(), e);
} }
subtitleSet.put(result.getKey(), subtitles);
} }
return subtitleSet; return remainingVideos;
} }
} }
private static class DownloadTask extends SwingWorker<File, Void> { private static class DownloadTask extends SwingWorker<File, Void> {
private final SubtitleDescriptorBean subtitle; private final File video;
private final File destination; private final SubtitleDescriptorBean descriptor;
public DownloadTask(SubtitleDescriptorBean subtitle, File destination) { public DownloadTask(File video, SubtitleDescriptorBean descriptor) {
this.subtitle = subtitle; this.video = video;
this.destination = destination; this.descriptor = descriptor;
} }
public SubtitleDescriptorBean getSubtitleBean() { public SubtitleDescriptorBean getSubtitleBean() {
return subtitle; return descriptor;
} }
public File getDestination() { public File getDestination(MemoryFile subtitle) {
return destination; if (descriptor.getType() == null && subtitle == null)
return null;
String base = FileUtilities.getName(video);
String ext = descriptor.getType() != null ? descriptor.getType() : getExtension(subtitle.getName());
return new File(video.getParentFile(), formatSubtitle(base, descriptor.getLanguage(), ext));
} }
@ -726,13 +790,14 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
protected File doInBackground() { protected File doInBackground() {
try { try {
// fetch subtitle // fetch subtitle
ByteBuffer data = subtitle.fetch(); MemoryFile subtitle = descriptor.fetch();
if (isCancelled()) if (isCancelled())
return null; return null;
// save to file // save to file
writeFile(data, destination); File destination = getDestination(subtitle);
writeFile(subtitle.getData(), destination);
return destination; return destination;
} catch (Exception e) { } catch (Exception e) {
@ -744,40 +809,46 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
} }
private static class VideoHashSubtitleServiceBean extends AbstractBean { protected static abstract class SubtitleServiceBean extends AbstractBean {
private final VideoHashSubtitleService service; private final String name;
private final Icon icon;
private final URI link;
private StateValue state; private StateValue state = StateValue.PENDING;
private Throwable error; private Throwable error = null;
public VideoHashSubtitleServiceBean(VideoHashSubtitleService service) { public SubtitleServiceBean(String name, Icon icon, URI link) {
this.service = service; this.name = name;
this.state = StateValue.PENDING; this.icon = icon;
this.link = link;
} }
public String getName() { public String getName() {
return service.getName(); return name;
} }
public Icon getIcon() { public Icon getIcon() {
return service.getIcon(); return icon;
} }
public URI getLink() { public URI getLink() {
return service.getLink(); return link;
} }
public Map<File, List<SubtitleDescriptor>> getSubtitleList(File[] files, String languageName) throws Exception { protected abstract Map<File, List<SubtitleDescriptor>> getSubtitleList(Collection<File> files, String languageName) throws Exception;
public final Map<File, List<SubtitleDescriptor>> lookupSubtitles(Collection<File> files, String languageName) throws Exception {
setState(StateValue.STARTED); setState(StateValue.STARTED);
try { try {
return service.getSubtitleList(files, languageName); return getSubtitleList(files, languageName);
} catch (Exception e) { } catch (Exception e) {
// remember error // remember error
error = e; error = e;
@ -804,7 +875,74 @@ class VideoHashSubtitleDownloadDialog extends JDialog {
public Throwable getError() { public Throwable getError() {
return error; return error;
} }
}
protected static class VideoHashSubtitleServiceBean extends SubtitleServiceBean {
private VideoHashSubtitleService service;
public VideoHashSubtitleServiceBean(VideoHashSubtitleService service) {
super(service.getName(), service.getIcon(), service.getLink());
this.service = service;
}
@Override
protected Map<File, List<SubtitleDescriptor>> getSubtitleList(Collection<File> files, String languageName) throws Exception {
return service.getSubtitleList(files.toArray(new File[0]), languageName);
}
}
protected static class SubtitleProviderBean extends SubtitleServiceBean {
private SubtitleProvider service;
public SubtitleProviderBean(SubtitleProvider service) {
super(service.getName(), service.getIcon(), service.getLink());
this.service = service;
}
@Override
protected Map<File, List<SubtitleDescriptor>> getSubtitleList(Collection<File> files, String languageName) throws Exception {
Map<File, List<SubtitleDescriptor>> subtitlesByFile = new HashMap<File, List<SubtitleDescriptor>>();
for (File file : files) {
subtitlesByFile.put(file, new ArrayList<SubtitleDescriptor>());
}
// auto-detect query and search for subtitles
Collection<String> querySet = new SeriesNameMatcher().matchAll(files.toArray(new File[0]));
List<SubtitleDescriptor> subtitles = findSubtitles(service, querySet, languageName);
// first match everything as best as possible, then filter possibly bad matches
Matcher<File, SubtitleDescriptor> matcher = new Matcher<File, SubtitleDescriptor>(files, 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) {
subtitlesByFile.get(it.getValue()).add(it.getCandidate());
}
}
// add other possible matches to the options
SimilarityMetric matchMetric = new MetricCascade(FileName, EpisodeIdentifier, Title, Name);
float matchSimilarity = 0.6f;
for (File file : files) {
// add matching subtitles
for (SubtitleDescriptor it : subtitles) {
if (matchMetric.getSimilarity(file, it) >= matchSimilarity && !subtitlesByFile.get(file).contains(it)) {
subtitlesByFile.get(file).add(it);
}
}
}
return subtitlesByFile;
}
} }
} }

View File

@ -4,6 +4,7 @@ package net.sourceforge.filebot.vfs;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
public enum ArchiveType { public enum ArchiveType {
@ -26,6 +27,26 @@ public enum ArchiveType {
UNDEFINED { UNDEFINED {
@Override
public Iterable<MemoryFile> fromData(ByteBuffer data) {
for (ArchiveType type : EnumSet.of(ZIP, RAR)) {
try {
Iterable<MemoryFile> files = type.fromData(data);
if (files.iterator().hasNext()) {
return files;
}
} catch (Exception e) {
// ignore
}
}
// cannot extract data, return empty archive
return Collections.emptySet();
}
},
UNKOWN {
@Override @Override
public Iterable<MemoryFile> fromData(ByteBuffer data) { public Iterable<MemoryFile> fromData(ByteBuffer data) {
// cannot extract data, return empty archive // cannot extract data, return empty archive
@ -34,13 +55,16 @@ public enum ArchiveType {
}; };
public static ArchiveType forName(String name) { public static ArchiveType forName(String name) {
if (name == null)
return UNDEFINED;
if ("zip".equalsIgnoreCase(name)) if ("zip".equalsIgnoreCase(name))
return ZIP; return ZIP;
if ("rar".equalsIgnoreCase(name)) if ("rar".equalsIgnoreCase(name))
return RAR; return RAR;
return UNDEFINED; return UNKOWN;
} }

View File

@ -145,6 +145,23 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor {
} }
@Override
public int hashCode() {
return getProperty(Property.IDSubtitle).hashCode();
}
@Override
public boolean equals(Object object) {
if (object instanceof OpenSubtitlesSubtitleDescriptor) {
OpenSubtitlesSubtitleDescriptor other = (OpenSubtitlesSubtitleDescriptor) object;
return getProperty(Property.IDSubtitle).equals(other.getProperty(Property.IDSubtitle));
}
return false;
}
@Override @Override
public String toString() { public String toString() {
return String.format("%s [%s]", getName(), getLanguageName()); return String.format("%s [%s]", getName(), getLanguageName());

View File

@ -119,6 +119,23 @@ public class SublightSubtitleDescriptor implements SubtitleDescriptor {
} }
@Override
public int hashCode() {
return subtitle.getSubtitleID().hashCode();
}
@Override
public boolean equals(Object object) {
if (object instanceof SublightSubtitleDescriptor) {
SublightSubtitleDescriptor other = (SublightSubtitleDescriptor) object;
return subtitle.getSubtitleID().equals(other.subtitle.getSubtitleID());
}
return false;
}
@Override @Override
public String toString() { public String toString() {
return String.format("%s [%s]", getName(), getLanguageName()); return String.format("%s [%s]", getName(), getLanguageName());

View File

@ -46,7 +46,7 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor {
@Override @Override
public String getType() { public String getType() {
return getSubtitleInfo().get("typeId"); return null;
} }
@ -87,7 +87,7 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor {
@Override @Override
public String getPath() { public String getPath() {
return String.format("%s.%s", getName(), getType()); return String.format("%s.%s", getName(), getSubtitleInfo().get("typeId"));
} }
@ -97,6 +97,23 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor {
} }
@Override
public int hashCode() {
return subtitlePage.getPath().hashCode();
}
@Override
public boolean equals(Object object) {
if (object instanceof SubsceneSubtitleDescriptor) {
SubsceneSubtitleDescriptor other = (SubsceneSubtitleDescriptor) object;
return subtitlePage.getPath().equals(other.getPath());
}
return false;
}
@Override @Override
public String toString() { public String toString() {
return String.format("%s [%s]", getName(), getLanguageName()); return String.format("%s [%s]", getName(), getLanguageName());