package net.filebot.ui.subtitle; import static javax.swing.BorderFactory.*; import static javax.swing.JOptionPane.*; import static net.filebot.Logging.*; import static net.filebot.Settings.*; import static net.filebot.subtitle.SubtitleUtilities.*; import static net.filebot.util.FileUtilities.*; import static net.filebot.util.ui.SwingUI.*; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Window; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.net.URI; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.DefaultCellEditor; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.SwingWorker.StateValue; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import net.filebot.ResourceManager; import net.filebot.WebServices; import net.filebot.platform.mac.MacAppUtilities; import net.filebot.subtitle.SubtitleMetrics; import net.filebot.subtitle.SubtitleNaming; import net.filebot.util.ui.AbstractBean; import net.filebot.util.ui.DashedSeparator; import net.filebot.util.ui.EmptySelectionModel; import net.filebot.util.ui.LinkButton; import net.filebot.util.ui.RoundBorder; import net.filebot.vfs.MemoryFile; import net.filebot.web.SubtitleDescriptor; import net.filebot.web.SubtitleProvider; import net.filebot.web.VideoHashSubtitleService; import net.miginfocom.swing.MigLayout; class SubtitleAutoMatchDialog extends JDialog { private static final Color hashMatchColor = new Color(0xFAFAD2); // LightGoldenRodYellow private static final Color nameMatchColor = new Color(0xFFEBCD); // BlanchedAlmond private final JPanel hashMatcherServicePanel = createServicePanel(hashMatchColor); private final JPanel nameMatcherServicePanel = createServicePanel(nameMatchColor); private final List<SubtitleServiceBean> services = new ArrayList<SubtitleServiceBean>(); private final JTable subtitleMappingTable = createTable(); private final JComboBox<SubtitleNaming> preferredSubtitleNaming = new JComboBox<SubtitleNaming>(SubtitleNaming.values()); private ExecutorService queryService; private ExecutorService downloadService; public SubtitleAutoMatchDialog(Window owner) { super(owner, "Download Subtitles", ModalityType.DOCUMENT_MODAL); preferredSubtitleNaming.setSelectedItem(SubtitleNaming.MATCH_VIDEO_ADD_LANGUAGE_TAG); JComponent content = (JComponent) getContentPane(); content.setLayout(new MigLayout("fill, insets 12 15 7 15, nogrid, novisualpadding", "", "[fill][pref!]")); content.add(new JScrollPane(subtitleMappingTable), "grow, wrap"); content.add(hashMatcherServicePanel, "gap after rel"); content.add(nameMatcherServicePanel, "gap after indent*2"); preferredSubtitleNaming.setBorder(createCompoundBorder(createTitledBorder("Subtitle Naming"), preferredSubtitleNaming.getBorder())); content.add(new JLabel(), "grow"); // SPACER content.add(preferredSubtitleNaming, "gap after indent*2"); content.add(new JButton(downloadAction), "tag ok"); content.add(new JButton(finishAction), "tag cancel"); } protected JPanel createServicePanel(Color color) { JPanel panel = new JPanel(new MigLayout("hidemode 3, novisualpadding")); panel.setBorder(new RoundBorder()); panel.setOpaque(false); panel.setBackground(color); panel.setVisible(false); return panel; } protected JTable createTable() { JTable table = new JTable(new SubtitleMappingTableModel()); table.setDefaultRenderer(SubtitleMapping.class, new SubtitleMappingOptionRenderer()); table.setRowHeight(24); table.setIntercellSpacing(new Dimension(5, 5)); table.setBackground(Color.white); table.setAutoCreateRowSorter(true); table.setFillsViewportHeight(true); JComboBox editor = new SimpleComboBox(ResourceManager.getIcon("action.select")); editor.setRenderer(new SubtitleOptionRenderer(true)); // disable selection table.setSelectionModel(new EmptySelectionModel()); editor.setFocusable(false); table.setDefaultEditor(SubtitleMapping.class, new DefaultCellEditor(editor) { @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { JComboBox editor = (JComboBox) super.getTableCellEditorComponent(table, value, isSelected, row, column); SubtitleMapping mapping = (SubtitleMapping) value; DefaultComboBoxModel model = new DefaultComboBoxModel(mapping.getOptions()); model.addElement(null); // option to cancel selection editor.setModel(model); editor.setSelectedItem(mapping.getSelectedOption()); return editor; } }); return table; } public void setVideoFiles(File[] videoFiles) { subtitleMappingTable.setModel(new SubtitleMappingTableModel(videoFiles)); } public void addSubtitleService(VideoHashSubtitleService service) { addSubtitleService(new VideoHashSubtitleServiceBean(service), hashMatcherServicePanel); } public void addSubtitleService(SubtitleProvider service) { addSubtitleService(new SubtitleProviderBean(service, this), nameMatcherServicePanel); } protected void addSubtitleService(final SubtitleServiceBean service, final JPanel servicePanel) { final LinkButton component = new LinkButton(service.getDescription(), null, ResourceManager.getIcon("database"), service.getLink()); component.setBorder(BorderFactory.createEmptyBorder()); component.setVisible(false); service.addPropertyChangeListener(evt -> { if (service.getState() == StateValue.STARTED) { component.setIcon(ResourceManager.getIcon("database.go")); } else { component.setIcon(ResourceManager.getIcon(service.getError() == null ? "database.ok" : "database.error")); } component.setVisible(true); component.setToolTipText(String.format("%s: %s", service.getName(), service.getError() == null ? service.getState().toString().toLowerCase() : service.getError().getMessage())); servicePanel.setVisible(true); servicePanel.getParent().revalidate(); }); services.add(service); servicePanel.add(component); } public void startQuery(Locale locale) { SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); QueryTask queryTask = new QueryTask(services, mappingModel.getVideoFiles(), locale, SubtitleAutoMatchDialog.this) { @Override protected void process(List<Map<File, List<SubtitleDescriptorBean>>> sequence) { for (Map<File, List<SubtitleDescriptorBean>> subtitles : sequence) { // update subtitle options for (SubtitleMapping subtitleMapping : mappingModel) { List<SubtitleDescriptorBean> options = subtitles.get(subtitleMapping.getVideoFile()); if (options != null && options.size() > 0) { subtitleMapping.addOptions(options); } } // make subtitle column visible if (subtitles.size() > 0) { mappingModel.setOptionColumnVisible(true); } } } @Override protected void done() { SwingUtilities.invokeLater(mappingModel::fireTableStructureChanged); // make sure UI is refershed after completion } }; queryService = Executors.newSingleThreadExecutor(); queryService.submit(queryTask); } private Boolean showConfirmReplaceDialog(List<?> files) { JList existingFilesComponent = new JList(files.toArray()) { @Override public Dimension getPreferredScrollableViewportSize() { // adjust component size return new Dimension(80, 50); } }; Object[] message = new Object[] { "Replace existing subtitle files?", new JScrollPane(existingFilesComponent) }; Object[] options = new Object[] { "Replace All", "Skip All", "Cancel" }; JOptionPane optionPane = new JOptionPane(message, WARNING_MESSAGE, YES_NO_CANCEL_OPTION, null, options); // display option dialog optionPane.createDialog(SubtitleAutoMatchDialog.this, "Replace").setVisible(true); // replace all if (options[0] == optionPane.getValue()) return true; // skip all if (options[1] == optionPane.getValue()) return false; // cancel return null; } private final Action downloadAction = new AbstractAction("Download", ResourceManager.getIcon("dialog.continue")) { @Override public void actionPerformed(ActionEvent evt) { // disable any active cell editor if (subtitleMappingTable.getCellEditor() != null) { subtitleMappingTable.getCellEditor().stopCellEditing(); } // don't allow restart of download as long as there are still unfinished download tasks if (downloadService != null && !downloadService.isTerminated()) { return; } final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); // make sure we have access to the parent folder structure, not just the dropped file if (isMacSandbox()) { MacAppUtilities.askUnlockFolders(getWindow(evt.getSource()), mappingModel.getVideoFiles()); } // collect the subtitles that will be fetched List<DownloadTask> downloadQueue = new ArrayList<DownloadTask>(); for (final SubtitleMapping mapping : mappingModel) { SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); if (subtitleBean != null && subtitleBean.getState() == null) { downloadQueue.add(new DownloadTask(mapping.getVideoFile(), subtitleBean, (SubtitleNaming) preferredSubtitleNaming.getSelectedItem()) { @Override protected void done() { try { mapping.setSubtitleFile(get()); } catch (Exception e) { debug.log(Level.WARNING, e.getMessage(), e); } } }); } } // collect downloads that will override a file List<DownloadTask> confirmReplaceDownloadQueue = new ArrayList<DownloadTask>(); List<String> existingFiles = new ArrayList<String>(); for (DownloadTask download : downloadQueue) { // 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(target.getName()); } } // confirm replace if (confirmReplaceDownloadQueue.size() > 0) { Boolean option = showConfirmReplaceDialog(existingFiles); // abort the operation altogether if (option == null) { return; } // don't replace any files if (option == false) { downloadQueue.removeAll(confirmReplaceDownloadQueue); } } // start download if (downloadQueue.size() > 0) { downloadService = Executors.newSingleThreadExecutor(); for (DownloadTask downloadTask : downloadQueue) { downloadTask.getSubtitleBean().setState(StateValue.PENDING); downloadService.execute(downloadTask); } // terminate after all downloads have been completed downloadService.shutdown(); } } }; private final Action finishAction = new AbstractAction("Close", ResourceManager.getIcon("dialog.cancel")) { @Override public void actionPerformed(ActionEvent evt) { if (queryService != null) { queryService.shutdownNow(); } if (downloadService != null) { downloadService.shutdownNow(); } setVisible(false); dispose(); } }; private static class SubtitleMappingOptionRenderer extends DefaultTableCellRenderer { private final JComboBox optionComboBox = new SimpleComboBox(ResourceManager.getIcon("action.select")); public SubtitleMappingOptionRenderer() { optionComboBox.setBackground(Color.white); optionComboBox.setRenderer(new SubtitleOptionRenderer(false)); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { SubtitleMapping mapping = (SubtitleMapping) value; SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); // render combobox for subtitle options if (mapping.isEditable() && subtitleBean != null) { optionComboBox.setModel(new DefaultComboBoxModel(new Object[] { subtitleBean })); return optionComboBox; } // render status label super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); setForeground(table.getForeground()); if (subtitleBean == null) { if (mapping.getOptions().length == 0) { // no subtitles found setText("No subtitles found"); setIcon(null); setForeground(Color.gray); } else { // no subtitles found setText("No subtitles selected"); setIcon(null); setForeground(Color.gray); } } else if (subtitleBean.getState() == StateValue.PENDING) { // download in the queue setText(subtitleBean.getText()); setIcon(ResourceManager.getIcon("worker.pending")); } else if (subtitleBean.getState() == StateValue.STARTED) { // download in progress setText(subtitleBean.getText()); setIcon(ResourceManager.getIcon("action.fetch")); } else if (mapping.getSubtitleFile() != null) { // download complete setText(mapping.getSubtitleFile().getName()); setIcon(ResourceManager.getIcon("status.ok")); } else { setText(null); setIcon(null); } return this; } } private static class SubtitleOptionRenderer extends DefaultListCellRenderer { private final Border padding = createEmptyBorder(3, 3, 3, 3); private final boolean isEditor; public SubtitleOptionRenderer(boolean isEditor) { this.isEditor = isEditor; } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { super.getListCellRendererComponent(list, null, index, isSelected, cellHasFocus); setBorder(padding); SubtitleDescriptorBean subtitleBean = (SubtitleDescriptorBean) value; if (isEditor && index == list.getModel().getSize() - 2) { setBorder(new CompoundBorder(new DashedSeparator(10, 4, Color.lightGray, list.getBackground()), getBorder())); // this element is always the last one } if (value == null) { setText("Cancel selection"); setIcon(ResourceManager.getIcon("dialog.cancel")); } else { if (subtitleBean.getError() == null) { setText(subtitleBean.getText()); setIcon(subtitleBean.getIcon()); } else { setText(String.format("%s (%s)", subtitleBean.getError(), subtitleBean.getText())); setIcon(ResourceManager.getIcon("status.warning")); } if (!isSelected) { float f = subtitleBean.getMatchProbability(); if (f < 1) { setOpaque(true); setBackground(nameMatchColor); } if (f < 0.9f) { setOpaque(true); setBackground(derive(Color.RED, (1 - f) * 0.5f)); } } } return this; } } private static class SubtitleMappingTableModel extends AbstractTableModel implements Iterable<SubtitleMapping> { private final SubtitleMapping[] data; private boolean optionColumnVisible = false; public SubtitleMappingTableModel(File... videoFiles) { data = new SubtitleMapping[videoFiles.length]; for (int i = 0; i < videoFiles.length; i++) { data[i] = new SubtitleMapping(videoFiles[i]); data[i].addPropertyChangeListener(new SubtitleMappingListener(i)); } } public List<File> getVideoFiles() { return new AbstractList<File>() { @Override public File get(int index) { return data[index].getVideoFile(); } @Override public int size() { return data.length; } }; } @Override public Iterator<SubtitleMapping> iterator() { return Arrays.asList(data).iterator(); } public void setOptionColumnVisible(boolean optionColumnVisible) { if (this.optionColumnVisible == optionColumnVisible) return; this.optionColumnVisible = optionColumnVisible; // update columns fireTableStructureChanged(); } @Override public int getColumnCount() { return optionColumnVisible ? 2 : 1; } @Override public String getColumnName(int column) { switch (column) { case 0: return "Video"; case 1: return "Subtitle"; } return null; } @Override public int getRowCount() { return data.length; } @Override public Object getValueAt(int row, int column) { switch (column) { case 0: return data[row].getVideoFile().getName(); case 1: return data[row]; } return null; } @Override public void setValueAt(Object value, int row, int column) { data[row].setSelectedOption((SubtitleDescriptorBean) value); } @Override public boolean isCellEditable(int row, int column) { return column == 1 && data[row].isEditable(); } @Override public Class<?> getColumnClass(int column) { switch (column) { case 0: return String.class; case 1: return SubtitleMapping.class; } return null; } private class SubtitleMappingListener implements PropertyChangeListener { private final int index; public SubtitleMappingListener(int index) { this.index = index; } @Override public void propertyChange(PropertyChangeEvent evt) { // update state and subtitle options fireTableRowsUpdated(index, index); } } } private static class SubtitleMapping extends AbstractBean { private File videoFile; private File subtitleFile; private SubtitleDescriptorBean selectedOption; private List<SubtitleDescriptorBean> options = new ArrayList<SubtitleDescriptorBean>(); public SubtitleMapping(File videoFile) { this.videoFile = videoFile; } public File getVideoFile() { return videoFile; } public File getSubtitleFile() { return subtitleFile; } public void setSubtitleFile(File subtitleFile) { this.subtitleFile = subtitleFile; firePropertyChange("subtitleFile", null, this.subtitleFile); } public boolean isEditable() { return subtitleFile == null && options.size() > 0 && (selectedOption == null || (selectedOption.getState() == null || selectedOption.getError() != null)); } public SubtitleDescriptorBean getSelectedOption() { return selectedOption; } public void setSelectedOption(SubtitleDescriptorBean selectedOption) { if (this.selectedOption != null) { this.selectedOption.removePropertyChangeListener(selectedOptionListener); } this.selectedOption = selectedOption; if (this.selectedOption != null) { this.selectedOption.addPropertyChangeListener(selectedOptionListener); } firePropertyChange("selectedOption", null, this.selectedOption); } public SubtitleDescriptorBean[] getOptions() { return options.toArray(new SubtitleDescriptorBean[0]); } public void addOptions(List<SubtitleDescriptorBean> options) { this.options.addAll(options); if (selectedOption == null && options.size() > 0) { setSelectedOption(options.get(0)); } } private final PropertyChangeListener selectedOptionListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { firePropertyChange("selectedOption", null, selectedOption); } }; } private static class SubtitleDescriptorBean extends AbstractBean { private final File videoFile; private final SubtitleDescriptor descriptor; private final SubtitleServiceBean service; private StateValue state; private Exception error; public SubtitleDescriptorBean(File videoFile, SubtitleDescriptor descriptor, SubtitleServiceBean service) { this.videoFile = videoFile; this.descriptor = descriptor; this.service = service; } public SubtitleDescriptor getDescriptor() { return descriptor; } public float getMatchProbability() { return service.getMatchProbabilty(videoFile, descriptor); } public String getText() { return formatSubtitle(descriptor.getName(), getLanguageName(), getType()); } public Icon getIcon() { return service.getIcon(); } public String getLanguageName() { return descriptor.getLanguageName(); } public String getType() { return descriptor.getType(); } public MemoryFile fetch() throws Exception { setState(StateValue.STARTED); try { return fetchSubtitle(descriptor); } catch (Exception e) { // remember and rethrow exception throw (error = e); } finally { setState(StateValue.DONE); } } public Exception getError() { return error; } public StateValue getState() { return state; } public void setState(StateValue state) { this.state = state; firePropertyChange("state", null, this.state); } @Override public String toString() { return getText(); } } private static class QueryTask extends SwingWorker<Collection<File>, Map<File, List<SubtitleDescriptorBean>>> { private final Component parent; private final Collection<SubtitleServiceBean> services; private final Collection<File> remainingVideos; private final Locale locale; public QueryTask(Collection<SubtitleServiceBean> services, Collection<File> videoFiles, Locale locale, Component parent) { this.parent = parent; this.services = services; this.remainingVideos = new TreeSet<File>(videoFiles); this.locale = locale; } @Override protected Collection<File> doInBackground() throws Exception { for (SubtitleServiceBean service : services) { if (isCancelled() || Thread.interrupted()) { throw new CancellationException(); } if (remainingVideos.isEmpty()) { break; } try { Map<File, List<SubtitleDescriptorBean>> subtitleSet = new HashMap<File, List<SubtitleDescriptorBean>>(); for (final Entry<File, List<SubtitleDescriptor>> result : service.lookupSubtitles(remainingVideos, locale, parent).entrySet()) { Set<SubtitleDescriptor> subtitlesByRelevance = new LinkedHashSet<SubtitleDescriptor>(); // guess best hash match (default order is open bad due to invalid hash links) SubtitleDescriptor bestMatch = getBestMatch(result.getKey(), result.getValue(), false); if (bestMatch != null) { subtitlesByRelevance.add(bestMatch); } subtitlesByRelevance.addAll(result.getValue()); // associate subtitles with services List<SubtitleDescriptorBean> subtitles = new ArrayList<SubtitleDescriptorBean>(); for (SubtitleDescriptor it : subtitlesByRelevance) { subtitles.add(new SubtitleDescriptorBean(result.getKey(), it, 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 (CancellationException e) { // don't ignore cancellation throw e; } catch (InterruptedException e) { // don't ignore cancellation throw e; } catch (Exception e) { // log and ignore debug.log(Level.WARNING, e.getMessage(), e); } } return remainingVideos; } } private static class DownloadTask extends SwingWorker<File, Void> { private final File video; private final SubtitleDescriptorBean descriptor; private final SubtitleNaming naming; public DownloadTask(File video, SubtitleDescriptorBean descriptor, SubtitleNaming naming) { this.video = video; this.descriptor = descriptor; this.naming = naming; } public SubtitleDescriptorBean getSubtitleBean() { return descriptor; } public File getDestination(MemoryFile subtitle) { if (descriptor.getType() == null && subtitle == null) return null; // prefer type from descriptor because we need to know before we download the actual subtitle file String name = naming.format(video, descriptor.getDescriptor(), descriptor.getType()); return new File(video.getParentFile(), name); } @Override protected File doInBackground() { try { // fetch subtitle MemoryFile subtitle = descriptor.fetch(); if (isCancelled()) return null; // save to file File destination = getDestination(subtitle); writeFile(subtitle.getData(), destination); return destination; } catch (Exception e) { // display error message in GUI descriptor.error = e; // print to error log debug.log(Level.WARNING, e.getMessage(), e); } return null; } } protected static abstract class SubtitleServiceBean extends AbstractBean { private final String name; private final Icon icon; private final URI link; private StateValue state = StateValue.PENDING; private Exception error = null; public SubtitleServiceBean(String name, Icon icon, URI link) { this.name = name; this.icon = icon; this.link = link; } public String getName() { return name; } public Icon getIcon() { return icon; } public URI getLink() { return link; } public abstract String getDescription(); public abstract float getMatchProbabilty(File videoFile, SubtitleDescriptor descriptor); protected abstract Map<File, List<SubtitleDescriptor>> getSubtitleList(Collection<File> files, Locale locale, Component parent) throws Exception; public final Map<File, List<SubtitleDescriptor>> lookupSubtitles(Collection<File> files, Locale locale, Component parent) throws Exception { setState(StateValue.STARTED); try { return getSubtitleList(files, locale, parent); } catch (Exception e) { throw (error = e); } finally { setState(StateValue.DONE); } } private void setState(StateValue state) { this.state = state; firePropertyChange("state", null, this.state); } public StateValue getState() { return state; } 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 public String getDescription() { return service == WebServices.OpenSubtitles ? "Exact Search" : service.getName(); } @Override protected Map<File, List<SubtitleDescriptor>> getSubtitleList(Collection<File> files, Locale locale, Component parent) throws Exception { return lookupSubtitlesByHash(service, files, locale, true, false); } @Override public float getMatchProbabilty(File videoFile, SubtitleDescriptor descriptor) { return 1; } } protected static class SubtitleProviderBean extends SubtitleServiceBean { private SubtitleProvider service; public SubtitleProviderBean(SubtitleProvider service, SubtitleAutoMatchDialog inputProvider) { super(service.getName(), service.getIcon(), service.getLink()); this.service = service; } @Override public String getDescription() { return "Fuzzy Search"; } @Override protected Map<File, List<SubtitleDescriptor>> getSubtitleList(Collection<File> fileSet, Locale locale, Component parent) throws Exception { return findSubtitlesByName(service, fileSet, locale, null, true, false); } @Override public float getMatchProbabilty(File videoFile, SubtitleDescriptor descriptor) { return SubtitleMetrics.verificationMetric().getSimilarity(videoFile, descriptor); } } }