+ Subtitle Upload Support !!!

This commit is contained in:
Reinhard Pointner 2013-09-21 07:29:57 +00:00
parent fae437f780
commit 2fa1ca6dc2
18 changed files with 1876 additions and 1386 deletions

View File

@ -57,6 +57,7 @@ import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.Movie;
import net.sourceforge.filebot.web.MovieIdentificationService;
import net.sourceforge.filebot.web.SearchResult;
import net.sourceforge.filebot.web.TMDbClient.MovieInfo;
import net.sourceforge.filebot.web.TheTVDBClient.SeriesInfo;
import net.sourceforge.filebot.web.TheTVDBSearchResult;
@ -106,6 +107,10 @@ public class MediaDetection {
}
}
public static Locale guessLanguageFromSuffix(File file) {
return releaseInfo.getLanguageSuffix(getName(file));
}
public static boolean isEpisode(String name, boolean strict) {
return parseEpisodeNumber(name, strict) != null || parseDate(name) != null;
}
@ -965,6 +970,14 @@ public class MediaDetection {
return WebServices.TheTVDB.getSeriesInfoByID(grepTheTvdbId(new String(readFile(nfo), "UTF-8")).iterator().next(), locale);
}
public static Movie tmdb2imdb(Movie m) throws IOException {
if (m.getTmdbId() <= 0 && m.getImdbId() <= 0)
throw new IllegalArgumentException();
MovieInfo info = WebServices.TMDb.getMovieInfo(m, Locale.ENGLISH);
return new Movie(info.getName(), info.getReleased().getYear(), info.getImdbId(), info.getId());
}
/*
* Heavy-duty name matcher used for matching a file to or more movies (out of a list of ~50k)
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 B

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.ui;
import static javax.swing.ScrollPaneConstants.*;
import static net.sourceforge.filebot.ui.NotificationLogging.*;
import static net.sourceforge.tuned.ui.TunedUtilities.*;
@ -44,40 +42,38 @@ import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.matchers.TextMatcherEditor;
import ca.odell.glazedlists.swing.AutoCompleteSupport;
public abstract class AbstractSearchPanel<S, E> extends JComponent {
protected final JPanel tabbedPaneGroup = new JPanel(new MigLayout("nogrid, fill, insets 0", "align center", "[fill]8px[pref!]4px"));
protected final JTabbedPane tabbedPane = new JTabbedPane();
protected final HistoryPanel historyPanel = new HistoryPanel();
protected final SelectButtonTextField<S> searchTextField = new SelectButtonTextField<S>();
protected final EventList<String> searchHistory = createSearchHistory();
public AbstractSearchPanel() {
historyPanel.setColumnHeader(2, "Duration");
JScrollPane historyScrollPane = new JScrollPane(historyPanel, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_NEVER);
historyScrollPane.setBorder(BorderFactory.createEmptyBorder());
tabbedPane.addTab("History", ResourceManager.getIcon("action.find"), historyScrollPane);
tabbedPaneGroup.setBorder(BorderFactory.createTitledBorder("Search Results"));
tabbedPaneGroup.add(tabbedPane, "grow, wrap");
setLayout(new MigLayout("nogrid, fill, insets 10px 10px 15px 10px", "align center", "[pref!]10px[fill]"));
setLayout(new MigLayout("nogrid, fill, insets 10px 10px 15px 10px", "align 45%", "[pref!]10px[fill]"));
add(searchTextField);
add(new JButton(searchAction), "gap 18px, id search");
add(new JButton(searchAction), "gap 16px, id search, sgy button");
add(tabbedPaneGroup, "newline, grow");
searchTextField.getEditor().setAction(searchAction);
searchTextField.getSelectButton().setModel(Arrays.asList(getSearchEngines()));
searchTextField.getSelectButton().setLabelProvider(getSearchEngineLabelProvider());
try {
// restore selected subtitle client
searchTextField.getSelectButton().setSelectedIndex(Integer.parseInt(getSettings().get("engine.selected", "0")));
@ -85,195 +81,181 @@ public abstract class AbstractSearchPanel<S, E> extends JComponent {
// log and ignore
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
}
// save selected client on change
searchTextField.getSelectButton().getSelectionModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
getSettings().put("engine.selected", Integer.toString(searchTextField.getSelectButton().getSelectedIndex()));
}
});
AutoCompleteSupport.install(searchTextField.getEditor(), searchHistory).setFilterMode(TextMatcherEditor.CONTAINS);
installAction(this, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), searchAction);
}
protected abstract S[] getSearchEngines();
protected abstract LabelProvider<S> getSearchEngineLabelProvider();
protected abstract Settings getSettings();
protected abstract RequestProcessor<?, E> createRequestProcessor();
private void search(RequestProcessor<?, E> requestProcessor) {
FileBotTab<?> tab = requestProcessor.tab;
tab.setTitle(requestProcessor.getTitle());
tab.setLoading(true);
tab.setIcon(requestProcessor.getIcon());
tab.addTo(tabbedPane);
tabbedPane.setSelectedComponent(tab);
// search in background
new SearchTask(requestProcessor).execute();
}
protected EventList<String> createSearchHistory() {
// create in-memory history
BasicEventList<String> history = new BasicEventList<String>();
// get the preferences node that contains the history entries
// and get a StringList that read and writes directly from and to the preferences
// get the preferences node that contains the history entries
// and get a StringList that read and writes directly from and to the preferences
List<String> persistentHistory = getSettings().node("history").asList();
// add history from the preferences to the current in-memory history (for completion)
history.addAll(persistentHistory);
// perform all insert/add/remove operations on the in-memory history on the preferences node as well
// perform all insert/add/remove operations on the in-memory history on the preferences node as well
ListChangeSynchronizer.syncEventListToList(history, persistentHistory);
return history;
}
private final AbstractAction searchAction = new AbstractAction("Find", ResourceManager.getIcon("action.find")) {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand() == null) {
// command triggered by auto-completion
return;
}
search(createRequestProcessor());
}
};
private class SearchTask extends SwingWorker<Collection<? extends SearchResult>, Void> {
private final RequestProcessor<?, E> requestProcessor;
public SearchTask(RequestProcessor<?, E> requestProcessor) {
this.requestProcessor = requestProcessor;
}
@Override
protected Collection<? extends SearchResult> doInBackground() throws Exception {
long start = System.currentTimeMillis();
try {
return requestProcessor.search();
} finally {
requestProcessor.duration += (System.currentTimeMillis() - start);
}
}
@Override
public void done() {
FileBotTab<?> tab = requestProcessor.tab;
// tab might have been closed
if (tab.isClosed())
return;
try {
Collection<? extends SearchResult> results = get();
SearchResult selectedSearchResult = null;
switch (results.size()) {
case 0:
UILogger.log(Level.WARNING, String.format("'%s' has not been found.", requestProcessor.request.getSearchText()));
break;
case 1:
selectedSearchResult = results.iterator().next();
break;
default:
selectedSearchResult = requestProcessor.selectSearchResult(results, SwingUtilities.getWindowAncestor(AbstractSearchPanel.this));
break;
case 0:
UILogger.log(Level.WARNING, String.format("'%s' has not been found.", requestProcessor.request.getSearchText()));
break;
case 1:
selectedSearchResult = results.iterator().next();
break;
default:
selectedSearchResult = requestProcessor.selectSearchResult(results, SwingUtilities.getWindowAncestor(AbstractSearchPanel.this));
break;
}
if (selectedSearchResult == null) {
tab.close();
return;
}
// set search result
requestProcessor.setSearchResult(selectedSearchResult);
String historyEntry = requestProcessor.getHistoryEntry();
if (historyEntry != null && !searchHistory.contains(historyEntry)) {
searchHistory.add(historyEntry);
}
tab.setTitle(requestProcessor.getTitle());
// fetch elements of the selected search result
new FetchTask(requestProcessor).execute();
} catch (Exception e) {
tab.close();
UILogger.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
}
}
private class FetchTask extends SwingWorker<Collection<E>, Void> {
private final RequestProcessor<?, E> requestProcessor;
public FetchTask(RequestProcessor<?, E> requestProcessor) {
this.requestProcessor = requestProcessor;
}
@Override
protected final Collection<E> doInBackground() throws Exception {
long start = System.currentTimeMillis();
try {
return requestProcessor.fetch();
} finally {
requestProcessor.duration += (System.currentTimeMillis() - start);
}
}
@Override
public void done() {
FileBotTab<?> tab = requestProcessor.tab;
if (tab.isClosed())
return;
try {
// check if an exception occurred
Collection<E> elements = get();
requestProcessor.process(elements);
String title = requestProcessor.getTitle();
Icon icon = requestProcessor.getIcon();
String statusMessage = requestProcessor.getStatusMessage(elements);
historyPanel.add(title, requestProcessor.getLink(), icon, statusMessage, String.format("%,d ms", requestProcessor.getDuration()));
// close tab if no elements were fetched
if (get().size() <= 0) {
UILogger.warning(statusMessage);
@ -287,119 +269,100 @@ public abstract class AbstractSearchPanel<S, E> extends JComponent {
}
}
}
protected static class Request {
private final String searchText;
public Request(String searchText) {
this.searchText = searchText;
}
public String getSearchText() {
return searchText;
}
}
protected abstract static class RequestProcessor<R extends Request, E> {
protected final R request;
private FileBotTab<JComponent> tab;
private SearchResult searchResult;
private long duration = 0;
public RequestProcessor(R request, JComponent component) {
this.request = request;
this.tab = new FileBotTab<JComponent>(component);
}
public abstract Collection<? extends SearchResult> search() throws Exception;
public abstract Collection<E> fetch() throws Exception;
public abstract void process(Collection<E> elements);
public abstract URI getLink();
public JComponent getComponent() {
return tab.getComponent();
}
public SearchResult getSearchResult() {
return searchResult;
}
public void setSearchResult(SearchResult searchResult) {
this.searchResult = searchResult;
}
public String getStatusMessage(Collection<E> result) {
return String.format("%d elements found", result.size());
}
public String getTitle() {
if (searchResult != null)
return searchResult.getName();
return request.getSearchText();
}
public String getHistoryEntry() {
SeriesNameMatcher nameMatcher = new SeriesNameMatcher();
// the common word sequence of query and search result
// the common word sequence of query and search result
// common name will maintain the exact word characters (incl. case) of the first argument
return nameMatcher.matchByFirstCommonWordSequence(searchResult.getName(), request.getSearchText());
}
public Icon getIcon() {
return null;
}
protected SearchResult selectSearchResult(Collection<? extends SearchResult> searchResults, Window window) throws Exception {
// multiple results have been found, user must select one
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(window, searchResults);
configureSelectDialog(selectDialog);
selectDialog.setVisible(true);
// selected value or null if the dialog was canceled by the user
return selectDialog.getSelectedValue();
}
protected void configureSelectDialog(SelectDialog<SearchResult> selectDialog) {
selectDialog.setLocation(getOffsetLocation(selectDialog.getOwner()));
selectDialog.setIconImage(getImage(getIcon()));
selectDialog.setMinimumSize(new Dimension(250, 150));
}
public long getDuration() {
return duration;
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.ui;
import static java.util.Arrays.*;
import static java.util.Collections.*;
@ -12,58 +10,49 @@ import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
public class Language {
private final String code;
private final String name;
public Language(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
public Locale toLocale() {
return new Locale(getCode());
}
@Override
public Language clone() {
return new Language(code, name);
}
public static final Comparator<Language> ALPHABETIC_ORDER = new Comparator<Language>() {
@Override
public int compare(Language o1, Language o2) {
return o1.name.compareToIgnoreCase(o2.name);
}
};
public static Language getLanguage(String code) {
ResourceBundle bundle = ResourceBundle.getBundle(Language.class.getName());
try {
return new Language(code, bundle.getString(code + ".name"));
} catch (Exception e) {
@ -73,29 +62,30 @@ public class Language {
return new Language(code, new Locale(code).getDisplayLanguage(Locale.ROOT));
}
}
public static List<Language> getLanguages(String... codes) {
Language[] languages = new Language[codes.length];
for (int i = 0; i < codes.length; i++) {
languages[i] = getLanguage(codes[i]);
}
return asList(languages);
}
public static Language getLanguage(Locale locale) {
return locale == null ? null : getLanguageByName(locale.getDisplayLanguage(Locale.ENGLISH));
}
public static Language getLanguageByName(String name) {
for (Language it : availableLanguages()) {
if (name.equalsIgnoreCase(it.getName()))
return it;
}
return null;
}
public static String getISO3LanguageCodeByName(String languageName) {
Language language = Language.getLanguageByName(languageName);
if (language != null) {
@ -105,34 +95,31 @@ public class Language {
return language.getCode();
}
}
return null;
}
public static List<Language> availableLanguages() {
ResourceBundle bundle = ResourceBundle.getBundle(Language.class.getName());
return getLanguages(bundle.getString("languages.all").split(","));
}
public static List<Language> commonLanguages() {
ResourceBundle bundle = ResourceBundle.getBundle(Language.class.getName());
return getLanguages(bundle.getString("languages.common").split(","));
}
public static List<Language> preferredLanguages() {
Set<String> codes = new LinkedHashSet<String>();
// English | System language | common languages
codes.add("en");
codes.add(Locale.getDefault().getLanguage());
ResourceBundle bundle = ResourceBundle.getBundle(Language.class.getName());
addAll(codes, bundle.getString("languages.common").split(","));
return getLanguages(codes.toArray(new String[0]));
}
}

View File

@ -1,69 +1,70 @@
package net.sourceforge.filebot.ui;
import static java.awt.event.ItemEvent.*;
import static net.sourceforge.filebot.ui.Language.*;
import static net.sourceforge.filebot.ui.LanguageComboBoxModel.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.AbstractList;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import net.sourceforge.filebot.Settings;
import net.sourceforge.tuned.PreferencesList;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
public class LanguageComboBox extends JComboBox {
private final PreferencesEntry<String> persistentSelectedLanguage;
private final PreferencesList<String> persistentFavoriteLanguages;
public LanguageComboBox(JComponent parent, Language initialSelection) {
super(new LanguageComboBoxModel(initialSelection != ALL_LANGUAGES, initialSelection));
private Entry<String, String> persistentSelectedLanguage;
private List<String> persistentFavoriteLanguages;
public LanguageComboBox(Language initialSelection, Settings settings) {
super(new LanguageComboBoxModel(initialSelection, initialSelection));
setRenderer(new LanguageComboBoxCellRenderer(super.getRenderer()));
persistentSelectedLanguage = Settings.forPackage(parent.getClass()).entry("language.selected");
persistentFavoriteLanguages = Settings.forPackage(parent.getClass()).node("language.favorites").asList();
if (settings != null) {
persistentSelectedLanguage = settings.entry("language.selected");
persistentFavoriteLanguages = settings.node("language.favorites").asList();
} else {
persistentSelectedLanguage = new SimpleEntry<String, String>(null, null);
persistentFavoriteLanguages = new ArrayList<String>();
}
// restore selected language
getModel().setSelectedItem(Language.getLanguage(persistentSelectedLanguage.getValue()));
// restore favorite languages
for (String favoriteLanguage : persistentFavoriteLanguages) {
getModel().favorites().add(getModel().favorites().size(), getLanguage(favoriteLanguage));
}
// guess favorite languages
if (getModel().favorites().isEmpty()) {
for (Locale locale : new Locale[] { Locale.getDefault(), Locale.ENGLISH }) {
getModel().favorites().add(getLanguage(locale.getLanguage()));
}
}
// update favorites on change
addPopupMenuListener(new PopupSelectionListener() {
@Override
public void itemStateChanged(ItemEvent e) {
Language language = (Language) e.getItem();
if (getModel().favorites().add(language)) {
persistentFavoriteLanguages.set(new AbstractList<String>() {
persistentFavoriteLanguages.clear();
persistentFavoriteLanguages.addAll(new AbstractList<String>() {
@Override
public String get(int index) {
return getModel().favorites().get(index).getCode();
}
@Override
public int size() {
@ -71,56 +72,50 @@ public class LanguageComboBox extends JComboBox {
}
});
}
persistentSelectedLanguage.setValue(language.getCode());
}
});
}
@Override
public LanguageComboBoxModel getModel() {
return (LanguageComboBoxModel) super.getModel();
}
private static class PopupSelectionListener implements PopupMenuListener, ItemListener {
private Object selected = null;
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
JComboBox comboBox = (JComboBox) e.getSource();
// selected item before popup
selected = comboBox.getSelectedItem();
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
JComboBox comboBox = (JComboBox) e.getSource();
// check selected item after popup
if (selected != comboBox.getSelectedItem()) {
itemStateChanged(new ItemEvent(comboBox, ITEM_STATE_CHANGED, comboBox.getSelectedItem(), SELECTED));
}
selected = null;
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
selected = null;
}
@Override
public void itemStateChanged(ItemEvent e) {
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.ui;
import static net.sourceforge.filebot.ui.Language.*;
import java.util.AbstractList;
@ -12,65 +10,56 @@ import java.util.Set;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
public class LanguageComboBoxModel extends AbstractListModel implements ComboBoxModel {
public static final Language ALL_LANGUAGES = new Language("undefined", "All Languages");
private boolean requireSpecificLanguage;
private Language defaultLanguage;
private Language selection;
private List<Language> favorites = new Favorites(2);
private List<Language> values = availableLanguages();
public LanguageComboBoxModel(boolean requireSpecificLanguage, Language initialSelection) {
this.requireSpecificLanguage = requireSpecificLanguage;
public LanguageComboBoxModel(Language defaultLanguage, Language initialSelection) {
this.defaultLanguage = defaultLanguage;
this.selection = initialSelection;
}
@Override
public Language getElementAt(int index) {
// "All Languages"
if (!requireSpecificLanguage) {
if (index == 0)
return ALL_LANGUAGES;
// "All Languages" offset
index -= 1;
}
if (index == 0)
return defaultLanguage;
// "All Languages" offset
index -= 1;
if (index < favorites.size()) {
return favorites.get(index);
}
// "Favorites" offset
index -= favorites.size();
return values.get(index);
}
@Override
public int getSize() {
// "All Languages" : favorites[] : values[]
return (requireSpecificLanguage ? 0 : 1) + favorites.size() + values.size();
return 1 + favorites.size() + values.size();
}
public List<Language> favorites() {
return favorites;
}
@Override
public Language getSelectedItem() {
return selection;
}
@Override
public void setSelectedItem(Object value) {
if (value instanceof Language) {
@ -78,110 +67,98 @@ public class LanguageComboBoxModel extends AbstractListModel implements ComboBox
selection = ALL_LANGUAGES.getCode().equals(language.getCode()) ? ALL_LANGUAGES : language;
}
}
protected int convertFavoriteIndexToModel(int favoriteIndex) {
return (requireSpecificLanguage ? 0 : 1) + favoriteIndex;
return 1 + favoriteIndex;
}
protected void fireFavoritesAdded(int from, int to) {
fireIntervalAdded(this, convertFavoriteIndexToModel(from), convertFavoriteIndexToModel(to));
}
protected void fireFavoritesRemoved(int from, int to) {
fireIntervalRemoved(this, convertFavoriteIndexToModel(from), convertFavoriteIndexToModel(to));
}
private class Favorites extends AbstractList<Language> implements Set<Language> {
private final List<Language> data;
private final int capacity;
public Favorites(int capacity) {
this.data = new ArrayList<Language>(capacity);
this.capacity = capacity;
}
@Override
public Language get(int index) {
return data.get(index);
}
public boolean add(Language element) {
// add first
return addIfAbsent(0, element);
}
public void add(int index, Language element) {
addIfAbsent(index, element);
}
public boolean addIfAbsent(int index, Language element) {
// 1. ignore null values
// 2. ignore ALL_LANGUAGES
// 3. make sure there are no duplicates
// 4. limit size to capacity
if (element == null || element == ALL_LANGUAGES || contains(element) || index >= capacity) {
if (element == null || element == ALL_LANGUAGES || element.getCode().equals(defaultLanguage.getCode()) || contains(element) || index >= capacity) {
return false;
}
// make sure there is free space
if (data.size() >= capacity) {
// remove last
remove(data.size() - 1);
}
// add clone of language, because KeySelection behaviour will
// get weird if the same object is in the model multiple times
data.add(index, element.clone());
// update view
fireFavoritesAdded(index, index);
return true;
}
@Override
public boolean contains(Object obj) {
// check via language code, because data consists of clones
if (obj instanceof Language) {
Language language = (Language) obj;
for (Language element : data) {
if (language.getCode().equals(element.getCode()))
return true;
}
}
return false;
}
@Override
public Language remove(int index) {
Language old = data.remove(index);
// update view
fireFavoritesRemoved(index, index);
return old;
}
@Override
public int size() {
return data.size();
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.ui.episodelist;
import static net.sourceforge.filebot.ui.episodelist.SeasonSpinnerModel.*;
import static net.sourceforge.filebot.web.EpisodeUtilities.*;
@ -54,57 +52,51 @@ import net.sourceforge.tuned.ui.SelectButton;
import net.sourceforge.tuned.ui.SimpleLabelProvider;
import net.sourceforge.tuned.ui.TunedUtilities;
public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, Episode> {
private SeasonSpinnerModel seasonSpinnerModel = new SeasonSpinnerModel();
private LanguageComboBox languageComboBox = new LanguageComboBox(this, Language.getLanguage("en"));
private LanguageComboBox languageComboBox = new LanguageComboBox(Language.getLanguage("en"), getSettings());
private JComboBox sortOrderComboBox = new JComboBox(SortOrder.values());
public EpisodeListPanel() {
historyPanel.setColumnHeader(0, "Show");
historyPanel.setColumnHeader(1, "Number of Episodes");
JSpinner seasonSpinner = new JSpinner(seasonSpinnerModel);
seasonSpinner.setEditor(new SeasonSpinnerEditor(seasonSpinner));
// set minimum size to "All Seasons" preferred size
seasonSpinner.setMinimumSize(seasonSpinner.getPreferredSize());
// add after text field
add(seasonSpinner, "sgy combo, gap indent", 1);
add(sortOrderComboBox, "sgy combo, gap rel", 2);
add(languageComboBox, "sgy combo, gap indent+5", 3);
add(seasonSpinner, "sgy button, gap indent", 1);
add(sortOrderComboBox, "sgy button, gap rel", 2);
add(languageComboBox, "sgy button, gap indent+5", 3);
// add after tabbed pane
tabbedPaneGroup.add(new JButton(new SaveAction(new SelectedTabExportHandler())));
searchTextField.getSelectButton().addPropertyChangeListener(SelectButton.SELECTED_VALUE, selectButtonListener);
TunedUtilities.installAction(this, KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_MASK), new SpinSeasonAction(1));
TunedUtilities.installAction(this, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.SHIFT_MASK), new SpinSeasonAction(-1));
}
@Override
protected EpisodeListProvider[] getSearchEngines() {
return WebServices.getEpisodeListProviders();
}
@Override
protected LabelProvider<EpisodeListProvider> getSearchEngineLabelProvider() {
return SimpleLabelProvider.forClass(EpisodeListProvider.class);
}
@Override
protected Settings getSettings() {
return Settings.forPackage(EpisodeListPanel.class);
}
@Override
protected EpisodeListRequestProcessor createRequestProcessor() {
EpisodeListProvider provider = searchTextField.getSelectButton().getSelectedValue();
@ -112,16 +104,16 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
int season = seasonSpinnerModel.getSeason();
SortOrder order = (SortOrder) sortOrderComboBox.getSelectedItem();
Locale language = languageComboBox.getModel().getSelectedItem().toLocale();
return new EpisodeListRequestProcessor(new EpisodeListRequest(provider, text, season, order, language));
};
private final PropertyChangeListener selectButtonListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
EpisodeListProvider provider = searchTextField.getSelectButton().getSelectedValue();
// lock season spinner on "All Seasons" if provider doesn't support fetching of single seasons
if (!provider.hasSingleSeasonSupport()) {
seasonSpinnerModel.lock(ALL_SEASONS);
@ -130,25 +122,22 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
}
}
};
private class SpinSeasonAction extends AbstractAction {
public SpinSeasonAction(int spin) {
super(String.format("Spin%+d", spin));
putValue("spin", spin);
}
@Override
public void actionPerformed(ActionEvent e) {
seasonSpinnerModel.spin((Integer) getValue("spin"));
}
}
private class SelectedTabExportHandler implements FileExportHandler {
/**
* @return the <code>FileExportHandler</code> of the currently selected tab
*/
@ -162,41 +151,36 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
return null;
}
}
@Override
public boolean canExport() {
FileExportHandler handler = getExportHandler();
if (handler == null)
return false;
return handler.canExport();
}
@Override
public void export(File file) throws IOException {
getExportHandler().export(file);
}
@Override
public String getDefaultFileName() {
return getExportHandler().getDefaultFileName();
}
}
protected static class EpisodeListRequest extends Request {
public final EpisodeListProvider provider;
public final int season;
public final SortOrder order;
public final Locale language;
public EpisodeListRequest(EpisodeListProvider provider, String searchText, int season, SortOrder order, Locale language) {
super(searchText);
this.provider = provider;
@ -205,25 +189,22 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
this.language = language;
}
}
protected static class EpisodeListRequestProcessor extends RequestProcessor<EpisodeListRequest, Episode> {
public EpisodeListRequestProcessor(EpisodeListRequest request) {
super(request, new EpisodeListTab());
}
@Override
public Collection<SearchResult> search() throws Exception {
return request.provider.search(request.getSearchText(), request.language);
}
@Override
public Collection<Episode> fetch() throws Exception {
List<Episode> episodes = request.provider.getEpisodeList(getSearchResult(), request.order, request.language);
if (request.season != ALL_SEASONS) {
List<Episode> episodeForSeason = filterBySeason(episodes, request.season);
if (episodeForSeason.isEmpty()) {
@ -231,108 +212,97 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
}
episodes = episodeForSeason;
}
Analytics.trackEvent(request.provider.getName(), "ViewEpisodeList", getSearchResult().getName());
return episodes;
}
@Override
public URI getLink() {
return request.provider.getEpisodeListLink(getSearchResult());
}
@Override
public void process(Collection<Episode> episodes) {
// set a proper title for the export handler before adding episodes
getComponent().setTitle(getTitle());
getComponent().getModel().addAll(episodes);
}
@Override
public String getStatusMessage(Collection<Episode> result) {
return (result.isEmpty()) ? "No episodes found" : String.format("%d episodes", result.size());
}
@Override
public EpisodeListTab getComponent() {
return (EpisodeListTab) super.getComponent();
}
@Override
public String getTitle() {
if (request.season == ALL_SEASONS)
return super.getTitle();
// add additional information to default title
return String.format("%s - Season %d", super.getTitle(), request.season);
}
@Override
public Icon getIcon() {
return request.provider.getIcon();
}
@Override
protected void configureSelectDialog(SelectDialog<SearchResult> selectDialog) {
super.configureSelectDialog(selectDialog);
selectDialog.getHeaderLabel().setText("Select a Show:");
}
}
protected static class EpisodeListTab extends FileBotList<Episode> {
public EpisodeListTab() {
// initialize dnd and clipboard export handler for episode list
setExportHandler(new EpisodeListExportHandler(this));
getTransferHandler().setClipboardHandler(new EpisodeListExportHandler(this));
// allow removal of episode list entries
getRemoveAction().setEnabled(true);
// remove borders
listScrollPane.setBorder(null);
setBorder(null);
}
}
protected static class EpisodeListExportHandler extends FileBotListExportHandler implements ClipboardHandler {
public EpisodeListExportHandler(FileBotList<Episode> list) {
super(list);
}
@Override
public Transferable createTransferable(JComponent c) {
Transferable episodeArray = new ArrayTransferable<Episode>(list.getModel().toArray(new Episode[0]));
Transferable textFile = super.createTransferable(c);
return new CompositeTranserable(episodeArray, textFile);
}
@Override
public void exportToClipboard(JComponent c, Clipboard clipboard, int action) throws IllegalStateException {
Object[] selection = list.getListComponent().getSelectedValues();
Episode[] episodes = Arrays.copyOf(selection, selection.length, Episode[].class);
Transferable episodeArray = new ArrayTransferable<Episode>(episodes);
Transferable stringSelection = new StringSelection(StringUtilities.join(episodes, "\n"));
clipboard.setContents(new CompositeTranserable(episodeArray, stringSelection), null);
}
}
}

