+ added support for subtitle download via video/movie hash

* added video/subtitle file drop target in SubtitlePanel
* added VideoHashSubtitleDownloadDialog
This commit is contained in:
Reinhard Pointner 2009-10-21 22:39:02 +00:00
parent 39dd413eec
commit 916f168d98
26 changed files with 1405 additions and 28 deletions

BIN
fw/database.ok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -75,8 +75,8 @@ public abstract class AbstractSearchPanel<S, E> extends JComponent {
searchTextField.getEditor().setAction(searchAction);
searchTextField.getSelectButton().setModel(Arrays.asList(createSearchEngines()));
searchTextField.getSelectButton().setLabelProvider(createSearchEngineLabelProvider());
searchTextField.getSelectButton().setModel(Arrays.asList(getSearchEngines()));
searchTextField.getSelectButton().setLabelProvider(getSearchEngineLabelProvider());
try {
// restore selected subtitle client
@ -101,10 +101,10 @@ public abstract class AbstractSearchPanel<S, E> extends JComponent {
}
protected abstract S[] createSearchEngines();
protected abstract S[] getSearchEngines();
protected abstract LabelProvider<S> createSearchEngineLabelProvider();
protected abstract LabelProvider<S> getSearchEngineLabelProvider();
protected abstract Settings getSettings();

View File

@ -79,7 +79,7 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
@Override
protected EpisodeListProvider[] createSearchEngines() {
protected EpisodeListProvider[] getSearchEngines() {
return new EpisodeListProvider[] {
new TVRageClient(),
new AnidbClient(),
@ -91,7 +91,7 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
@Override
protected LabelProvider<EpisodeListProvider> createSearchEngineLabelProvider() {
protected LabelProvider<EpisodeListProvider> getSearchEngineLabelProvider() {
return SimpleLabelProvider.forClass(EpisodeListProvider.class);
}

View File

@ -449,7 +449,7 @@ class HistoryDialog extends JDialog {
type = QUESTION_MESSAGE;
options = EnumSet.of(Option.Rename, Option.ChangeDirectory, Option.Cancel);
} else {
String text = String.format("Some files are missing. Please select a different directory.");
String text = "Some files are missing. Please select a different directory.";
JList missingFilesComponent = new JList(missingFiles.toArray()) {
@Override

View File

@ -0,0 +1,70 @@
package net.sourceforge.filebot.ui.panel.subtitle;
import static javax.swing.BorderFactory.*;
import java.awt.Color;
import java.awt.Rectangle;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;
import net.sourceforge.filebot.ResourceManager;
public class SimpleComboBox extends JComboBox {
public SimpleComboBox() {
setUI(new SimpleComboBoxUI());
setBorder(createEmptyBorder());
}
private static class SimpleComboBoxUI extends BasicComboBoxUI {
@Override
protected JButton createArrowButton() {
JButton button = new JButton(ResourceManager.getIcon("arrow.down"));
button.setContentAreaFilled(false);
button.setBorderPainted(false);
button.setFocusPainted(false);
button.setOpaque(false);
return button;
}
@Override
protected ComboPopup createPopup() {
return new BasicComboPopup(comboBox) {
@Override
protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {
Rectangle bounds = super.computePopupBounds(px, py, pw, ph);
// allow combobox popup to be wider than the combobox itself
bounds.width = Math.max(bounds.width, list.getPreferredSize().width);
return bounds;
}
@Override
protected void configurePopup() {
super.configurePopup();
setOpaque(true);
setBackground(list.getBackground());
// use gray instead of black border for combobox popup
setBorder(createCompoundBorder(createLineBorder(Color.gray, 1), createEmptyBorder(1, 1, 1, 1)));
}
};
}
}
}

View File

@ -0,0 +1,238 @@
package net.sourceforge.filebot.ui.panel.subtitle;
import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.filebot.ui.transfer.FileTransferable.*;
import static net.sourceforge.tuned.FileUtilities.*;
import static net.sourceforge.tuned.ui.TunedUtilities.*;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileNameExtensionFilter;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.web.VideoHashSubtitleService;
abstract class SubtitleDropTarget extends JButton {
private enum DropAction {
Download,
Upload,
Cancel
}
public SubtitleDropTarget() {
setHorizontalAlignment(CENTER);
setHideActionText(true);
setBorderPainted(false);
setContentAreaFilled(false);
setFocusPainted(false);
setBackground(Color.white);
setOpaque(false);
// initialize with default mode
setDropAction(DropAction.Download);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
// install mouse listener
addActionListener(clickHandler);
// install drop target
new DropTarget(this, dropHandler);
}
private void setDropAction(DropAction dropAction) {
setIcon(getIcon(dropAction));
}
private Icon getIcon(DropAction dropAction) {
switch (dropAction) {
case Download:
return ResourceManager.getIcon("subtitle.exact.download");
case Upload:
return ResourceManager.getIcon("subtitle.exact.upload");
default:
return ResourceManager.getIcon("message.error");
}
}
public abstract VideoHashSubtitleService[] getServices();
public abstract String getQueryLanguage();
private boolean handleDownload(List<File> videoFiles) {
VideoHashSubtitleDownloadDialog dialog = new VideoHashSubtitleDownloadDialog(getWindow(this));
// initialize download parameters
dialog.setVideoFiles(videoFiles.toArray(new File[0]));
for (VideoHashSubtitleService service : getServices()) {
dialog.addSubtitleService(service);
}
// start looking for subtitles
dialog.startQuery(getQueryLanguage());
// initialize window properties
dialog.setIconImage(getImage(getIcon(DropAction.Download)));
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.pack();
// show dialog
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setVisible(true);
// now it's up to the user
return true;
}
private boolean handleUpload(Map<File, File> videosMappedBySubtitle) {
// TODO implement upload
throw new UnsupportedOperationException("Not implemented yet");
}
private boolean handleDrop(List<File> files) {
// perform a drop action depending on the given files
if (containsOnly(files, VIDEO_FILES)) {
return handleDownload(files);
}
if (containsOnly(files, FOLDERS)) {
// collect all video files from the dropped folders
return handleDownload(filter(listFiles(files, 0), VIDEO_FILES));
}
if (containsOnly(files, SUBTITLE_FILES)) {
// TODO implement upload
throw new UnsupportedOperationException("Not implemented yet");
}
if (filter(files, VIDEO_FILES).size() == filter(files, SUBTITLE_FILES).size()) {
// TODO implement upload
throw new UnsupportedOperationException("Not implemented yet");
}
return false;
}
private DropAction getDropAction(List<File> files) {
// video files only, or any folder, containing video files
if (containsOnly(files, VIDEO_FILES) || (containsOnly(files, FOLDERS) && filter(listFiles(files, 0), VIDEO_FILES).size() > 0)) {
return DropAction.Download;
}
// subtitle files only, or video/subtitle pairs
if (containsOnly(files, SUBTITLE_FILES) || filter(files, VIDEO_FILES).size() == filter(files, SUBTITLE_FILES).size()) {
return DropAction.Upload;
}
// unknown input
return DropAction.Cancel;
}
private final DropTargetAdapter dropHandler = new DropTargetAdapter() {
@Override
public void dragEnter(DropTargetDragEvent dtde) {
DropAction dropAction = DropAction.Cancel;
try {
dropAction = getDropAction(getFilesFromTransferable(dtde.getTransferable()));
} catch (Exception e) {
// ignore
}
// update visual representation
setDropAction(dropAction);
// accept or reject
if (dropAction != DropAction.Cancel) {
dtde.acceptDrag(DnDConstants.ACTION_COPY);
} else {
dtde.rejectDrag();
}
}
public void dragExit(DropTargetEvent dte) {
// reset to default state
setDropAction(DropAction.Download);
};
@Override
public void drop(DropTargetDropEvent dtde) {
dtde.acceptDrop(DnDConstants.ACTION_REFERENCE);
try {
dtde.dropComplete(handleDrop(getFilesFromTransferable(dtde.getTransferable())));
} catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e);
}
// reset to default state
dragExit(dtde);
}
};
private final ActionListener clickHandler = new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
JFileChooser chooser = new JFileChooser();
chooser.setMultiSelectionEnabled(true);
// collect media file extensions (video and subtitle files)
List<String> extensions = new ArrayList<String>();
Collections.addAll(extensions, VIDEO_FILES.extensions());
Collections.addAll(extensions, SUBTITLE_FILES.extensions());
chooser.setFileFilter(new FileNameExtensionFilter("Media files", extensions.toArray(new String[0])));
if (chooser.showOpenDialog(getWindow(evt.getSource())) == JFileChooser.APPROVE_OPTION) {
List<File> files = Arrays.asList(chooser.getSelectedFiles());
if (getDropAction(files) != DropAction.Cancel) {
handleDrop(Arrays.asList(chooser.getSelectedFiles()));
}
}
}
};
}

View File

@ -2,10 +2,14 @@
package net.sourceforge.filebot.ui.panel.subtitle;
import static net.sourceforge.filebot.Settings.*;
import static net.sourceforge.filebot.ui.panel.subtitle.LanguageComboBoxModel.*;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ItemEvent;
import java.awt.geom.Path2D;
import java.net.URI;
import java.util.AbstractList;
import java.util.ArrayList;
@ -19,13 +23,10 @@ import javax.swing.JComboBox;
import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.ui.AbstractSearchPanel;
import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.web.OpenSubtitlesClient;
import net.sourceforge.filebot.web.SearchResult;
import net.sourceforge.filebot.web.SublightSubtitleClient;
import net.sourceforge.filebot.web.SubsceneSubtitleClient;
import net.sourceforge.filebot.web.SubtitleDescriptor;
import net.sourceforge.filebot.web.SubtitleProvider;
import net.sourceforge.filebot.web.SubtitleSourceClient;
import net.sourceforge.filebot.web.VideoHashSubtitleService;
import net.sourceforge.tuned.PreferencesList;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
import net.sourceforge.tuned.ui.LabelProvider;
@ -92,22 +93,60 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
// add after text field
add(languageComboBox, 1);
// add at the top right corner
add(dropTarget, "width 1.6cm, height 1.2cm, pos n 0% 100% n", 0);
}
private final SubtitleDropTarget dropTarget = new SubtitleDropTarget() {
@Override
public VideoHashSubtitleService[] getServices() {
return SubtitleServices.getVideoHashSubtitleServices();
}
@Override
protected SubtitleProvider[] createSearchEngines() {
return new SubtitleProvider[] {
new OpenSubtitlesClient(String.format("%s %s", getApplicationName(), getApplicationVersion())),
new SubsceneSubtitleClient(),
new SublightSubtitleClient(getApplicationName(), getApplicationProperty("sublight.apikey")),
new SubtitleSourceClient()
public String getQueryLanguage() {
// use currently selected language for drop target
return languageModel.getSelectedItem() == ALL_LANGUAGES ? null : languageModel.getSelectedItem().getName();
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Path2D path = new Path2D.Float();
path.moveTo(0, 0);
path.lineTo(0, getHeight() - 1 - 12);
path.quadTo(0, getHeight() - 1, 12, getHeight() - 1);
path.lineTo(getWidth(), getHeight() - 1);
path.lineTo(getWidth(), 0);
g2d.setPaint(getBackground());
g2d.fill(path);
g2d.setPaint(Color.gray);
g2d.draw(path);
g2d.translate(2, 0);
super.paintComponent(g2d);
g2d.dispose();
}
};
@Override
protected SubtitleProvider[] getSearchEngines() {
return SubtitleServices.getSubtitleProviders();
}
@Override
protected LabelProvider<SubtitleProvider> createSearchEngineLabelProvider() {
protected LabelProvider<SubtitleProvider> getSearchEngineLabelProvider() {
return SimpleLabelProvider.forClass(SubtitleProvider.class);
}

View File

@ -0,0 +1,41 @@
package net.sourceforge.filebot.ui.panel.subtitle;
import static net.sourceforge.filebot.Settings.*;
import net.sourceforge.filebot.web.OpenSubtitlesClient;
import net.sourceforge.filebot.web.SublightSubtitleClient;
import net.sourceforge.filebot.web.SubsceneSubtitleClient;
import net.sourceforge.filebot.web.SubtitleProvider;
import net.sourceforge.filebot.web.SubtitleSourceClient;
import net.sourceforge.filebot.web.VideoHashSubtitleService;
final class SubtitleServices {
public static final OpenSubtitlesClient openSubtitlesClient = new OpenSubtitlesClient(String.format("%s %s", getApplicationName(), getApplicationVersion()));
public static final SublightSubtitleClient sublightSubtitleClient = new SublightSubtitleClient(getApplicationName(), getApplicationProperty("sublight.apikey"));
public static final SubsceneSubtitleClient subsceneSubtitleClient = new SubsceneSubtitleClient();
public static final SubtitleSourceClient subtitleSourceClient = new SubtitleSourceClient();
public static SubtitleProvider[] getSubtitleProviders() {
return new SubtitleProvider[] { openSubtitlesClient, subsceneSubtitleClient, sublightSubtitleClient, subtitleSourceClient };
}
public static VideoHashSubtitleService[] getVideoHashSubtitleServices() {
return new VideoHashSubtitleService[] { openSubtitlesClient, sublightSubtitleClient };
}
/**
* Dummy constructor to prevent instantiation.
*/
private SubtitleServices() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,804 @@
package net.sourceforge.filebot.ui.panel.subtitle;
import static javax.swing.BorderFactory.*;
import static javax.swing.JOptionPane.*;
import static net.sourceforge.filebot.ui.panel.subtitle.SubtitleUtilities.*;
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.nio.ByteBuffer;
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.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListSelectionModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.SwingWorker.StateValue;
import javax.swing.border.Border;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.web.SubtitleDescriptor;
import net.sourceforge.filebot.web.VideoHashSubtitleService;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.ui.AbstractBean;
import net.sourceforge.tuned.ui.LinkButton;
import net.sourceforge.tuned.ui.RoundBorder;
class VideoHashSubtitleDownloadDialog extends JDialog {
private final JPanel servicePanel = new JPanel(new MigLayout());
private final List<VideoHashSubtitleServiceBean> services = new ArrayList<VideoHashSubtitleServiceBean>();
private final JTable subtitleMappingTable = createTable();
private ExecutorService downloadService;
public VideoHashSubtitleDownloadDialog(Window owner) {
super(owner, "Download Subtitles", ModalityType.MODELESS);
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(new JButton(downloadAction), "tag ok");
content.add(new JButton(finishAction), "tag cancel");
}
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();
editor.setRenderer(new SubtitleOptionRenderer());
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;
editor.setModel(new DefaultComboBoxModel(mapping.getOptions()));
editor.setSelectedItem(mapping.getSelectedOption());
return editor;
}
});
// disable selection
table.setSelectionModel(new DefaultListSelectionModel() {
@Override
public void addSelectionInterval(int from, int to) {
// ignore
}
@Override
public void setSelectionInterval(int from, int to) {
// ignore
}
@Override
public void setAnchorSelectionIndex(int anchorIndex) {
// ignore
}
@Override
public void setLeadSelectionIndex(int leadIndex) {
// ignore
}
});
return table;
}
public void setVideoFiles(File[] videoFiles) {
subtitleMappingTable.setModel(new SubtitleMappingTableModel(videoFiles));
}
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());
serviceBean.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (serviceBean.getState() == StateValue.STARTED) {
component.setIcon(ResourceManager.getIcon("database.go"));
} else {
component.setIcon(ResourceManager.getIcon(serviceBean.getError() == null ? "database.ok" : "database.error"));
}
component.setToolTipText(serviceBean.getError() == null ? null : serviceBean.getError().getMessage());
}
});
services.add(serviceBean);
servicePanel.add(component);
}
public void startQuery(String languageName) {
final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel();
// query services concurrently
for (VideoHashSubtitleServiceBean service : services) {
QueryTask task = new QueryTask(service, mappingModel.getVideoFiles(), languageName) {
@Override
protected void done() {
try {
Map<File, List<SubtitleDescriptorBean>> subtitles = get();
// 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
mappingModel.setOptionColumnVisible(true);
} catch (Exception e) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
}
}
};
// start background worker
task.execute();
}
}
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(VideoHashSubtitleDownloadDialog.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) {
// 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();
// collect the subtitles that will be fetched
List<DownloadTask> downloadQueue = new ArrayList<DownloadTask>();
for (SubtitleMapping mapping : mappingModel) {
SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption();
if (subtitleBean != null && subtitleBean.getState() == null) {
downloadQueue.add(new DownloadTask(subtitleBean, mapping.getSubtitleFile()));
}
}
// collect downloads that will override a file
List<DownloadTask> confirmReplaceDownloadQueue = new ArrayList<DownloadTask>();
List<String> existingFiles = new ArrayList<String>();
for (DownloadTask download : downloadQueue) {
if (download.getDestination().exists()) {
confirmReplaceDownloadQueue.add(download);
existingFiles.add(download.getDestination().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 (downloadService != null) {
downloadService.shutdownNow();
}
setVisible(false);
dispose();
}
};
private static class SubtitleMappingOptionRenderer extends DefaultTableCellRenderer {
private final JComboBox optionComboBox = new SimpleComboBox();
public SubtitleMappingOptionRenderer() {
optionComboBox.setRenderer(new SubtitleOptionRenderer());
}
@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()) {
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) {
// no subtitles found
setText("No subtitles found");
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 {
// download complete
setText(mapping.getSubtitleFile().getName());
setIcon(ResourceManager.getIcon("status.ok"));
}
return this;
}
}
private static class SubtitleOptionRenderer extends DefaultListCellRenderer {
private final Border padding = createEmptyBorder(3, 3, 3, 3);
@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;
setText(subtitleBean.getText());
setIcon(subtitleBean.getError() == null ? subtitleBean.getIcon() : ResourceManager.getIcon("status.warning"));
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) {
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
if (optionColumnVisible) {
fireTableCellUpdated(index, 1);
}
}
}
}
private static class SubtitleMapping extends AbstractBean {
private final File videoFile;
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 new File(videoFile.getParentFile(), FileUtilities.getName(videoFile) + '.' + selectedOption.getType());
}
public boolean isEditable() {
return 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;
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 SubtitleDescriptor subtitle;
private final VideoHashSubtitleServiceBean source;
private StateValue state;
private Exception error;
public SubtitleDescriptorBean(SubtitleDescriptor subtitle, VideoHashSubtitleServiceBean source) {
this.subtitle = subtitle;
this.source = source;
}
public String getText() {
return subtitle.getName() + '.' + subtitle.getType();
}
public Icon getIcon() {
return source.getIcon();
}
public String getType() {
return subtitle.getType();
}
public ByteBuffer fetch() throws Exception {
setState(StateValue.STARTED);
try {
return subtitle.fetch();
} catch (Exception e) {
// remember exception
error = e;
// rethrow exception
throw 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<Map<File, List<SubtitleDescriptorBean>>, Void> {
private final VideoHashSubtitleServiceBean service;
private final File[] videoFiles;
private final String languageName;
public QueryTask(VideoHashSubtitleServiceBean service, Collection<File> videoFiles, String languageName) {
this.service = service;
this.videoFiles = videoFiles.toArray(new File[0]);
this.languageName = languageName;
}
@Override
protected Map<File, List<SubtitleDescriptorBean>> doInBackground() throws Exception {
Map<File, List<SubtitleDescriptorBean>> subtitleSet = new HashMap<File, List<SubtitleDescriptorBean>>();
for (final Entry<File, List<SubtitleDescriptor>> result : service.getSubtitleList(videoFiles, 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);
}
return subtitleSet;
}
}
private static class DownloadTask extends SwingWorker<File, Void> {
private final SubtitleDescriptorBean subtitle;
private final File destination;
public DownloadTask(SubtitleDescriptorBean subtitle, File destination) {
this.subtitle = subtitle;
this.destination = destination;
}
public SubtitleDescriptorBean getSubtitleBean() {
return subtitle;
}
public File getDestination() {
return destination;
}
@Override
protected File doInBackground() {
try {
// fetch subtitle
ByteBuffer data = subtitle.fetch();
if (isCancelled())
return null;
// save to file
write(data, destination);
return destination;
} catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e);
}
return null;
}
}
private static class VideoHashSubtitleServiceBean extends AbstractBean {
private final VideoHashSubtitleService service;
private StateValue state;
private Throwable error;
public VideoHashSubtitleServiceBean(VideoHashSubtitleService service) {
this.service = service;
this.state = StateValue.PENDING;
}
public String getName() {
return service.getName();
}
public Icon getIcon() {
return service.getIcon();
}
public URI getLink() {
return service.getLink();
}
public Map<File, List<SubtitleDescriptor>> getSubtitleList(File[] files, String languageName) throws Exception {
setState(StateValue.STARTED);
try {
return service.getSubtitleList(files, languageName);
} catch (Exception e) {
// remember error
error = e;
// rethrow error
throw 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;
}
}
}

View File

@ -44,6 +44,12 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
}
@Override
public URI getLink() {
return URI.create("http://www.opensubtitles.org");
}
@Override
public Icon getIcon() {
return ResourceManager.getIcon("search.opensubtitles");

View File

@ -58,6 +58,12 @@ public class SublightSubtitleClient implements SubtitleProvider, VideoHashSubtit
}
@Override
public URI getLink() {
return URI.create("http://www.sublight.si");
}
@Override
public Icon getIcon() {
return ResourceManager.getIcon("search.sublight");
@ -131,7 +137,7 @@ public class SublightSubtitleClient implements SubtitleProvider, VideoHashSubtit
}
} catch (LinkageError e) {
// MediaInfo native lib not available
throw new UnsupportedOperationException(e);
throw new UnsupportedOperationException(e.getMessage(), e);
}
return subtitles;
@ -292,7 +298,8 @@ public class SublightSubtitleClient implements SubtitleProvider, VideoHashSubtit
@Override
public URI getSubtitleListLink(SearchResult searchResult, String languageName) {
return null;
// note that sublight can only be accessed via the soap API
return URI.create("http://www.sublight.si/SearchSubtitles.aspx");
}

