diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 2be5e993..e1e64cc7 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -63,7 +63,7 @@ public class ArgumentProcessor { Set files = new LinkedHashSet(args.getFiles(true)); if (args.getSubtitles) { - List subtitles = cli.getSubtitles(files, args.query, args.lang, args.output, args.encoding); + List subtitles = cli.getSubtitles(files, args.query, args.lang, args.output, args.encoding, !args.nonStrict); files.addAll(subtitles); } diff --git a/source/net/sourceforge/filebot/cli/CmdlineInterface.java b/source/net/sourceforge/filebot/cli/CmdlineInterface.java index bd52ea16..d7fad084 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineInterface.java +++ b/source/net/sourceforge/filebot/cli/CmdlineInterface.java @@ -12,7 +12,7 @@ public interface CmdlineInterface { List rename(Collection files, String query, String format, String db, String lang, boolean strict) throws Exception; - List getSubtitles(Collection files, String query, String lang, String output, String encoding) throws Exception; + List getSubtitles(Collection files, String query, String lang, String output, String encoding, boolean strict) throws Exception; boolean check(Collection files) throws Exception; diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index 8538d094..f55613e9 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -19,7 +19,6 @@ 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; @@ -55,7 +54,6 @@ import net.sourceforge.filebot.similarity.StrictEpisodeMetrics; import net.sourceforge.filebot.subtitle.SubtitleFormat; import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.rename.HistorySpooler; -import net.sourceforge.filebot.vfs.ArchiveType; import net.sourceforge.filebot.vfs.MemoryFile; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.EpisodeFormat; @@ -363,7 +361,7 @@ public class CmdlineOperations implements CmdlineInterface { @Override - public List getSubtitles(Collection files, String query, String languageName, String output, String csn) throws Exception { + public List getSubtitles(Collection files, String query, String languageName, String output, String csn, boolean strict) throws Exception { final Language language = getLanguage(languageName); // 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 { + CLILogger.fine("Looking up subtitles by filehash via " + service.getName()); 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()) { + // lookup subtitles via text search, only perform hash lookup in strict mode + if ((query != null || !strict) && !collector.isComplete()) { // auto-detect search query Collection querySet = (query == null) ? detectQuery(filter(files, VIDEO_FILES), false) : singleton(query); @@ -401,6 +400,7 @@ public class CmdlineOperations implements CmdlineInterface { } try { + CLILogger.fine(format("Searching for %s at [%s]", querySet.toString(), service.getName())); collector.addAll(service.getName(), lookupSubtitleByFileName(service, querySet, language, collector.remainingVideos())); } catch (RuntimeException e) { CLILogger.warning(format("Search for [%s] failed: %s", querySet, e.getMessage())); @@ -422,7 +422,7 @@ public class CmdlineOperations implements CmdlineInterface { @Override public File call() throws Exception { 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 List subtitleFiles = new ArrayList(); - ExecutorService executor = Executors.newFixedThreadPool(4); - try { - for (Future it : executor.invokeAll(downloadQueue.values())) { - subtitleFiles.add(it.get()); + if (downloadQueue.size() > 0) { + ExecutorService executor = Executors.newFixedThreadPool(4); + + try { + for (Future it : executor.invokeAll(downloadQueue.values())) { + subtitleFiles.add(it.get()); + } + } finally { + executor.shutdownNow(); } - } finally { - executor.shutdownNow(); } 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 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); - } + MemoryFile subtitleFile = fetchSubtitle(descriptor); // subtitle filename is based on movie filename - String name = getName(movieFile); - String lang = Language.getISO3LanguageCodeByName(descriptor.getLanguageName()); + String base = getName(movieFile); String ext = getExtension(subtitleFile.getName()); ByteBuffer data = subtitleFile.getData(); @@ -477,7 +467,7 @@ public class CmdlineOperations implements CmdlineInterface { 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())); writeFile(data, destination); @@ -487,11 +477,10 @@ public class CmdlineOperations implements CmdlineInterface { 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())); + 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)); } } @@ -503,30 +492,18 @@ public class CmdlineOperations implements CmdlineInterface { 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))); - } + // search for subtitles + List subtitles = findSubtitles(service, querySet, language.getName()); - // 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); - } - + // match subtitle files to video files + if (subtitles.size() > 0) { // 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())); + CLILogger.finest(format("Matched [%s] to [%s] via filename", it.getValue().getName(), it.getCandidate().getName())); subtitleByVideo.put(it.getValue(), it.getCandidate()); } } diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index a4b152f1..bfd2d471 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -32,7 +32,7 @@ def rename(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) { diff --git a/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java b/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java index cc9113af..bec78031 100644 --- a/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java +++ b/source/net/sourceforge/filebot/subtitle/SubtitleUtilities.java @@ -3,6 +3,7 @@ package net.sourceforge.filebot.subtitle; import static java.lang.Math.*; +import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.tuned.FileUtilities.*; import java.io.File; @@ -12,14 +13,62 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; 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.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.web.SearchResult; +import net.sourceforge.filebot.web.SubtitleDescriptor; +import net.sourceforge.filebot.web.SubtitleProvider; public final class SubtitleUtilities { + public static List findSubtitles(SubtitleProvider service, Collection querySet, String languageName) throws Exception { + List subtitles = new ArrayList(); + + // search for and automatically select movie / show entry + Set resultSet = new HashSet(); + 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 findProbableMatches(String query, Iterable searchResults, float threshold) { + // auto-select most probable search result + Set probableMatches = new LinkedHashSet(); + + // 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 */ @@ -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 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. */ diff --git a/source/net/sourceforge/filebot/ui/Language.java b/source/net/sourceforge/filebot/ui/Language.java index 447f0c55..6ea1c5f8 100644 --- a/source/net/sourceforge/filebot/ui/Language.java +++ b/source/net/sourceforge/filebot/ui/Language.java @@ -103,8 +103,7 @@ public class Language { } } - // we won't get here, but just in case - return languageName.replaceAll("\\W", ""); + return null; } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java index 2bf6ce12..db501cd2 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java @@ -32,8 +32,8 @@ import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.filechooser.FileNameExtensionFilter; -import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.web.SubtitleProvider; 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(); @@ -98,7 +101,11 @@ abstract class SubtitleDropTarget extends JButton { // initialize download parameters 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); } @@ -108,14 +115,12 @@ abstract class SubtitleDropTarget extends JButton { // initialize window properties dialog.setIconImage(getImage(getIcon(DropAction.Download))); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - dialog.pack(); + dialog.setSize(670, 575); // show dialog dialog.setLocation(getOffsetLocation(dialog.getOwner())); dialog.setVisible(true); - // now it's up to the user - Analytics.trackEvent("GUI", "LookupSubtitleByHash", getQueryLanguage(), videoFiles.size()); return true; } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java index c5f0acb6..589a1646 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePackage.java @@ -165,7 +165,7 @@ public class SubtitlePackage { ArchiveType archiveType = ArchiveType.forName(subtitle.getType()); - if (archiveType == ArchiveType.UNDEFINED) { + if (archiveType == ArchiveType.UNKOWN) { // cannot extract files from archive return singletonList(new MemoryFile(subtitle.getPath(), data)); } @@ -198,7 +198,7 @@ public class SubtitlePackage { // check if file is a supported archive ArchiveType type = ArchiveType.forName(FileUtilities.getExtension(file.getName())); - if (type != ArchiveType.UNDEFINED) { + if (type != ArchiveType.UNKOWN) { // extract nested archives recursively vfs.addAll(extract(type, file.getData())); } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java index f6342226..25f60133 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java @@ -16,7 +16,6 @@ import java.util.List; import javax.swing.Icon; -import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.ui.AbstractSearchPanel; @@ -51,11 +50,17 @@ public class SubtitlePanel extends AbstractSearchPanel services = new ArrayList(); + private final JPanel hashMatcherServicePanel = createServicePanel(0xFAFAD2); // LightGoldenRodYellow + private final JPanel nameMatcherServicePanel = createServicePanel(0xFFEBCD); // BlanchedAlmond + private final List services = new ArrayList(); private final JTable subtitleMappingTable = createTable(); @@ -81,18 +94,25 @@ class VideoHashSubtitleDownloadDialog extends JDialog { JComponent content = (JComponent) getContentPane(); 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(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(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() { JTable table = new JTable(new SubtitleMappingTableModel()); table.setDefaultRenderer(SubtitleMapping.class, new SubtitleMappingOptionRenderer()); @@ -134,25 +154,37 @@ class VideoHashSubtitleDownloadDialog extends JDialog { } - public void addSubtitleService(final VideoHashSubtitleService service) { - final VideoHashSubtitleServiceBean serviceBean = new VideoHashSubtitleServiceBean(service); - final LinkButton component = new LinkButton(serviceBean.getName(), ResourceManager.getIcon("database.go"), serviceBean.getLink()); + public void addSubtitleService(VideoHashSubtitleService service) { + addSubtitleService(new VideoHashSubtitleServiceBean(service), hashMatcherServicePanel); + } + + + 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 public void propertyChange(PropertyChangeEvent evt) { - if (serviceBean.getState() == StateValue.STARTED) { + if (service.getState() == StateValue.STARTED) { component.setIcon(ResourceManager.getIcon("database.go")); } 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); } @@ -160,38 +192,30 @@ class VideoHashSubtitleDownloadDialog extends JDialog { public void startQuery(String languageName) { final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); - // query services sequentially - queryService = Executors.newFixedThreadPool(1); - - for (final VideoHashSubtitleServiceBean service : services) { - QueryTask task = new QueryTask(service, mappingModel.getVideoFiles(), languageName) { - - @Override - protected void done() { - try { - Map> subtitles = get(); + QueryTask queryTask = new QueryTask(services, mappingModel.getVideoFiles(), languageName) { + + @Override + protected void process(List>> sequence) { + for (Map> subtitles : sequence) { + // update subtitle options + for (SubtitleMapping subtitleMapping : mappingModel) { + List options = subtitles.get(subtitleMapping.getVideoFile()); - // update subtitle options - for (SubtitleMapping subtitleMapping : mappingModel) { - List options = subtitles.get(subtitleMapping.getVideoFile()); - - if (options != null && options.size() > 0) { - subtitleMapping.addOptions(options); - } + 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); - } 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 List downloadQueue = new ArrayList(); - for (SubtitleMapping mapping : mappingModel) { + for (final SubtitleMapping mapping : mappingModel) { SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); 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 existingFiles = new ArrayList(); 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); - existingFiles.add(download.getDestination().getName()); + existingFiles.add(target.getName()); } } @@ -349,10 +386,13 @@ class VideoHashSubtitleDownloadDialog extends JDialog { // download in progress setText(subtitleBean.getText()); setIcon(ResourceManager.getIcon("action.fetch")); - } else { + } else if (mapping.getSubtitleFile() != null) { // download complete setText(mapping.getSubtitleFile().getName()); setIcon(ResourceManager.getIcon("status.ok")); + } else { + setText(null); + setIcon(null); } return this; @@ -514,7 +554,8 @@ class VideoHashSubtitleDownloadDialog extends JDialog { private static class SubtitleMapping extends AbstractBean { - private final File videoFile; + private File videoFile; + private File subtitleFile; private SubtitleDescriptorBean selectedOption; private List options = new ArrayList(); @@ -531,18 +572,18 @@ class VideoHashSubtitleDownloadDialog extends JDialog { public File getSubtitleFile() { - if (selectedOption == null) - 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()); - - return new File(videoFile.getParentFile(), subtitleName); + return subtitleFile; + } + + + public void setSubtitleFile(File subtitleFile) { + this.subtitleFile = subtitleFile; + firePropertyChange("subtitleFile", null, this.subtitleFile); } 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 final SubtitleDescriptor subtitle; - private final VideoHashSubtitleServiceBean service; + private final SubtitleDescriptor descriptor; + private final SubtitleServiceBean service; private StateValue state; private Exception error; - public SubtitleDescriptorBean(SubtitleDescriptor subtitle, VideoHashSubtitleServiceBean source) { - this.subtitle = subtitle; - this.service = source; + public SubtitleDescriptorBean(SubtitleDescriptor descriptor, SubtitleServiceBean service) { + this.descriptor = descriptor; + this.service = service; } 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() { - return Language.getISO3LanguageCodeByName(subtitle.getLanguageName()); + return Language.getISO3LanguageCodeByName(descriptor.getLanguageName()); } public String getType() { - return subtitle.getType(); + return descriptor.getType(); } - public ByteBuffer fetch() throws Exception { + public MemoryFile fetch() throws Exception { setState(StateValue.STARTED); try { - ByteBuffer data = subtitle.fetch(); - Analytics.trackEvent(service.getName(), "DownloadSubtitle", subtitle.getLanguageName(), 1); + MemoryFile data = fetchSubtitle(descriptor); + Analytics.trackEvent(service.getName(), "DownloadSubtitle", descriptor.getLanguageName(), 1); return data; } catch (Exception e) { @@ -665,60 +706,83 @@ class VideoHashSubtitleDownloadDialog extends JDialog { } - private static class QueryTask extends SwingWorker>, Void> { + private static class QueryTask extends SwingWorker, Map>> { - private final VideoHashSubtitleServiceBean service; + private final Collection services; - private final File[] videoFiles; + private final Collection remainingVideos; private final String languageName; - public QueryTask(VideoHashSubtitleServiceBean service, Collection videoFiles, String languageName) { - this.service = service; - this.videoFiles = videoFiles.toArray(new File[0]); + public QueryTask(Collection services, Collection videoFiles, String languageName) { + this.services = services; + this.remainingVideos = new TreeSet(videoFiles); this.languageName = languageName; } @Override - protected Map> doInBackground() throws Exception { - Map> subtitleSet = new HashMap>(); - - for (final Entry> result : service.getSubtitleList(videoFiles, languageName).entrySet()) { - List subtitles = new ArrayList(); - - // associate subtitles with services - for (SubtitleDescriptor subtitleDescriptor : result.getValue()) { - subtitles.add(new SubtitleDescriptorBean(subtitleDescriptor, service)); + protected Collection doInBackground() throws Exception { + for (SubtitleServiceBean service : services) { + try { + if (isCancelled()) + throw new CancellationException(); + + Map> subtitleSet = new HashMap>(); + + for (final Entry> result : service.lookupSubtitles(remainingVideos, languageName).entrySet()) { + List subtitles = new ArrayList(); + + // 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> 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 { - private final SubtitleDescriptorBean subtitle; - private final File destination; + private final File video; + private final SubtitleDescriptorBean descriptor; - public DownloadTask(SubtitleDescriptorBean subtitle, File destination) { - this.subtitle = subtitle; - this.destination = destination; + public DownloadTask(File video, SubtitleDescriptorBean descriptor) { + this.video = video; + this.descriptor = descriptor; } public SubtitleDescriptorBean getSubtitleBean() { - return subtitle; + return descriptor; } - public File getDestination() { - return destination; + public File getDestination(MemoryFile subtitle) { + 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() { try { // fetch subtitle - ByteBuffer data = subtitle.fetch(); + MemoryFile subtitle = descriptor.fetch(); if (isCancelled()) return null; // save to file - writeFile(data, destination); + File destination = getDestination(subtitle); + writeFile(subtitle.getData(), destination); return destination; } 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 Throwable error; + private StateValue state = StateValue.PENDING; + private Throwable error = null; - public VideoHashSubtitleServiceBean(VideoHashSubtitleService service) { - this.service = service; - this.state = StateValue.PENDING; + public SubtitleServiceBean(String name, Icon icon, URI link) { + this.name = name; + this.icon = icon; + this.link = link; } public String getName() { - return service.getName(); + return name; } public Icon getIcon() { - return service.getIcon(); + return icon; } public URI getLink() { - return service.getLink(); + return link; } - public Map> getSubtitleList(File[] files, String languageName) throws Exception { + protected abstract Map> getSubtitleList(Collection files, String languageName) throws Exception; + + + public final Map> lookupSubtitles(Collection files, String languageName) throws Exception { setState(StateValue.STARTED); try { - return service.getSubtitleList(files, languageName); + return getSubtitleList(files, languageName); } catch (Exception e) { // remember error error = e; @@ -804,7 +875,74 @@ class VideoHashSubtitleDownloadDialog extends JDialog { public Throwable getError() { 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> getSubtitleList(Collection 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> getSubtitleList(Collection files, String languageName) throws Exception { + Map> subtitlesByFile = new HashMap>(); + for (File file : files) { + subtitlesByFile.put(file, new ArrayList()); + } + + // auto-detect query and search for subtitles + Collection querySet = new SeriesNameMatcher().matchAll(files.toArray(new File[0])); + List subtitles = findSubtitles(service, querySet, languageName); + + // first match everything as best as possible, then filter possibly bad matches + Matcher matcher = new Matcher(files, 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) { + 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; + } } } diff --git a/source/net/sourceforge/filebot/vfs/ArchiveType.java b/source/net/sourceforge/filebot/vfs/ArchiveType.java index 9579cd89..838e39a9 100644 --- a/source/net/sourceforge/filebot/vfs/ArchiveType.java +++ b/source/net/sourceforge/filebot/vfs/ArchiveType.java @@ -4,6 +4,7 @@ package net.sourceforge.filebot.vfs; import java.nio.ByteBuffer; import java.util.Collections; +import java.util.EnumSet; public enum ArchiveType { @@ -26,6 +27,26 @@ public enum ArchiveType { UNDEFINED { + @Override + public Iterable fromData(ByteBuffer data) { + for (ArchiveType type : EnumSet.of(ZIP, RAR)) { + try { + Iterable 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 public Iterable fromData(ByteBuffer data) { // cannot extract data, return empty archive @@ -34,13 +55,16 @@ public enum ArchiveType { }; public static ArchiveType forName(String name) { + if (name == null) + return UNDEFINED; + if ("zip".equalsIgnoreCase(name)) return ZIP; if ("rar".equalsIgnoreCase(name)) return RAR; - return UNDEFINED; + return UNKOWN; } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java index 722bd6cc..d783fd03 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java @@ -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 public String toString() { return String.format("%s [%s]", getName(), getLanguageName()); diff --git a/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java index 919d9060..2c9def44 100644 --- a/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SublightSubtitleDescriptor.java @@ -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 public String toString() { return String.format("%s [%s]", getName(), getLanguageName()); diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java index 21b636cd..45bf5f9b 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java @@ -46,7 +46,7 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor { @Override public String getType() { - return getSubtitleInfo().get("typeId"); + return null; } @@ -87,7 +87,7 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor { @Override 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 public String toString() { return String.format("%s [%s]", getName(), getLanguageName());