View File

@ -1,8 +1,7 @@
package net.sourceforge.filebot.ui.subtitle;
import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.filebot.media.MediaDetection.*;
import static net.sourceforge.filebot.ui.NotificationLogging.*;
import static net.sourceforge.filebot.ui.transfer.FileTransferable.*;
import static net.sourceforge.tuned.FileUtilities.*;
@ -21,9 +20,12 @@ import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import javax.swing.Icon;
@ -33,179 +35,56 @@ import javax.swing.JFileChooser;
import javax.swing.filechooser.FileNameExtensionFilter;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.web.OpenSubtitlesClient;
import net.sourceforge.filebot.web.SubtitleProvider;
import net.sourceforge.filebot.web.VideoHashSubtitleService;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.FileUtilities.ParentFilter;
abstract class SubtitleDropTarget extends JButton {
private enum DropAction {
Download,
Upload,
Cancel
Accept, 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);
setDropAction(DropAction.Accept);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
// install mouse listener
addActionListener(clickHandler);
// install drop target
new DropTarget(this, dropHandler);
}
private void setDropAction(DropAction dropAction) {
protected 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[] getVideoHashSubtitleServices();
public abstract SubtitleProvider[] getSubtitleProviders();
public abstract String getQueryLanguage();
private boolean handleDownload(List<File> videoFiles) {
SubtitleAutoMatchDialog dialog = new SubtitleAutoMatchDialog(getWindow(this));
// initialize download parameters
dialog.setVideoFiles(videoFiles.toArray(new File[0]));
for (VideoHashSubtitleService service : getVideoHashSubtitleServices()) {
dialog.addSubtitleService(service);
}
for (SubtitleProvider service : getSubtitleProviders()) {
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.setSize(820, 575);
// show dialog
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setVisible(true);
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 handleDownloadLater(files);
}
if (containsOnly(files, FOLDERS)) {
// collect all video files from the dropped folders
List<File> videoFiles = filter(listFiles(files, 5, false), VIDEO_FILES);
if (videoFiles.size() > 0) {
return handleDownloadLater(videoFiles);
}
}
if (containsOnly(files, SUBTITLE_FILES)) {
// TODO implement upload
throw new UnsupportedOperationException("Not implemented yet");
}
if (containsOnlyVideoSubtitleMatches(files)) {
// TODO implement upload
throw new UnsupportedOperationException("Not implemented yet");
}
return false;
}
private boolean handleDownloadLater(final List<File> videoFiles) {
// invoke later so we don't block the DnD operation with the download dialog
invokeLater(0, new Runnable() {
@Override
public void run() {
handleDownload(videoFiles);
}
});
return true;
}
private boolean containsOnlyVideoSubtitleMatches(List<File> files) {
List<File> subtitles = filter(files, SUBTITLE_FILES);
if (subtitles.isEmpty())
return false;
// number of subtitle files must match the number of video files
return subtitles.size() == filter(files, VIDEO_FILES).size();
}
private DropAction getDropAction(List<File> files) {
// video files only, or any folder, containing video files
if (containsOnly(files, VIDEO_FILES) || (containsOnly(files, FOLDERS))) {
return DropAction.Download;
}
// subtitle files only, or video/subtitle matches
if (containsOnly(files, SUBTITLE_FILES) || containsOnlyVideoSubtitleMatches(files)) {
return DropAction.Upload;
}
// unknown input
return DropAction.Cancel;
}
protected abstract boolean handleDrop(List<File> files);
protected abstract DropAction getDropAction(List<File> files);
protected abstract Icon getIcon(DropAction dropAction);
private final DropTargetAdapter dropHandler = new DropTargetAdapter() {
@Override
public void dragEnter(DropTargetDragEvent dtde) {
DropAction dropAction = DropAction.Download;
DropAction dropAction = DropAction.Accept;
try {
dropAction = getDropAction(getFilesFromTransferable(dtde.getTransferable()));
} catch (Exception e) {
@ -213,10 +92,10 @@ abstract class SubtitleDropTarget extends JButton {
// because on some implementations we can't access transferable data before we accept the drag,
// but accepting or rejecting the drag depends on the files dragged
}
// update visual representation
setDropAction(dropAction);
// accept or reject
if (dropAction != DropAction.Cancel) {
dtde.acceptDrag(DnDConstants.ACTION_REFERENCE);
@ -224,53 +103,221 @@ abstract class SubtitleDropTarget extends JButton {
dtde.rejectDrag();
}
}
@Override
public void dragExit(DropTargetEvent dte) {
// reset to default state
setDropAction(DropAction.Download);
setDropAction(DropAction.Accept);
};
@Override
public void drop(DropTargetDropEvent dtde) {
dtde.acceptDrop(DnDConstants.ACTION_REFERENCE);
try {
dtde.dropComplete(handleDrop(getFilesFromTransferable(dtde.getTransferable())));
} catch (Exception e) {
UILogger.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()));
}
}
}
};
public static abstract class Download extends SubtitleDropTarget {
public abstract VideoHashSubtitleService[] getVideoHashSubtitleServices();
public abstract SubtitleProvider[] getSubtitleProviders();
public abstract String getQueryLanguage();
@Override
protected DropAction getDropAction(List<File> input) {
// accept video files and folders
return filter(input, VIDEO_FILES, FOLDERS).size() > 0 ? DropAction.Accept : DropAction.Cancel;
}
@Override
protected boolean handleDrop(List<File> input) {
// perform a drop action depending on the given files
final Collection<File> videoFiles = new TreeSet<File>();
// video files only
videoFiles.addAll(filter(input, VIDEO_FILES));
videoFiles.addAll(filter(listFiles(filter(input, FOLDERS), 5, false), VIDEO_FILES));
if (videoFiles.size() > 0) {
// invoke later so we don't block the DnD operation with the download dialog
invokeLater(0, new Runnable() {
@Override
public void run() {
handleDownload(videoFiles);
}
});
return true;
}
return false;
}
protected boolean handleDownload(Collection<File> videoFiles) {
SubtitleAutoMatchDialog dialog = new SubtitleAutoMatchDialog(getWindow(this));
// initialize download parameters
dialog.setVideoFiles(videoFiles.toArray(new File[0]));
for (VideoHashSubtitleService service : getVideoHashSubtitleServices()) {
dialog.addSubtitleService(service);
}
for (SubtitleProvider service : getSubtitleProviders()) {
dialog.addSubtitleService(service);
}
// start looking for subtitles
dialog.startQuery(getQueryLanguage());
// initialize window properties
dialog.setIconImage(getImage(getIcon(DropAction.Accept)));
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setSize(820, 575);
// show dialog
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setVisible(true);
return true;
}
protected Icon getIcon(DropAction dropAction) {
switch (dropAction) {
case Accept:
return ResourceManager.getIcon("subtitle.exact.download");
default:
return ResourceManager.getIcon("message.error");
}
}
}
public static abstract class Upload extends SubtitleDropTarget {
public abstract OpenSubtitlesClient getSubtitleService();
@Override
protected DropAction getDropAction(List<File> input) {
if (getSubtitleService().isAnonymous())
return DropAction.Cancel;
// accept video files and folders
return (filter(input, VIDEO_FILES).size() > 0 && filter(input, SUBTITLE_FILES).size() > 0) || filter(input, FOLDERS).size() > 0 ? DropAction.Accept : DropAction.Cancel;
}
@Override
protected boolean handleDrop(List<File> input) {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
// perform a drop action depending on the given files
final Collection<File> files = new TreeSet<File>();
// video files only
files.addAll(filter(input, FILES));
files.addAll(listFiles(filter(input, FOLDERS), 5, false));
final List<File> videos = filter(files, VIDEO_FILES);
final List<File> subtitles = filter(files, SUBTITLE_FILES);
final Map<File, File> uploadPlan = new LinkedHashMap<File, File>();
for (File subtitle : subtitles) {
File video = getVideoForSubtitle(subtitle, filter(videos, new ParentFilter(subtitle.getParentFile())));
uploadPlan.put(subtitle, video);
}
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
if (uploadPlan.size() > 0) {
// invoke later so we don't block the DnD operation with the download dialog
invokeLater(0, new Runnable() {
@Override
public void run() {
handleUpload(uploadPlan);
}
});
return true;
}
return false;
}
protected void handleUpload(Map<File, File> uploadPlan) {
SubtitleUploadDialog dialog = new SubtitleUploadDialog(getSubtitleService(), getWindow(this));
// initialize window properties
dialog.setIconImage(getImage(getIcon(DropAction.Accept)));
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setSize(820, 575);
// show dialog
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
// start processing
dialog.setUploadPlan(uploadPlan);
dialog.startChecking();
// show dialog
dialog.setVisible(true);
}
protected File getVideoForSubtitle(File subtitle, List<File> videos) {
String baseName = stripReleaseInfo(FileUtilities.getName(subtitle)).toLowerCase();
// find corresponding movie file
for (File it : videos) {
if (!baseName.isEmpty() && stripReleaseInfo(FileUtilities.getName(it)).toLowerCase().startsWith(baseName)) {
return it;
}
}
return null;
}
protected Icon getIcon(DropAction dropAction) {
switch (dropAction) {
case Accept:
return ResourceManager.getIcon("subtitle.exact.upload");
default:
return ResourceManager.getIcon("message.error");
}
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.ui.subtitle;
import static net.sourceforge.filebot.Settings.*;
import static net.sourceforge.filebot.ui.LanguageComboBoxModel.*;
import static net.sourceforge.filebot.ui.NotificationLogging.*;
@ -49,219 +47,230 @@ import net.sourceforge.filebot.web.VideoHashSubtitleService;
import net.sourceforge.tuned.ui.LabelProvider;
import net.sourceforge.tuned.ui.SimpleLabelProvider;
public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, SubtitlePackage> {
private LanguageComboBox languageComboBox = new LanguageComboBox(this, ALL_LANGUAGES);
private LanguageComboBox languageComboBox = new LanguageComboBox(ALL_LANGUAGES, getSettings());
public SubtitlePanel() {
historyPanel.setColumnHeader(0, "Show / Movie");
historyPanel.setColumnHeader(1, "Number of Subtitles");
// add after text field
add(languageComboBox, "gap indent", 1);
add(createImageButton(setUserAction), "width 26px!, height 26px!, gap rel", 2);
add(languageComboBox, "gap indent, sgy button", 1);
add(createImageButton(setUserAction), "width 26px!, height 26px!, gap rel, sgy button", 2);
// add at the top right corner
add(dropTarget, "width 1.6cm!, height 1.2cm!, pos n 0% 100% n", 0);
add(uploadDropTarget, "width 1.45cm!, height 1.2cm!, pos n 0% 100%-1.8cm n", 0);
add(downloadDropTarget, "width 1.45cm!, height 1.2cm!, pos n 0% 100%-0.15cm n", 0);
}
private final SubtitleDropTarget dropTarget = new SubtitleDropTarget() {
@Override
public VideoHashSubtitleService[] getVideoHashSubtitleServices() {
return WebServices.getVideoHashSubtitleServices();
}
@Override
public SubtitleProvider[] getSubtitleProviders() {
return WebServices.getSubtitleProviders();
}
@Override
public String getQueryLanguage() {
// use currently selected language for drop target
return languageComboBox.getModel().getSelectedItem() == ALL_LANGUAGES ? null : languageComboBox.getModel().getSelectedItem().getName();
}
private final SubtitleDropTarget uploadDropTarget = new SubtitleDropTarget.Upload() {
public OpenSubtitlesClient getSubtitleService() {
return WebServices.OpenSubtitles;
};
@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);
path.lineTo(12, getHeight() - 1);
path.lineTo(getWidth() - 1 - 12, getHeight() - 1);
path.lineTo(getWidth() - 1, getHeight() - 1 - 12);
path.lineTo(getWidth() - 1, 0);
g2d.setPaint(getBackground());
g2d.fill(path);
g2d.setPaint(Color.gray);
g2d.draw(path);
g2d.translate(2, 0);
super.paintComponent(g2d);
g2d.dispose();
}
};
private final SubtitleDropTarget downloadDropTarget = new SubtitleDropTarget.Download() {
@Override
public VideoHashSubtitleService[] getVideoHashSubtitleServices() {
return WebServices.getVideoHashSubtitleServices();
}
@Override
public SubtitleProvider[] getSubtitleProviders() {
return WebServices.getSubtitleProviders();
}
@Override
public String getQueryLanguage() {
// use currently selected language for drop target
return languageComboBox.getModel().getSelectedItem() == ALL_LANGUAGES ? null : languageComboBox.getModel().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.lineTo(12, getHeight() - 1);
path.lineTo(getWidth() - 1 - 12, getHeight() - 1);
path.lineTo(getWidth() - 1, getHeight() - 1 - 12);
path.lineTo(getWidth() - 1, 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 WebServices.getSubtitleProviders();
}
@Override
protected LabelProvider<SubtitleProvider> getSearchEngineLabelProvider() {
return SimpleLabelProvider.forClass(SubtitleProvider.class);
}
@Override
protected Settings getSettings() {
return Settings.forPackage(SubtitlePanel.class);
}
@Override
protected SubtitleRequestProcessor createRequestProcessor() {
SubtitleProvider provider = searchTextField.getSelectButton().getSelectedValue();
String text = searchTextField.getText().trim();
Language language = languageComboBox.getModel().getSelectedItem();
return new SubtitleRequestProcessor(new SubtitleRequest(provider, text, language));
}
protected static class SubtitleRequest extends Request {
private final SubtitleProvider provider;
private final Language language;
public SubtitleRequest(SubtitleProvider provider, String searchText, Language language) {
super(searchText);
this.provider = provider;
this.language = language;
}
public SubtitleProvider getProvider() {
return provider;
}
public String getLanguageName() {
return language == ALL_LANGUAGES ? null : language.getName();
}
}
protected static class SubtitleRequestProcessor extends RequestProcessor<SubtitleRequest, SubtitlePackage> {
public SubtitleRequestProcessor(SubtitleRequest request) {
super(request, new SubtitleDownloadComponent());
}
@Override
public Collection<SearchResult> search() throws Exception {
return request.getProvider().search(request.getSearchText());
}
@Override
public Collection<SubtitlePackage> fetch() throws Exception {
List<SubtitlePackage> packages = new ArrayList<SubtitlePackage>();
for (SubtitleDescriptor subtitle : request.getProvider().getSubtitleList(getSearchResult(), request.getLanguageName())) {
packages.add(new SubtitlePackage(request.getProvider(), subtitle));
}
return packages;
}
@Override
public URI getLink() {
return request.getProvider().getSubtitleListLink(getSearchResult(), request.getLanguageName());
}
@Override
public void process(Collection<SubtitlePackage> subtitles) {
getComponent().setLanguageVisible(request.getLanguageName() == null);
getComponent().getPackageModel().addAll(subtitles);
}
@Override
public SubtitleDownloadComponent getComponent() {
return (SubtitleDownloadComponent) super.getComponent();
}
@Override
public String getStatusMessage(Collection<SubtitlePackage> result) {
return (result.isEmpty()) ? "No subtitles found" : String.format("%d subtitles", result.size());
}
@Override
public Icon getIcon() {
return request.provider.getIcon();
}
@Override
protected void configureSelectDialog(SelectDialog<SearchResult> selectDialog) {
super.configureSelectDialog(selectDialog);
selectDialog.getHeaderLabel().setText("Select a Show / Movie:");
}
}
protected final Action setUserAction = new AbstractAction("Set User", ResourceManager.getIcon("action.user")) {
@Override
public void actionPerformed(ActionEvent evt) {
final JDialog authPanel = new JDialog(getWindow(SubtitlePanel.this), ModalityType.APPLICATION_MODAL);
authPanel.setTitle("Login");
authPanel.setLocation(getOffsetLocation(authPanel.getOwner()));
JPanel osdbGroup = new JPanel(new MigLayout("fill, insets panel"));
osdbGroup.setBorder(new TitledBorder("OpenSubtitles"));
osdbGroup.add(new JLabel("Username:"), "gap rel");
final JTextField osdbUser = new JTextField(12);
osdbGroup.add(osdbUser, "growx, wrap rel");
osdbGroup.add(new JLabel("Password:"), "gap rel");
final JPasswordField osdbPass = new JPasswordField(12);
osdbGroup.add(osdbPass, "growx, wrap rel");
JRootPane container = authPanel.getRootPane();
container.setLayout(new MigLayout("fill, insets dialog"));
container.removeAll();
container.add(osdbGroup, "growx, wrap");
Action ok = new AbstractAction("OK") {
@Override
public void actionPerformed(ActionEvent evt) {
authPanel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
boolean approved = true;
try {
if (osdbUser.getText().length() > 0 && osdbPass.getPassword().length > 0) {
OpenSubtitlesClient osdb = new OpenSubtitlesClient(String.format("%s %s", getApplicationName(), getApplicationVersion()));
@ -272,7 +281,7 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
UILogger.log(Level.WARNING, "OpenSubtitles: " + e.getMessage());
approved = false;
}
authPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
if (approved) {
WebServices.setLogin("osdb.user", osdbUser.getText(), new String(osdbPass.getPassword()));
@ -281,7 +290,7 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
}
};
Action cancel = new AbstractAction("Cancel") {
@Override
public void actionPerformed(ActionEvent evt) {
authPanel.setVisible(false);
@ -289,15 +298,15 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
};
container.add(new JButton(cancel), "tag cancel, split 2");
container.add(new JButton(ok), "tag ok");
// restore values
String[] osdbAuth = WebServices.getLogin("osdb.user");
osdbUser.setText(osdbAuth[0]);
osdbPass.setText(osdbAuth[1]);
authPanel.pack();
authPanel.setVisible(true);
}
};
}

View File

@ -0,0 +1,666 @@
package net.sourceforge.filebot.ui.subtitle;
import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.filebot.media.MediaDetection.*;
import static net.sourceforge.tuned.ui.TunedUtilities.*;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
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.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.EventObject;
import java.util.List;
import java.util.Locale;
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.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;
import javax.swing.event.CellEditorListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.media.MediaDetection;
import net.sourceforge.filebot.ui.Language;
import net.sourceforge.filebot.ui.LanguageComboBox;
import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.web.Movie;
import net.sourceforge.filebot.web.OpenSubtitlesClient;
import net.sourceforge.filebot.web.VideoHashSubtitleService.CheckResult;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.ui.AbstractBean;
import net.sourceforge.tuned.ui.EmptySelectionModel;
public class SubtitleUploadDialog extends JDialog {
private final JTable subtitleMappingTable = createTable();
private final OpenSubtitlesClient database;
private ExecutorService checkExecutorService;
private ExecutorService uploadExecutorService;
public SubtitleUploadDialog(OpenSubtitlesClient database, Window owner) {
super(owner, "Upload Subtitles", ModalityType.DOCUMENT_MODAL);
this.database = database;
JComponent content = (JComponent) getContentPane();
content.setLayout(new MigLayout("fill, insets dialog, nogrid", "", "[fill][pref!]"));
content.add(new JScrollPane(subtitleMappingTable), "grow, wrap");
content.add(new JButton(uploadAction), "tag ok");
content.add(new JButton(finishAction), "tag cancel");
}
protected JTable createTable() {
JTable table = new JTable(new SubtitleMappingTableModel());
table.setDefaultRenderer(Movie.class, new MovieRenderer());
table.setDefaultRenderer(File.class, new FileRenderer());
table.setDefaultRenderer(Language.class, new LanguageRenderer());
table.setDefaultRenderer(SubtitleMapping.Status.class, new StatusRenderer());
table.setRowHeight(28);
table.setIntercellSpacing(new Dimension(5, 5));
table.setBackground(Color.white);
table.setAutoCreateRowSorter(true);
table.setFillsViewportHeight(true);
LanguageComboBox languageEditor = new LanguageComboBox(Language.getLanguage("en"), null);
// disable selection
table.setSelectionModel(new EmptySelectionModel());
languageEditor.setFocusable(false);
table.setDefaultEditor(Language.class, new DefaultCellEditor(languageEditor) {
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
LanguageComboBox editor = (LanguageComboBox) super.getTableCellEditorComponent(table, value, isSelected, row, column);
editor.getModel().setSelectedItem(value);
return editor;
}
});
table.setDefaultEditor(Movie.class, new TableCellEditor() {
@Override
public boolean stopCellEditing() {
return true;
}
@Override
public boolean shouldSelectCell(EventObject evt) {
return false;
}
@Override
public void removeCellEditorListener(CellEditorListener listener) {
}
@Override
public boolean isCellEditable(EventObject evt) {
return true;
}
@Override
public Object getCellEditorValue() {
return null;
}
@Override
public void cancelCellEditing() {
}
@Override
public void addCellEditorListener(CellEditorListener evt) {
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
table.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
try {
SubtitleMappingTableModel model = (SubtitleMappingTableModel) table.getModel();
SubtitleMapping mapping = model.getData()[table.convertRowIndexToModel(row)];
Object originalIdentity = mapping.getIdentity();
File video = mapping.getVideo();
String input = showInputDialog("Enter movie / series name:", stripReleaseInfo(FileUtilities.getName(video)), String.format("%s/%s", video.getParentFile().getName(), video.getName()), SubtitleUploadDialog.this);
if (input != null && input.length() > 0) {
List<Movie> options = database.searchMovie(input, Locale.ENGLISH);
if (options.size() > 0) {
SelectDialog<Movie> dialog = new SelectDialog<Movie>(SubtitleUploadDialog.this, options);
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setVisible(true);
Movie selectedValue = dialog.getSelectedValue();
if (selectedValue != null) {
for (SubtitleMapping it : model.getData()) {
if (originalIdentity == it.getIdentity() || (originalIdentity != null && originalIdentity.equals(it.getIdentity()))) {
if (model.isCellEditable(table.convertRowIndexToModel(row), table.convertColumnIndexToModel(column))) {
it.setIdentity(selectedValue);
}
}
}
if (mapping.getIdentity() != null && mapping.getLanguage() != null) {
mapping.setState(SubtitleMapping.Status.UploadReady);
}
}
}
}
} catch (Exception e) {
Logger.getLogger(SubtitleUploadDialog.class.getClass().getName()).log(Level.WARNING, e.getMessage(), e);
}
table.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
return null;
}
});
return table;
}
public void setUploadPlan(Map<File, File> uploadPlan) {
List<SubtitleMapping> mappings = new ArrayList<SubtitleMapping>(uploadPlan.size());
for (Entry<File, File> entry : uploadPlan.entrySet()) {
File subtitle = entry.getKey();
File video = entry.getValue();
Locale locale = MediaDetection.guessLanguageFromSuffix(subtitle);
Language language = Language.getLanguage(locale);
mappings.add(new SubtitleMapping(subtitle, video, language));
}
subtitleMappingTable.setModel(new SubtitleMappingTableModel(mappings.toArray(new SubtitleMapping[0])));
}
public void startChecking() {
checkExecutorService = Executors.newFixedThreadPool(2);
SubtitleMapping[] data = ((SubtitleMappingTableModel) subtitleMappingTable.getModel()).getData();
for (SubtitleMapping it : data) {
if (it.getSubtitle() != null && it.getVideo() != null) {
checkExecutorService.submit(new CheckTask(it));
} else {
it.setState(SubtitleMapping.Status.IllegalInput);
}
}
checkExecutorService.shutdown();
}
private final Action uploadAction = new AbstractAction("Upload", 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 upload as long as there are still unfinished download tasks
if (uploadExecutorService != null && !uploadExecutorService.isTerminated()) {
return;
}
uploadExecutorService = Executors.newFixedThreadPool(1);
SubtitleMapping[] data = ((SubtitleMappingTableModel) subtitleMappingTable.getModel()).getData();
for (final SubtitleMapping it : data) {
if (it.getStatus() == SubtitleMapping.Status.UploadReady) {
uploadExecutorService.submit(new UploadTask(it));
}
}
// terminate after all uploads have been completed
uploadExecutorService.shutdown();
}
};
private final Action finishAction = new AbstractAction("Close", ResourceManager.getIcon("dialog.cancel")) {
@Override
public void actionPerformed(ActionEvent evt) {
if (checkExecutorService != null) {
checkExecutorService.shutdownNow();
}
if (uploadExecutorService != null) {
uploadExecutorService.shutdownNow();
}
setVisible(false);
dispose();
}
};
private class MovieRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
String text = null;
String tooltip = null;
Icon icon = null;
Movie movie = (Movie) value;
if (movie != null) {
text = movie.toString();
tooltip = String.format("%s [tt%07d]", movie.toString(), movie.getImdbId());
icon = database.getIcon();
}
setText(text);
setToolTipText(tooltip);
setIcon(icon);
return this;
}
}
private class FileRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
String text = null;
String tooltip = null;
Icon icon = null;
if (value != null) {
File file = (File) value;
text = file.getName();
tooltip = file.getPath();
if (SUBTITLE_FILES.accept(file)) {
icon = ResourceManager.getIcon("file.subtitle");
} else if (VIDEO_FILES.accept(file)) {
icon = ResourceManager.getIcon("file.video");
}
}
setText(text);
setToolTipText(text);
setIcon(icon);
return this;
}
}
private class LanguageRenderer implements TableCellRenderer, ListCellRenderer {
private DefaultTableCellRenderer tableCell = new DefaultTableCellRenderer();
private DefaultListCellRenderer listCell = new DefaultListCellRenderer();
private Component configure(JLabel c, Object value, boolean isSelected, boolean hasFocus) {
String text = null;
Icon icon = null;
if (value != null) {
Language language = (Language) value;
text = language.getName();
icon = ResourceManager.getFlagIcon(language.getCode());
}
c.setText(text);
c.setIcon(icon);
return c;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return configure((DefaultTableCellRenderer) tableCell.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column), value, isSelected, hasFocus);
}
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
return configure((DefaultListCellRenderer) listCell.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus), value, isSelected, cellHasFocus);
}
}
private class StatusRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
String text = null;
Icon icon = null;
// CheckPending, Checking, CheckFailed, AlreadyExists, Identifying, IdentificationRequired, UploadPending, Uploading, UploadComplete, UploadFailed;
switch ((SubtitleMapping.Status) value) {
case IllegalInput:
text = "No video/subtitle pair";
icon = ResourceManager.getIcon("status.error");
break;
case CheckPending:
text = "Pending...";
icon = ResourceManager.getIcon("worker.pending");
break;
case Checking:
text = "Checking database...";
icon = ResourceManager.getIcon("database.go");
break;
case CheckFailed:
text = "Failed to check database";
icon = ResourceManager.getIcon("database.error");
break;
case AlreadyExists:
text = "Subtitle already exists in database";
icon = ResourceManager.getIcon("database.ok");
break;
case Identifying:
text = "Auto-detect missing information";
icon = ResourceManager.getIcon("action.export");
break;
case IdentificationRequired:
text = "Unable to auto-detect movie / series info";
icon = ResourceManager.getIcon("dialog.continue.invalid");
break;
case UploadReady:
text = "Ready for upload";
icon = ResourceManager.getIcon("dialog.continue");
break;
case Uploading:
text = "Uploading...";
icon = ResourceManager.getIcon("database.go");
break;
case UploadComplete:
text = "Upload successful";
icon = ResourceManager.getIcon("database.ok");
break;
case UploadFailed:
text = "Upload failed";
icon = ResourceManager.getIcon("database.error");
break;
}
setText(text);
setIcon(icon);
return this;
}
}
private static class SubtitleMappingTableModel extends AbstractTableModel {
private final SubtitleMapping[] data;
public SubtitleMappingTableModel(SubtitleMapping... mappings) {
this.data = mappings.clone();
for (int i = 0; i < data.length; i++) {
data[i].addPropertyChangeListener(new SubtitleMappingListener(i));
}
}
public SubtitleMapping[] getData() {
return data.clone();
}
@Override
public int getColumnCount() {
return 5;
}
@Override
public String getColumnName(int column) {
switch (column) {
case 0:
return "Movie / Series";
case 1:
return "Video";
case 2:
return "Subtitle";
case 3:
return "Language";
case 4:
return "Status";
}
return null;
}
@Override
public int getRowCount() {
return data.length;
}
@Override
public Object getValueAt(int row, int column) {
switch (column) {
case 0:
return data[row].getIdentity();
case 1:
return data[row].getVideo();
case 2:
return data[row].getSubtitle();
case 3:
return data[row].getLanguage();
case 4:
return data[row].getStatus();
}
return null;
}
@Override
public void setValueAt(Object value, int row, int column) {
if (getColumnClass(column) == Language.class && value instanceof Language) {
data[row].setLanguage((Language) value);
}
}
@Override
public boolean isCellEditable(int row, int column) {
return (EnumSet.of(SubtitleMapping.Status.IdentificationRequired, SubtitleMapping.Status.UploadReady).contains(data[row].getStatus())) && (getColumnClass(column) == Movie.class || getColumnClass(column) == Language.class);
}
@Override
public Class<?> getColumnClass(int column) {
switch (column) {
case 0:
return Movie.class;
case 1:
return File.class;
case 2:
return File.class;
case 3:
return Language.class;
case 4:
return SubtitleMapping.Status.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 {
enum Status {
IllegalInput, CheckPending, Checking, CheckFailed, AlreadyExists, Identifying, IdentificationRequired, UploadReady, Uploading, UploadComplete, UploadFailed;
}
private Object identity;
private File subtitle;
private File video;
private Language language;
private Status status = Status.CheckPending;
private String message = null;
public SubtitleMapping(File subtitle, File video, Language language) {
this.subtitle = subtitle;
this.video = video;
this.language = language;
}
public Object getIdentity() {
return identity;
}
public File getSubtitle() {
return subtitle;
}
public File getVideo() {
return video;
}
public Language getLanguage() {
return language;
}
public Status getStatus() {
return status;
}
public void setIdentity(Object identity) {
this.identity = identity;
firePropertyChange("identity", null, this.identity);
}
public void setLanguage(Language language) {
this.language = language;
firePropertyChange("language", null, this.language);
}
public void setState(Status status) {
this.status = status;
firePropertyChange("status", null, this.status);
}
}
private class CheckTask extends SwingWorker<Object, Void> {
private final SubtitleMapping mapping;
public CheckTask(SubtitleMapping mapping) {
this.mapping = mapping;
}
@Override
protected Object doInBackground() throws Exception {
try {
mapping.setState(SubtitleMapping.Status.Checking);
CheckResult checkResult = database.checkSubtitle(mapping.getVideo(), mapping.getSubtitle());
// accept identity hint from search result
mapping.setIdentity(checkResult.identity);
if (checkResult.exists) {
mapping.setLanguage(Language.getLanguage(checkResult.language)); // trust language hint only if upload not required
mapping.setState(SubtitleMapping.Status.AlreadyExists);
return checkResult;
}
if (mapping.getLanguage() == null) {
mapping.setState(SubtitleMapping.Status.Identifying);
try {
Locale locale = database.detectLanguage(FileUtilities.readFile(mapping.getSubtitle()));
mapping.setLanguage(Language.getLanguage(locale));
} catch (Exception e) {
Logger.getLogger(CheckTask.class.getClass().getName()).log(Level.WARNING, "Failed to auto-detect language: " + e.getMessage());
}
}
// default to English
if (mapping.getLanguage() == null) {
mapping.setLanguage(Language.getLanguage("en"));
}
if (mapping.getIdentity() == null) {
mapping.setState(SubtitleMapping.Status.Identifying);
try {
Collection<Movie> identity = MediaDetection.detectMovie(mapping.getVideo(), database, database, Locale.ENGLISH, true);
for (Movie it : identity) {
if (it.getImdbId() <= 0 && it.getTmdbId() > 0) {
it = MediaDetection.tmdb2imdb(it);
}
if (it != null && it.getImdbId() > 0) {
mapping.setIdentity(it);
break;
}
}
} catch (Exception e) {
Logger.getLogger(CheckTask.class.getClass().getName()).log(Level.WARNING, "Failed to auto-detect movie: " + e.getMessage());
}
}
if (mapping.getIdentity() == null) {
mapping.setState(SubtitleMapping.Status.IdentificationRequired);
} else {
mapping.setState(SubtitleMapping.Status.UploadReady);
}
return checkResult;
} catch (Exception e) {
Logger.getLogger(CheckTask.class.getClass().getName()).log(Level.SEVERE, e.getMessage(), e);
mapping.setState(SubtitleMapping.Status.CheckFailed);
}
return null;
}
}
private class UploadTask extends SwingWorker<Object, Void> {
private final SubtitleMapping mapping;
public UploadTask(SubtitleMapping mapping) {
this.mapping = mapping;
}
@Override
protected Object doInBackground() {
try {
mapping.setState(SubtitleMapping.Status.Uploading);
if (true)
throw new RuntimeException();
database.uploadSubtitle(mapping.getIdentity(), mapping.getLanguage().toLocale(), mapping.getVideo(), mapping.getSubtitle());
mapping.setState(SubtitleMapping.Status.UploadComplete);
} catch (Exception e) {
Logger.getLogger(UploadTask.class.getClass().getName()).log(Level.SEVERE, e.getMessage(), e);
mapping.setState(SubtitleMapping.Status.UploadFailed);
}
return null;
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.web;
import static java.lang.Math.*;
import static java.util.Arrays.*;
import static java.util.Collections.*;
@ -43,62 +41,61 @@ import net.sourceforge.filebot.web.OpenSubtitlesXmlRpc.TryUploadResponse;
import net.sourceforge.tuned.Timer;
import redstone.xmlrpc.XmlRpcException;
/**
* SubtitleClient for OpenSubtitles.
*/
public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleService, MovieIdentificationService {
private final OpenSubtitlesXmlRpc xmlrpc;
public final OpenSubtitlesXmlRpc xmlrpc;
private String username = "";
private String password = "";
public OpenSubtitlesClient(String useragent) {
this.xmlrpc = new OpenSubtitlesXmlRpc(useragent);
}
public void setUser(String username, String password) {
public synchronized void setUser(String username, String password) {
// cancel previous session
this.logout();
this.username = username;
this.password = password;
}
public boolean isAnonymous() {
return username.isEmpty();
}
@Override
public String getName() {
return "OpenSubtitles";
}
@Override
public URI getLink() {
return URI.create("http://www.opensubtitles.org");
}
@Override
public Icon getIcon() {
return ResourceManager.getIcon("search.opensubtitles");
}
public ResultCache getCache() {
return new ResultCache("opensubtitles.org", Cache.getCache("web-datasource"));
}
@Override
public List<SearchResult> search(String query) throws Exception {
List<SearchResult> result = getCache().getSearchResult("search", query, null);
if (result != null) {
return result;
}
// require login
login();
try {
// search for movies / series
List<Movie> resultSet = xmlrpc.searchMoviesOnIMDB(query);
@ -107,52 +104,50 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
// unexpected xmlrpc responses (e.g. error messages instead of results) will trigger this
throw new XmlRpcException("Illegal XMLRPC response on searchMoviesOnIMDB");
}
getCache().putSearchResult("search", query, null, result);
return result;
}
@Override
public List<SubtitleDescriptor> getSubtitleList(SearchResult searchResult, String languageName) throws Exception {
List<SubtitleDescriptor> subtitles = getCache().getSubtitleDescriptorList(searchResult, languageName);
if (subtitles != null) {
return subtitles;
}
// singleton array with or empty array
int imdbid = ((Movie) searchResult).getImdbId();
String[] languageFilter = languageName != null ? new String[] { getSubLanguageID(languageName) } : new String[0];
// require login
login();
// get subtitle list
subtitles = asList(xmlrpc.searchSubtitles(imdbid, languageFilter).toArray(new SubtitleDescriptor[0]));
getCache().putSubtitleDescriptorList(searchResult, languageName, subtitles);
return subtitles;
}
@Override
public Map<File, List<SubtitleDescriptor>> getSubtitleList(File[] files, String languageName) throws Exception {
// singleton array with or empty array
String[] languageFilter = languageName != null ? new String[] { getSubLanguageID(languageName) } : new String[0];
// remember hash for each file
Map<Query, File> hashMap = new HashMap<Query, File>(files.length);
Map<File, List<SubtitleDescriptor>> resultMap = new HashMap<File, List<SubtitleDescriptor>>(files.length);
// create hash query for each file
List<Query> queryList = new ArrayList<Query>(files.length);
for (File file : files) {
// add query
if (file.length() > HASH_CHUNK_SIZE) {
String movieHash = computeHash(file);
Query query = Query.forHash(movieHash, file.length(), languageFilter);
// check hash
List<SubtitleDescriptor> cachedResults = getCache().getSubtitleDescriptorList(query, languageName);
if (cachedResults == null) {
@ -162,90 +157,119 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
resultMap.put(file, cachedResults);
}
}
// prepare result map
if (resultMap.get(file) == null) {
resultMap.put(file, new LinkedList<SubtitleDescriptor>());
}
}
if (queryList.size() > 0) {
// require login
login();
// dispatch query for all hashes
int batchSize = 50;
for (int bn = 0; bn < ceil((float) queryList.size() / batchSize); bn++) {
List<Query> batch = queryList.subList(bn * batchSize, min((bn * batchSize) + batchSize, queryList.size()));
// submit query and map results to given files
for (OpenSubtitlesSubtitleDescriptor subtitle : xmlrpc.searchSubtitles(batch)) {
// get file for hash
File file = hashMap.get(Query.forHash(subtitle.getMovieHash(), subtitle.getMovieByteSize(), languageFilter));
// add subtitle
resultMap.get(file).add(subtitle);
}
for (Query query : batch) {
getCache().putSubtitleDescriptorList(query, languageName, resultMap.get(hashMap.get(query)));
}
}
}
return resultMap;
}
@Override
public boolean publishSubtitle(int imdbid, String languageName, File[] videoFile, File[] subtitleFile) throws Exception {
SubFile[] subs = new SubFile[subtitleFile.length];
// subhash (md5 of subtitles), subfilename, moviehash, moviebytesize, moviefilename.
for (int i = 0; i < subtitleFile.length; i++) {
SubFile sub = new SubFile();
sub.setSubHash(md5(subtitleFile[i]));
sub.setSubFileName(subtitleFile[i].getName());
sub.setMovieHash(computeHash(videoFile[i]));
sub.setMovieByteSize(videoFile[i].length());
sub.setMovieFileName(videoFile[i].getName());
subs[i] = sub;
}
public CheckResult checkSubtitle(File videoFile, File subtitleFile) throws Exception {
// subhash (md5 of subtitles), subfilename, moviehash, moviebytesize, moviefilename
SubFile sub = new SubFile();
sub.setSubHash(md5(subtitleFile));
sub.setSubFileName(subtitleFile.getName());
sub.setMovieHash(computeHash(videoFile));
sub.setMovieByteSize(videoFile.length());
sub.setMovieFileName(videoFile.getName());
// require login
login();
// check if subs already exist in db
TryUploadResponse response = xmlrpc.tryUploadSubtitles(subs);
System.out.println(response); // TODO only upload if necessary OR return false
BaseInfo info = new BaseInfo();
info.setIDMovieImdb(imdbid);
info.setSubLanguageID(getSubLanguageID(languageName));
// encode subtitle contents
for (int i = 0; i < subtitleFile.length; i++) {
// grab subtitle content
subs[i].setSubContent(readFile(subtitleFile[i]));
// check if subs already exist in DB
TryUploadResponse response = xmlrpc.tryUploadSubtitles(sub);
// TryUploadResponse: false => [{HashWasAlreadyInDb=1, MovieKind=movie, IDSubtitle=3167446, MoviefilenameWasAlreadyInDb=1, ISO639=en, MovieYear=2007, SubLanguageID=eng, MovieName=Blades of Glory, MovieNameEng=, IDMovieImdb=445934}]
boolean exists = !response.isUploadRequired();
Movie identity = null;
Locale language = null;
if (response.getSubtitleData().size() > 0) {
try {
// grab media info
MediaInfo mi = new MediaInfo();
mi.open(videoFile[i]);
subs[i].setMovieFPS(mi.get(StreamKind.Video, 0, "FrameRate"));
subs[i].setMovieTimeMS(mi.get(StreamKind.General, 0, "Duration"));
mi.close();
} catch (Throwable e) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
Map<String, String> fields = response.getSubtitleData().get(0);
String lang = fields.get("SubLanguageID");
language = new Locale(lang);
String imdb = fields.get("IDMovieImdb");
String name = fields.get("MovieName");
String year = fields.get("MovieYear");
identity = new Movie(name, Integer.parseInt(year), Integer.parseInt(imdb), -1);
} catch (Exception e) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage());
}
}
URI resource = xmlrpc.uploadSubtitles(info, subs);
System.out.println(resource);
return false;
return new CheckResult(exists, identity, language);
}
@Override
public void uploadSubtitle(Object identity, Locale language, File videoFile, File subtitleFile) throws Exception {
if (!(identity instanceof Movie && ((Movie) identity).getImdbId() > 0)) {
throw new IllegalArgumentException("Illegal Movie ID: " + identity);
}
int imdbid = ((Movie) identity).getImdbId();
String languageName = getSubLanguageID(language.getDisplayName(Locale.ENGLISH));
// subhash (md5 of subtitles), subfilename, moviehash, moviebytesize, moviefilename
SubFile sub = new SubFile();
sub.setSubHash(md5(subtitleFile));
sub.setSubFileName(subtitleFile.getName());
sub.setMovieHash(computeHash(videoFile));
sub.setMovieByteSize(videoFile.length());
sub.setMovieFileName(videoFile.getName());
BaseInfo info = new BaseInfo();
info.setIDMovieImdb(imdbid);
info.setSubLanguageID(languageName);
// encode subtitle contents
sub.setSubContent(readFile(subtitleFile));
try {
MediaInfo mi = new MediaInfo();
mi.open(videoFile);
sub.setMovieFPS(mi.get(StreamKind.Video, 0, "FrameRate"));
sub.setMovieTimeMS(mi.get(StreamKind.General, 0, "Duration"));
mi.close();
} catch (Throwable e) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
}
URI resource = xmlrpc.uploadSubtitles(info, sub);
System.out.println(resource);
}
/**
* Calculate MD5 hash.
*/
@ -258,8 +282,7 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
throw new RuntimeException(e); // won't happen
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public List<Movie> searchMovie(String query, Locale locale) throws Exception {
@ -267,51 +290,48 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
if (result != null) {
return (List) result;
}
// require login
login();
List<Movie> movies = xmlrpc.searchMoviesOnIMDB(query);
getCache().putSearchResult("searchMovie", query, locale, movies);
return movies;
}
@Override
public Movie getMovieDescriptor(int imdbid, Locale locale) throws Exception {
Movie result = getCache().getData("getMovieDescriptor", imdbid, locale, Movie.class);
if (result != null) {
return result;
}
// require login
login();
Movie movie = xmlrpc.getIMDBMovieDetails(imdbid);
getCache().putData("getMovieDescriptor", imdbid, locale, movie);
return movie;
}
public Movie getMovieDescriptor(File movieFile, Locale locale) throws Exception {
return getMovieDescriptors(singleton(movieFile), locale).get(movieFile);
}
@Override
public Map<File, Movie> getMovieDescriptors(Collection<File> movieFiles, Locale locale) throws Exception {
// create result array
Map<File, Movie> result = new HashMap<File, Movie>();
// compute movie hashes
Map<String, File> hashMap = new HashMap<String, File>(movieFiles.size());
for (File file : movieFiles) {
if (file.length() > HASH_CHUNK_SIZE) {
String hash = computeHash(file);
Movie entry = getCache().getData("getMovieDescriptor", hash, locale, Movie.class);
if (entry == null) {
hashMap.put(hash, file); // map file by hash
@ -320,46 +340,45 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
}
}
}
if (hashMap.size() > 0) {
// require login
login();
// dispatch query for all hashes
List<String> hashes = new ArrayList<String>(hashMap.keySet());
int batchSize = 50;
for (int bn = 0; bn < ceil((float) hashes.size() / batchSize); bn++) {
List<String> batch = hashes.subList(bn * batchSize, min((bn * batchSize) + batchSize, hashes.size()));
Set<String> unmatchedHashes = new HashSet<String>(batch);
int minSeenCount = 20; // make sure we don't get mismatches by making sure the hash has not been confirmed numerous times
for (Entry<String, Movie> it : xmlrpc.checkMovieHash(batch, minSeenCount).entrySet()) {
String hash = it.getKey();
Movie movie = it.getValue();
result.put(hashMap.get(hash), movie);
getCache().putData("getMovieDescriptor", hash, locale, movie);
unmatchedHashes.remove(hash);
}
// note hashes that are not matched to any items so we can ignore them in the future
for (String hash : unmatchedHashes) {
getCache().putData("getMovieDescriptor", hash, locale, new Movie("", -1, -1, -1));
}
}
}
return result;
}
@Override
public URI getSubtitleListLink(SearchResult searchResult, String languageName) {
Movie movie = (Movie) searchResult;
String sublanguageid = "all";
if (languageName != null) {
try {
sublanguageid = getSubLanguageID(languageName);
@ -367,32 +386,29 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
}
}
return URI.create(String.format("http://www.opensubtitles.org/en/search/imdbid-%d/sublanguageid-%s", movie.getImdbId(), sublanguageid));
}
public Locale detectLanguage(byte[] data) throws Exception {
// require login
login();
// detect language
List<String> languages = xmlrpc.detectLanguage(data);
// return first language
return languages.size() > 0 ? new Locale(languages.get(0)) : null;
}
public synchronized void login() throws Exception {
if (!xmlrpc.isLoggedOn()) {
xmlrpc.login(username, password, "en");
}
logoutTimer.set(10, TimeUnit.MINUTES, true);
}
protected synchronized void logout() {
if (xmlrpc.isLoggedOn()) {
try {
@ -401,19 +417,17 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Logout failed", e);
}
}
logoutTimer.cancel();
}
protected final Timer logoutTimer = new Timer() {
@Override
public void run() {
logout();
}
};
/**
* SubLanguageID by English language name
*/
@ -421,79 +435,72 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
protected synchronized Map<String, String> getSubLanguageMap() throws Exception {
Cache cache = Cache.getCache("web-datasource-lv2");
String cacheKey = getClass().getName() + ".subLanguageMap";
Map<String, String> subLanguageMap = cache.get(cacheKey, Map.class);
if (subLanguageMap == null) {
subLanguageMap = new HashMap<String, String>();
// fetch language data
for (Entry<String, String> entry : xmlrpc.getSubLanguages().entrySet()) {
// map id by name
subLanguageMap.put(entry.getValue().toLowerCase(), entry.getKey().toLowerCase());
}
// some additional special handling
subLanguageMap.put("brazilian", "pob");
// cache data
cache.put(cacheKey, subLanguageMap);
}
return subLanguageMap;
}
protected String getSubLanguageID(String languageName) throws Exception {
Map<String, String> subLanguageMap = getSubLanguageMap();
String key = languageName.toLowerCase();
if (!subLanguageMap.containsKey(key)) {
throw new IllegalArgumentException(String.format("SubLanguageID for '%s' not found", key));
}
return subLanguageMap.get(key);
}
protected String getLanguageName(String subLanguageID) throws Exception {
for (Entry<String, String> it : getSubLanguageMap().entrySet()) {
if (it.getValue().equals(subLanguageID.toLowerCase()))
return it.getKey();
}
return null;
}
protected static class ResultCache {
private final String id;
private final Cache cache;
public ResultCache(String id, Cache cache) {
this.id = id;
this.cache = cache;
}
protected String normalize(String query) {
return query == null ? null : query.trim().toLowerCase();
}
public <T extends SearchResult> List<T> putSearchResult(String method, String query, Locale locale, List<T> value) {
try {
cache.put(new Key(id, normalize(query)), value.toArray(new SearchResult[0]));
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesClient.class.getName()).log(Level.WARNING, e.getMessage());
}
return value;
}
@SuppressWarnings("unchecked")
public List<SearchResult> getSearchResult(String method, String query, Locale locale) {
try {
@ -504,22 +511,20 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesClient.class.getName()).log(Level.WARNING, e.getMessage(), e);
}
return null;
}
public List<SubtitleDescriptor> putSubtitleDescriptorList(Object key, String locale, List<SubtitleDescriptor> subtitles) {
try {
cache.put(new Key(id, key, locale), subtitles.toArray(new SubtitleDescriptor[0]));
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesClient.class.getName()).log(Level.WARNING, e.getMessage());
}
return subtitles;
}
public List<SubtitleDescriptor> getSubtitleDescriptorList(Object key, String locale) {
try {
SubtitleDescriptor[] descriptors = cache.get(new Key(id, key, locale), SubtitleDescriptor[].class);
@ -529,11 +534,10 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesClient.class.getName()).log(Level.WARNING, e.getMessage(), e);
}
return null;
}
public void putData(Object category, Object key, Locale locale, Object object) {
try {
cache.put(new Key(id, category, locale, key), object);
@ -541,8 +545,7 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
Logger.getLogger(OpenSubtitlesClient.class.getName()).log(Level.WARNING, e.getMessage());
}
}
public <T> T getData(Object category, Object key, Locale locale, Class<T> type) {
try {
T value = cache.get(new Key(id, category, locale, key), type);
@ -552,10 +555,10 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesClient.class.getName()).log(Level.WARNING, e.getMessage(), e);
}
return null;
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.filebot.web;
import static java.util.Collections.*;
import static net.sourceforge.tuned.StringUtilities.*;
@ -32,44 +30,40 @@ import redstone.xmlrpc.XmlRpcException;
import redstone.xmlrpc.XmlRpcFault;
import redstone.xmlrpc.util.Base64;
public class OpenSubtitlesXmlRpc {
private final String useragent;
private String token;
public OpenSubtitlesXmlRpc(String useragent) {
this.useragent = useragent;
}
/**
* Login as anonymous user
*/
public void loginAnonymous() throws XmlRpcFault {
login("", "", "en");
}
/**
* This will login user. This method should be called always when starting talking with
* server.
* This will login user. This method should be called always when starting talking with server.
*
* @param username username (blank for anonymous user)
* @param password password (blank for anonymous user)
* @param language ISO639 2-letter codes as language and later communication will be done in this language if
* applicable (error codes and so on).
* @param username
* username (blank for anonymous user)
* @param password
* password (blank for anonymous user)
* @param language
* ISO639 2-letter codes as language and later communication will be done in this language if applicable (error codes and so on).
*/
public synchronized void login(String username, String password, String language) throws XmlRpcFault {
Map<?, ?> response = invoke("LogIn", username, password, language, useragent);
// set session token
token = response.get("token").toString();
}
/**
* This will logout user (ends session id). Call this function is before closing the client program.
*/
@ -83,158 +77,148 @@ public class OpenSubtitlesXmlRpc {
token = null;
}
}
public boolean isLoggedOn() {
return token != null;
}
@SuppressWarnings("unchecked")
public Map<String, String> getServerInfo() throws XmlRpcFault {
return (Map<String, String>) invoke("ServerInfo", token);
}
public List<OpenSubtitlesSubtitleDescriptor> searchSubtitles(int imdbid, String... sublanguageids) throws XmlRpcFault {
return searchSubtitles(singleton(Query.forImdbId(imdbid, sublanguageids)));
}
@SuppressWarnings("unchecked")
public List<OpenSubtitlesSubtitleDescriptor> searchSubtitles(Collection<Query> queryList) throws XmlRpcFault {
List<OpenSubtitlesSubtitleDescriptor> subtitles = new ArrayList<OpenSubtitlesSubtitleDescriptor>();
Map<?, ?> response = invoke("SearchSubtitles", token, queryList);
try {
List<Map<String, String>> subtitleData = (List<Map<String, String>>) response.get("data");
for (Map<String, String> propertyMap : subtitleData) {
subtitles.add(new OpenSubtitlesSubtitleDescriptor(Property.asEnumMap(propertyMap)));
}
} catch (ClassCastException e) {
// no subtitle have been found
}
return subtitles;
}
@SuppressWarnings("unchecked")
public List<Movie> searchMoviesOnIMDB(String query) throws XmlRpcFault {
Map<?, ?> response = invoke("SearchMoviesOnIMDB", token, query);
List<Map<String, String>> movieData = (List<Map<String, String>>) response.get("data");
List<Movie> movies = new ArrayList<Movie>();
// title pattern
Pattern pattern = Pattern.compile("(.+)[(](\\d{4})([/]I+)?[)]");
for (Map<String, String> movie : movieData) {
try {
String imdbid = movie.get("id");
if (!imdbid.matches("\\d{1,7}"))
throw new IllegalArgumentException("Illegal IMDb movie ID: Must be a 7-digit number");
// match movie name and movie year from search result
Matcher matcher = pattern.matcher(movie.get("title"));
if (!matcher.find())
throw new IllegalArgumentException("Illegal title: Must be in 'name (year)' format");
String name = matcher.group(1).replaceAll("\"", "").trim();
int year = Integer.parseInt(matcher.group(2));
movies.add(new Movie(name, year, Integer.parseInt(imdbid), -1));
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesXmlRpc.class.getName()).log(Level.FINE, String.format("Ignore movie [%s]: %s", movie, e.getMessage()));
}
}
return movies;
}
@SuppressWarnings("unchecked")
public TryUploadResponse tryUploadSubtitles(SubFile... subtitles) throws XmlRpcFault {
Map<String, SubFile> struct = new HashMap<String, SubFile>();
// put cd1, cd2, ...
for (SubFile cd : subtitles) {
struct.put(String.format("cd%d", struct.size() + 1), cd);
}
Map<?, ?> response = invoke("TryUploadSubtitles", token, struct);
boolean uploadRequired = response.get("alreadyindb").equals("0");
boolean uploadRequired = response.get("alreadyindb").toString().equals("0");
List<Map<String, String>> subtitleData = new ArrayList<Map<String, String>>();
if (response.get("data") instanceof Map) {
subtitleData.add((Map<String, String>) response.get("data"));
} else if (response.get("data") instanceof List) {
subtitleData.addAll((List<Map<String, String>>) response.get("data"));
}
return new TryUploadResponse(uploadRequired, subtitleData);
}
public URI uploadSubtitles(BaseInfo baseInfo, SubFile... subtitles) throws XmlRpcFault {
Map<String, Object> struct = new HashMap<String, Object>();
// put cd1, cd2, ...
for (SubFile cd : subtitles) {
struct.put(String.format("cd%d", struct.size() + 1), cd);
}
// put baseinfo
struct.put("baseinfo", baseInfo);
Map<?, ?> response = invoke("UploadSubtitles", token, struct);
// subtitle link
return URI.create(response.get("data").toString());
}
@SuppressWarnings("unchecked")
public List<String> detectLanguage(byte[] data) throws XmlRpcFault {
// compress and base64 encode
String parameter = encodeData(data);
Map<String, Map<String, String>> response = (Map<String, Map<String, String>>) invoke("DetectLanguage", token, singleton(parameter));
List<String> languages = new ArrayList<String>(2);
if (response.containsKey("data")) {
languages.addAll(response.get("data").values());
}
return languages;
}
@SuppressWarnings("unchecked")
public Map<String, Integer> checkSubHash(Collection<String> hashes) throws XmlRpcFault {
Map<?, ?> response = invoke("CheckSubHash", token, hashes);
Map<String, ?> subHashData = (Map<String, ?>) response.get("data");
Map<String, Integer> subHashMap = new HashMap<String, Integer>();
for (Entry<String, ?> entry : subHashData.entrySet()) {
// non-existing subtitles are represented as Integer 0, not String "0"
subHashMap.put(entry.getKey(), Integer.parseInt(entry.getValue().toString()));
}
return subHashMap;
}
@SuppressWarnings("unchecked")
public Map<String, Movie> checkMovieHash(Collection<String> hashes, int minSeenCount) throws XmlRpcFault {
Map<String, Movie> movieHashMap = new HashMap<String, Movie>();
Map<?, ?> response = invoke("CheckMovieHash2", token, hashes);
Object payload = response.get("data");
if (payload instanceof Map) {
Map<String, ?> movieHashData = (Map<String, ?>) payload;
for (Entry<String, ?> entry : movieHashData.entrySet()) {
@ -242,24 +226,24 @@ public class OpenSubtitlesXmlRpc {
if (entry.getValue() instanceof List) {
String hash = entry.getKey();
List<Movie> matches = new ArrayList<Movie>();
List<?> hashMatches = (List<?>) entry.getValue();
for (Object match : hashMatches) {
if (match instanceof Map) {
Map<String, String> info = (Map<String, String>) match;
int seenCount = Integer.parseInt(info.get("SeenCount"));
// require minimum SeenCount before this hash match is considered trusted
if (seenCount >= minSeenCount) {
String name = info.get("MovieName");
int year = Integer.parseInt(info.get("MovieYear"));
int imdb = Integer.parseInt(info.get("MovieImdbID"));
matches.add(new Movie(name, year, imdb, -1));
}
}
}
if (matches.size() == 1) {
// perfect unambiguous match
movieHashMap.put(hash, matches.get(0));
@ -270,74 +254,68 @@ public class OpenSubtitlesXmlRpc {
}
}
}
return movieHashMap;
}
public Map<String, String> getSubLanguages() throws XmlRpcFault {
return getSubLanguages("en");
}
@SuppressWarnings("unchecked")
public Movie getIMDBMovieDetails(int imdbid) throws XmlRpcFault {
Map<?, ?> response = invoke("GetIMDBMovieDetails", token, imdbid);
try {
Map<String, String> data = (Map<String, String>) response.get("data");
String name = data.get("title");
int year = Integer.parseInt(data.get("year"));
return new Movie(name, year, imdbid, -1);
} catch (RuntimeException e) {
// ignore, invalid response
Logger.getLogger(getClass().getName()).log(Level.WARNING, String.format("Failed to lookup movie by imdbid %s: %s", imdbid, e.getMessage()));
}
return null;
}
@SuppressWarnings("unchecked")
public Map<String, String> getSubLanguages(String languageCode) throws XmlRpcFault {
Map<String, List<Map<String, String>>> response = (Map<String, List<Map<String, String>>>) invoke("GetSubLanguages", languageCode);
Map<String, String> subLanguageMap = new HashMap<String, String>();
for (Map<String, String> language : response.get("data")) {
subLanguageMap.put(language.get("SubLanguageID"), language.get("LanguageName"));
}
return subLanguageMap;
}
public void noOperation() throws XmlRpcFault {
invoke("NoOperation", token);
}
protected Map<?, ?> invoke(String method, Object... arguments) throws XmlRpcFault {
try {
XmlRpcClient rpc = new XmlRpcClient(getXmlRpcUrl(), false);
Map<?, ?> response = (Map<?, ?>) rpc.invoke(method, arguments);
checkResponse(response);
return response;
} catch (XmlRpcFault e) {
// invalidate session token if session has expired
if (e.getErrorCode() == 406)
token = null;
// rethrow exception
throw e;
}
}
protected URL getXmlRpcUrl() {
try {
return new URL("http://api.opensubtitles.org/xml-rpc");
@ -346,16 +324,15 @@ public class OpenSubtitlesXmlRpc {
throw new RuntimeException(e);
}
}
protected static String encodeData(byte[] data) {
try {
DeflaterInputStream compressedDataStream = new DeflaterInputStream(new ByteArrayInputStream(data));
// compress data
ByteBufferOutputStream buffer = new ByteBufferOutputStream(data.length);
buffer.transferFully(compressedDataStream);
// base64 encode
return new String(Base64.encode(buffer.getByteArray()));
} catch (IOException e) {
@ -363,166 +340,144 @@ public class OpenSubtitlesXmlRpc {
throw new RuntimeException(e);
}
}
/**
* Check whether status is OK or not
*
* @param status status code and message (e.g. 200 OK, 401 Unauthorized, ...)
* @throws XmlRpcFault thrown if status code is not OK
* @param status
* status code and message (e.g. 200 OK, 401 Unauthorized, ...)
* @throws XmlRpcFault
* thrown if status code is not OK
*/
protected void checkResponse(Map<?, ?> response) throws XmlRpcFault {
String status = (String) response.get("status");
// if there is no status at all, assume everything was OK
if (status == null || status.equals("200 OK")) {
return;
}
try {
throw new XmlRpcFault(new Scanner(status).nextInt(), status);
} catch (NoSuchElementException e) {
throw new XmlRpcException("Illegal status code: " + status);
}
}
public static final class Query extends HashMap<String, Object> implements Serializable {
private Query(String imdbid, String... sublanguageids) {
put("imdbid", imdbid);
put("sublanguageid", join(sublanguageids, ","));
}
private Query(String moviehash, String moviebytesize, String... sublanguageids) {
put("moviehash", moviehash);
put("moviebytesize", moviebytesize);
put("sublanguageid", join(sublanguageids, ","));
}
public static Query forHash(String moviehash, long moviebytesize, String... sublanguageids) {
return new Query(moviehash, Long.toString(moviebytesize), sublanguageids);
}
public static Query forImdbId(int imdbid, String... sublanguageids) {
return new Query(Integer.toString(imdbid), sublanguageids);
}
}
public static final class BaseInfo extends HashMap<String, Object> {
public void setIDMovieImdb(int imdb) {
put("idmovieimdb", Integer.toString(imdb));
}
public void setSubLanguageID(String sublanguageid) {
put("sublanguageid", sublanguageid);
}
public void setMovieReleaseName(String moviereleasename) {
put("moviereleasename", moviereleasename);
}
public void setMovieAka(String movieaka) {
put("movieaka", movieaka);
}
public void setSubAuthorComment(String subauthorcomment) {
put("subauthorcomment", subauthorcomment);
}
}
public static final class SubFile extends HashMap<String, Object> {
public void setSubHash(String subhash) {
put("subhash", subhash);
}
public void setSubFileName(String subfilename) {
put("subfilename", subfilename);
}
public void setMovieHash(String moviehash) {
put("moviehash", moviehash);
}
public void setMovieByteSize(long moviebytesize) {
put("moviebytesize", Long.toString(moviebytesize));
}
public void setMovieFileName(String moviefilename) {
put("moviefilename", moviefilename);
}
public void setSubContent(byte[] data) {
put("subcontent", encodeData(data));
}
public void setMovieTimeMS(String movietimems) {
if (movietimems.length() > 0) {
put("movietimems", movietimems);
}
}
public void setMovieFPS(String moviefps) {
if (moviefps.length() > 0) {
put("moviefps", moviefps);
}
}
public void setMovieFrames(String movieframes) {
if (movieframes.length() > 0) {
put("movieframes", movieframes);
}
}
}
public static final class TryUploadResponse {
private final boolean uploadRequired;
private final List<Map<String, String>> subtitleData;
private TryUploadResponse(boolean uploadRequired, List<Map<String, String>> subtitleData) {
this.uploadRequired = uploadRequired;
this.subtitleData = subtitleData;
}
public boolean isUploadRequired() {
return uploadRequired;
}
public List<Map<String, String>> getSubtitleData() {
return subtitleData;
}
@Override
public String toString() {
return String.format("TryUploadResponse: %s => %s", uploadRequired, subtitleData);
}
}
}

View File

@ -1,29 +1,42 @@
package net.sourceforge.filebot.web;
import java.io.File;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.swing.Icon;
public interface VideoHashSubtitleService {
public Map<File, List<SubtitleDescriptor>> getSubtitleList(File[] videoFiles, String languageName) throws Exception;
public boolean publishSubtitle(int imdbid, String languageName, File[] videoFile, File[] subtitleFile) throws Exception;
public String getName();
public URI getLink();
public Icon getIcon();
public CheckResult checkSubtitle(File videoFile, File subtitleFile) throws Exception;
public void uploadSubtitle(Object identity, Locale locale, File videoFile, File subtitleFile) throws Exception;
public static class CheckResult {
public final boolean exists;
public final Object identity;
public final Locale language;
public CheckResult(boolean exists, Object identity, Locale language) {
this.exists = exists;
this.identity = identity;
this.language = language;
}
@Override
public String toString() {
return String.format("%s [%s] => %s", identity, language, exists);
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.tuned;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
@ -9,29 +7,27 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public abstract class Timer implements Runnable {
private final ThreadFactory threadFactory = new DefaultThreadFactory("Timer", Thread.NORM_PRIORITY, true);
private ScheduledThreadPoolExecutor executor;
private ScheduledFuture<?> scheduledFuture;
private Thread shutdownHook;
public synchronized void set(long delay, TimeUnit unit, boolean runBeforeShutdown) {
// create executor if necessary
if (executor == null) {
executor = new ScheduledThreadPoolExecutor(1, threadFactory);
}
// cancel existing future task
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
Runnable runnable = this;
if (runBeforeShutdown) {
try {
addShutdownHook();
@ -39,10 +35,10 @@ public abstract class Timer implements Runnable {
// may fail if running with restricted permissions
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getClass().getName() + ": " + e.getMessage());
}
// remove shutdown hook after execution
runnable = new Runnable() {
@Override
public void run() {
try {
@ -61,30 +57,27 @@ public abstract class Timer implements Runnable {
Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getClass().getName() + ": " + e.getMessage());
}
}
scheduledFuture = executor.schedule(runnable, delay, unit);
}
public synchronized void cancel() {
removeShutdownHook();
// stop executor
executor.shutdownNow();
if (executor != null) {
executor.shutdownNow();
}
scheduledFuture = null;
executor = null;
}
private synchronized void addShutdownHook() {
if (shutdownHook == null) {
shutdownHook = new Thread(this);
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
}
private synchronized void removeShutdownHook() {
if (shutdownHook != null) {
try {
@ -98,5 +91,5 @@ public abstract class Timer implements Runnable {
}
}
}
}

View File

@ -1,7 +1,5 @@
package net.sourceforge.tuned.ui;
import static java.util.Collections.*;
import static javax.swing.JOptionPane.*;
@ -39,32 +37,28 @@ import javax.swing.plaf.basic.BasicTableUI;
import javax.swing.text.JTextComponent;
import javax.swing.undo.UndoManager;
public final class TunedUtilities {
public static final Color TRANSLUCENT = new Color(255, 255, 255, 0);
public static void checkEventDispatchThread() {
if (!SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Method must be accessed from the Swing Event Dispatch Thread, but was called on Thread \"" + Thread.currentThread().getName() + "\"");
}
}
public static Color interpolateHSB(Color c1, Color c2, float f) {
float[] hsb1 = Color.RGBtoHSB(c1.getRed(), c1.getGreen(), c1.getBlue(), null);
float[] hsb2 = Color.RGBtoHSB(c2.getRed(), c2.getGreen(), c2.getBlue(), null);
float[] hsb = new float[3];
for (int i = 0; i < hsb.length; i++) {
hsb[i] = hsb1[i] + ((hsb2[i] - hsb1[i]) * f);
}
return Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
}
public static String escapeHTML(String s) {
char[] sc = new char[] { '&', '<', '>', '"', '\'' };
for (char c : sc) {
@ -72,84 +66,76 @@ public final class TunedUtilities {
}
return s;
}
public static Color derive(Color color, float alpha) {
return new Color(((int) ((alpha * 255)) << 24) | (color.getRGB() & 0x00FFFFFF), true);
}
public static boolean isShiftOrAltDown(ActionEvent evt) {
return checkModifiers(evt.getModifiers(), ActionEvent.SHIFT_MASK) || checkModifiers(evt.getModifiers(), ActionEvent.ALT_MASK);
}
public static boolean checkModifiers(int modifiers, int mask) {
return ((modifiers & mask) == mask);
}
public static JButton createImageButton(Action action) {
JButton button = new JButton(action);
button.setHideActionText(true);
button.setOpaque(false);
return button;
}
public static void installAction(JComponent component, KeyStroke keystroke, Action action) {
Object key = action.getValue(Action.NAME);
if (key == null)
throw new IllegalArgumentException("Action must have a name");
component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keystroke, key);
component.getActionMap().put(key, action);
}
public static UndoManager installUndoSupport(JTextComponent component) {
final UndoManager undoSupport = new UndoManager();
// install undo listener
component.getDocument().addUndoableEditListener(undoSupport);
// install undo action
installAction(component, KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_MASK), new AbstractAction("Undo") {
@Override
public void actionPerformed(ActionEvent e) {
if (undoSupport.canUndo())
undoSupport.undo();
}
});
// install redo action
installAction(component, KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_MASK), new AbstractAction("Redo") {
@Override
public void actionPerformed(ActionEvent e) {
if (undoSupport.canRedo())
undoSupport.redo();
}
});
return undoSupport;
}
public static boolean isMaximized(Frame frame) {
return (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0;
}
public static List<String> showMultiValueInputDialog(final String text, final String initialValue, final String title, final Component parent) throws InvocationTargetException, InterruptedException {
String input = showInputDialog(text, initialValue, title, parent);
if (input == null || input.isEmpty()) {
return emptyList();
}
for (char separator : new char[] { '|', ';', ',' }) {
if (input.indexOf(separator) >= 0) {
List<String> values = new ArrayList<String>();
@ -158,116 +144,113 @@ public final class TunedUtilities {
values.add(field);
}
}
if (values.size() > 0) {
return values;
}
}
}
return singletonList(input);
}
public static String showInputDialog(final String text, final String initialValue, final String title, final Component parent) throws InvocationTargetException, InterruptedException {
final StringBuilder buffer = new StringBuilder();
SwingUtilities.invokeAndWait(new Runnable() {
Runnable runnable = new Runnable() {
@Override
public void run() {
Object value = JOptionPane.showInputDialog(parent, text, title, PLAIN_MESSAGE, null, null, initialValue);
if (value != null) {
buffer.append(value);
}
}
});
};
if (SwingUtilities.isEventDispatchThread()) {
runnable.run();
} else {
SwingUtilities.invokeAndWait(runnable);
}
return buffer.length() == 0 ? null : buffer.toString();
}
public static Window getWindow(Object component) {
if (component instanceof Window)
return (Window) component;
if (component instanceof Component)
return SwingUtilities.getWindowAncestor((Component) component);
return null;
}
public static Point getOffsetLocation(Window owner) {
if (owner == null) {
Window[] toplevel = Window.getOwnerlessWindows();
if (toplevel.length == 0)
return new Point(120, 80);
// assume first top-level window as point of reference
owner = toplevel[0];
}
Point p = owner.getLocation();
Dimension d = owner.getSize();
return new Point(p.x + d.width / 4, p.y + d.height / 7);
}
public static Image getImage(Icon icon) {
if (icon == null)
return null;
if (icon instanceof ImageIcon)
return ((ImageIcon) icon).getImage();
// draw icon into a new image
BufferedImage image = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
icon.paintIcon(null, g2d, 0, 0);
g2d.dispose();
return image;
}
public static Dimension getDimension(Icon icon) {
return new Dimension(icon.getIconWidth(), icon.getIconHeight());
}
public static Timer invokeLater(int delay, final Runnable runnable) {
Timer timer = new Timer(delay, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
runnable.run();
}
});
timer.setRepeats(false);
timer.start();
return timer;
}
/**
* When trying to drag a row of a multi-select JTable, it will start selecting rows instead of initiating a drag. This TableUI will give the JTable proper dnd behaviour.
*/
public static class DragDropRowTableUI extends BasicTableUI {
@Override
protected MouseInputListener createMouseInputListener() {
return new DragDropRowMouseInputHandler();
}
protected class DragDropRowMouseInputHandler extends MouseInputHandler {
@Override
public void mouseDragged(MouseEvent e) {
// Only do special handling if we are drag enabled with multiple selection
@ -279,13 +262,12 @@ public final class TunedUtilities {
}
}
}
/**
* Dummy constructor to prevent instantiation.
*/
private TunedUtilities() {
throw new UnsupportedOperationException();
}
}