View File

@ -44,6 +44,12 @@ public class SubsceneSubtitleClient implements SubtitleProvider {
}
@Override
public URI getLink() {
return URI.create("http://subscene.com");
}
@Override
public Icon getIcon() {
return ResourceManager.getIcon("search.subscene");

View File

@ -22,6 +22,9 @@ public interface SubtitleProvider {
public String getName();
public URI getLink();
public Icon getIcon();
}

View File

@ -15,11 +15,11 @@ import java.util.Map;
import javax.swing.Icon;
import net.sourceforge.filebot.ResourceManager;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import net.sourceforge.filebot.ResourceManager;
public class SubtitleSourceClient implements SubtitleProvider {
@ -34,6 +34,12 @@ public class SubtitleSourceClient implements SubtitleProvider {
}
@Override
public URI getLink() {
return URI.create("http://www.subtitlesource.org");
}
@Override
public Icon getIcon() {
return ResourceManager.getIcon("search.subtitlesource");

View File

@ -3,9 +3,12 @@ package net.sourceforge.filebot.web;
import java.io.File;
import java.net.URI;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
public interface VideoHashSubtitleService {
@ -14,4 +17,13 @@ public interface VideoHashSubtitleService {
public boolean publishSubtitle(int imdbid, String languageName, File videoFile, File subtitleFile) throws Exception;
public String getName();
public URI getLink();
public Icon getIcon();
}

View File

@ -140,6 +140,32 @@ public final class FileUtilities {
}
public static List<File> listFiles(Iterable<File> folders, int maxDepth) {
List<File> files = new ArrayList<File>();
// collect files from directory tree
for (File folder : folders) {
listFiles(folder, 0, files, maxDepth);
}
return files;
}
private static void listFiles(File folder, int depth, List<File> files, int maxDepth) {
if (depth > maxDepth)
return;
for (File file : folder.listFiles()) {
if (file.isDirectory()) {
listFiles(file, depth + 1, files, maxDepth);
} else {
files.add(file);
}
}
}
/**
* Invalid filename characters: \, /, :, *, ?, ", <, >, |, \r and \n
*/

View File

@ -0,0 +1,46 @@
package net.sourceforge.tuned.ui;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import javax.swing.event.SwingPropertyChangeSupport;
public abstract class AbstractBean {
private final PropertyChangeSupport pcs;
public AbstractBean() {
pcs = new SwingPropertyChangeSupport(this, true);
}
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
pcs.firePropertyChange(propertyName, oldValue, newValue);
}
protected void firePropertyChange(PropertyChangeEvent e) {
pcs.firePropertyChange(e);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return pcs.getPropertyChangeListeners();
}
}

View File

@ -0,0 +1,73 @@
package net.sourceforge.tuned.ui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import javax.swing.border.AbstractBorder;
public class RoundBorder extends AbstractBorder {
private final Color color;
private final Insets insets;
private final int arc;
public RoundBorder() {
this.color = new Color(0xACACAC);
this.arc = 12;
this.insets = new Insets(1, 1, 1, 1);
}
public RoundBorder(Color color, int arc, Insets insets) {
this.color = color;
this.arc = arc;
this.insets = insets;
}
@Override
public boolean isBorderOpaque() {
return false;
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setPaint(c.getBackground());
g2d.fillRoundRect(x, y, width - 1, height - 1, arc, arc);
g2d.setPaint(color);
g2d.drawRoundRect(x, y, width - 1, height - 1, arc, arc);
g2d.dispose();
}
@Override
public Insets getBorderInsets(Component c) {
return new Insets(insets.top, insets.left, insets.bottom, insets.right);
}
@Override
public Insets getBorderInsets(Component c, Insets insets) {
insets.top = this.insets.top;
insets.left = this.insets.left;
insets.bottom = this.insets.bottom;
insets.right = this.insets.right;
return insets;
}
}