diff --git a/source/net/sourceforge/filebot/media/MediaDetection.java b/source/net/sourceforge/filebot/media/MediaDetection.java index 5d484051..d09cacc6 100644 --- a/source/net/sourceforge/filebot/media/MediaDetection.java +++ b/source/net/sourceforge/filebot/media/MediaDetection.java @@ -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) */ diff --git a/source/net/sourceforge/filebot/resources/file.subtitle.png b/source/net/sourceforge/filebot/resources/file.subtitle.png index 0fb90d9e..d68a56c6 100644 Binary files a/source/net/sourceforge/filebot/resources/file.subtitle.png and b/source/net/sourceforge/filebot/resources/file.subtitle.png differ diff --git a/source/net/sourceforge/filebot/resources/file.unknown.png b/source/net/sourceforge/filebot/resources/file.unknown.png index d78eca87..661d8c72 100644 Binary files a/source/net/sourceforge/filebot/resources/file.unknown.png and b/source/net/sourceforge/filebot/resources/file.unknown.png differ diff --git a/source/net/sourceforge/filebot/resources/file.video.png b/source/net/sourceforge/filebot/resources/file.video.png new file mode 100644 index 00000000..6e26d9c0 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/file.video.png differ diff --git a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java index b5265c40..1a0fd9a0 100644 --- a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java +++ b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java @@ -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 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 searchTextField = new SelectButtonTextField(); - + protected final EventList 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 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 getSearchEngineLabelProvider(); - - + protected abstract Settings getSettings(); - - + protected abstract RequestProcessor createRequestProcessor(); - - + private void search(RequestProcessor 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 createSearchHistory() { // create in-memory history BasicEventList history = new BasicEventList(); - - // 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 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, Void> { - + private final RequestProcessor requestProcessor; - - + public SearchTask(RequestProcessor requestProcessor) { this.requestProcessor = requestProcessor; } - - + @Override protected Collection 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 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, Void> { - + private final RequestProcessor requestProcessor; - - + public FetchTask(RequestProcessor requestProcessor) { this.requestProcessor = requestProcessor; } - - + @Override protected final Collection 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 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 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 { - + protected final R request; - + private FileBotTab tab; - + private SearchResult searchResult; - + private long duration = 0; - - + public RequestProcessor(R request, JComponent component) { this.request = request; this.tab = new FileBotTab(component); } - - + public abstract Collection search() throws Exception; - - + public abstract Collection fetch() throws Exception; - - + public abstract void process(Collection 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 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 searchResults, Window window) throws Exception { // multiple results have been found, user must select one SelectDialog selectDialog = new SelectDialog(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 selectDialog) { selectDialog.setLocation(getOffsetLocation(selectDialog.getOwner())); selectDialog.setIconImage(getImage(getIcon())); selectDialog.setMinimumSize(new Dimension(250, 150)); } - - + public long getDuration() { return duration; } - + } - + } diff --git a/source/net/sourceforge/filebot/ui/Language.java b/source/net/sourceforge/filebot/ui/Language.java index d63b749e..2b94b7e6 100644 --- a/source/net/sourceforge/filebot/ui/Language.java +++ b/source/net/sourceforge/filebot/ui/Language.java @@ -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 ALPHABETIC_ORDER = new Comparator() { - + @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 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 availableLanguages() { ResourceBundle bundle = ResourceBundle.getBundle(Language.class.getName()); return getLanguages(bundle.getString("languages.all").split(",")); } - - + public static List commonLanguages() { ResourceBundle bundle = ResourceBundle.getBundle(Language.class.getName()); return getLanguages(bundle.getString("languages.common").split(",")); } - - + public static List preferredLanguages() { Set codes = new LinkedHashSet(); - + // 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])); } - + } diff --git a/source/net/sourceforge/filebot/ui/LanguageComboBox.java b/source/net/sourceforge/filebot/ui/LanguageComboBox.java index 6f3835f1..39804d7c 100644 --- a/source/net/sourceforge/filebot/ui/LanguageComboBox.java +++ b/source/net/sourceforge/filebot/ui/LanguageComboBox.java @@ -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 persistentSelectedLanguage; - private final PreferencesList persistentFavoriteLanguages; - - public LanguageComboBox(JComponent parent, Language initialSelection) { - super(new LanguageComboBoxModel(initialSelection != ALL_LANGUAGES, initialSelection)); + private Entry persistentSelectedLanguage; + private List 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(null, null); + persistentFavoriteLanguages = new ArrayList(); + } + // 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() { - + persistentFavoriteLanguages.clear(); + persistentFavoriteLanguages.addAll(new AbstractList() { + @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) { - + } } - + } diff --git a/source/net/sourceforge/filebot/ui/LanguageComboBoxModel.java b/source/net/sourceforge/filebot/ui/LanguageComboBoxModel.java index 79b53a2b..75d3911e 100644 --- a/source/net/sourceforge/filebot/ui/LanguageComboBoxModel.java +++ b/source/net/sourceforge/filebot/ui/LanguageComboBoxModel.java @@ -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 favorites = new Favorites(2); - + private List 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 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 implements Set { - + private final List data; - + private final int capacity; - - + public Favorites(int capacity) { this.data = new ArrayList(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(); } } - + } diff --git a/source/net/sourceforge/filebot/ui/episodelist/EpisodeListPanel.java b/source/net/sourceforge/filebot/ui/episodelist/EpisodeListPanel.java index b8ab83f1..bc46d57e 100644 --- a/source/net/sourceforge/filebot/ui/episodelist/EpisodeListPanel.java +++ b/source/net/sourceforge/filebot/ui/episodelist/EpisodeListPanel.java @@ -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 { - + 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 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 AbstractSearchPanelFileExportHandler of the currently selected tab */ @@ -162,41 +151,36 @@ public class EpisodeListPanel extends AbstractSearchPanel { - + public EpisodeListRequestProcessor(EpisodeListRequest request) { super(request, new EpisodeListTab()); } - - + @Override public Collection search() throws Exception { return request.provider.search(request.getSearchText(), request.language); } - - + @Override public Collection fetch() throws Exception { List episodes = request.provider.getEpisodeList(getSearchResult(), request.order, request.language); - + if (request.season != ALL_SEASONS) { List episodeForSeason = filterBySeason(episodes, request.season); if (episodeForSeason.isEmpty()) { @@ -231,108 +212,97 @@ public class EpisodeListPanel extends AbstractSearchPanel episodes) { // set a proper title for the export handler before adding episodes getComponent().setTitle(getTitle()); - + getComponent().getModel().addAll(episodes); } - - + @Override public String getStatusMessage(Collection 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 selectDialog) { super.configureSelectDialog(selectDialog); selectDialog.getHeaderLabel().setText("Select a Show:"); } - + } - - + protected static class EpisodeListTab extends FileBotList { - + 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 list) { super(list); } - - + @Override public Transferable createTransferable(JComponent c) { Transferable episodeArray = new ArrayTransferable(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(episodes); Transferable stringSelection = new StringSelection(StringUtilities.join(episodes, "\n")); - + clipboard.setContents(new CompositeTranserable(episodeArray, stringSelection), null); } } - + } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitleAutoMatchDialog.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitleAutoMatchDialog.java index 43c4b777..83b90f00 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitleAutoMatchDialog.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitleAutoMatchDialog.java @@ -1,7 +1,5 @@ - package net.sourceforge.filebot.ui.subtitle; - import static javax.swing.BorderFactory.*; import static javax.swing.JOptionPane.*; import static net.sourceforge.filebot.media.MediaDetection.*; @@ -74,36 +72,33 @@ import net.sourceforge.tuned.ui.EmptySelectionModel; import net.sourceforge.tuned.ui.LinkButton; import net.sourceforge.tuned.ui.RoundBorder; - class SubtitleAutoMatchDialog extends JDialog { - + private static final Color hashMatchColor = new Color(0xFAFAD2); // LightGoldenRodYellow private static final Color nameMatchColor = new Color(0xFFEBCD); // BlanchedAlmond private final JPanel hashMatcherServicePanel = createServicePanel(hashMatchColor); private final JPanel nameMatcherServicePanel = createServicePanel(nameMatchColor); - + private final List services = new ArrayList(); private final JTable subtitleMappingTable = createTable(); - + private ExecutorService queryService; private ExecutorService downloadService; - - + public SubtitleAutoMatchDialog(Window owner) { super(owner, "Download Subtitles", ModalityType.DOCUMENT_MODAL); - + JComponent content = (JComponent) getContentPane(); content.setLayout(new MigLayout("fill, insets dialog, nogrid", "", "[fill][pref!]")); - + content.add(new JScrollPane(subtitleMappingTable), "grow, wrap"); content.add(hashMatcherServicePanel, "gap after rel"); content.add(nameMatcherServicePanel, "gap after indent*2"); - + content.add(new JButton(downloadAction), "tag ok"); content.add(new JButton(finishAction), "tag cancel"); } - - + protected JPanel createServicePanel(Color color) { JPanel panel = new JPanel(new MigLayout("hidemode 3")); panel.setBorder(new RoundBorder()); @@ -112,65 +107,60 @@ class SubtitleAutoMatchDialog extends JDialog { panel.setVisible(false); return panel; } - - + protected JTable createTable() { JTable table = new JTable(new SubtitleMappingTableModel()); table.setDefaultRenderer(SubtitleMapping.class, new SubtitleMappingOptionRenderer()); - + table.setRowHeight(24); table.setIntercellSpacing(new Dimension(5, 5)); - + table.setBackground(Color.white); table.setAutoCreateRowSorter(true); table.setFillsViewportHeight(true); - + JComboBox editor = new SimpleComboBox(); editor.setRenderer(new SubtitleOptionRenderer()); - + // disable selection table.setSelectionModel(new EmptySelectionModel()); editor.setFocusable(false); - + table.setDefaultEditor(SubtitleMapping.class, new DefaultCellEditor(editor) { - + @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { JComboBox editor = (JComboBox) super.getTableCellEditorComponent(table, value, isSelected, row, column); - + SubtitleMapping mapping = (SubtitleMapping) value; editor.setModel(new DefaultComboBoxModel(mapping.getOptions())); editor.setSelectedItem(mapping.getSelectedOption()); - + return editor; } }); - + return table; } - - + public void setVideoFiles(File[] videoFiles) { subtitleMappingTable.setModel(new SubtitleMappingTableModel(videoFiles)); } - - + public void addSubtitleService(VideoHashSubtitleService service) { addSubtitleService(new VideoHashSubtitleServiceBean(service), hashMatcherServicePanel); } - - + public void addSubtitleService(SubtitleProvider service) { addSubtitleService(new SubtitleProviderBean(service, this), nameMatcherServicePanel); } - - + protected void addSubtitleService(final SubtitleServiceBean service, final JPanel servicePanel) { final LinkButton component = new LinkButton(service.getName(), ResourceManager.getIcon("database"), service.getLink()); component.setVisible(false); - + service.addPropertyChangeListener(new PropertyChangeListener() { - + @Override public void propertyChange(PropertyChangeEvent evt) { if (service.getState() == StateValue.STARTED) { @@ -178,22 +168,21 @@ class SubtitleAutoMatchDialog extends JDialog { } else { component.setIcon(ResourceManager.getIcon(service.getError() == null ? "database.ok" : "database.error")); } - + component.setVisible(true); component.setToolTipText(service.getError() == null ? null : service.getError().getMessage()); servicePanel.setVisible(true); servicePanel.getParent().revalidate(); } }); - + services.add(service); servicePanel.add(component); } - + // remember last user input private List userQuery = new ArrayList(); - - + protected List getUserQuery(String suggestion, String title, Component parent) throws Exception { synchronized (userQuery) { if (userQuery.isEmpty()) { @@ -202,24 +191,23 @@ class SubtitleAutoMatchDialog extends JDialog { return userQuery; } } - - + public void startQuery(String languageName) { final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); QueryTask queryTask = new QueryTask(services, mappingModel.getVideoFiles(), languageName, SubtitleAutoMatchDialog.this) { - + @Override protected void process(List>> sequence) { for (Map> subtitles : sequence) { // update subtitle options for (SubtitleMapping subtitleMapping : mappingModel) { List options = subtitles.get(subtitleMapping.getVideoFile()); - + if (options != null && options.size() > 0) { subtitleMapping.addOptions(options); } } - + // make subtitle column visible if (subtitles.size() > 0) { mappingModel.setOptionColumnVisible(true); @@ -227,66 +215,65 @@ class SubtitleAutoMatchDialog extends JDialog { } } }; - + queryService = Executors.newFixedThreadPool(1); queryService.submit(queryTask); } - - + private Boolean showConfirmReplaceDialog(List files) { JList existingFilesComponent = new JList(files.toArray()) { - + @Override public Dimension getPreferredScrollableViewportSize() { // adjust component size return new Dimension(80, 50); } }; - + Object[] message = new Object[] { "Replace existing subtitle files?", new JScrollPane(existingFilesComponent) }; Object[] options = new Object[] { "Replace All", "Skip All", "Cancel" }; JOptionPane optionPane = new JOptionPane(message, WARNING_MESSAGE, YES_NO_CANCEL_OPTION, null, options); - + // display option dialog optionPane.createDialog(SubtitleAutoMatchDialog.this, "Replace").setVisible(true); - + // replace all if (options[0] == optionPane.getValue()) return true; - + // skip all if (options[1] == optionPane.getValue()) return false; - + // cancel return null; } - + private final Action downloadAction = new AbstractAction("Download", ResourceManager.getIcon("dialog.continue")) { - + @Override public void actionPerformed(ActionEvent evt) { // disable any active cell editor if (subtitleMappingTable.getCellEditor() != null) { - subtitleMappingTable.getCellEditor().cancelCellEditing(); + subtitleMappingTable.getCellEditor().stopCellEditing(); } - + // don't allow restart of download as long as there are still unfinished download tasks if (downloadService != null && !downloadService.isTerminated()) { return; } - + final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); - + // collect the subtitles that will be fetched List downloadQueue = new ArrayList(); - + for (final SubtitleMapping mapping : mappingModel) { SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); - + if (subtitleBean != null && subtitleBean.getState() == null) { downloadQueue.add(new DownloadTask(mapping.getVideoFile(), subtitleBean) { - + @Override protected void done() { try { @@ -298,94 +285,91 @@ class SubtitleAutoMatchDialog extends JDialog { }); } } - + // collect downloads that will override a file List confirmReplaceDownloadQueue = new ArrayList(); List existingFiles = new ArrayList(); - + for (DownloadTask download : downloadQueue) { // target destination may not be known until files are extracted from archives File target = download.getDestination(null); - + if (target != null && target.exists()) { confirmReplaceDownloadQueue.add(download); existingFiles.add(target.getName()); } } - + // confirm replace if (confirmReplaceDownloadQueue.size() > 0) { Boolean option = showConfirmReplaceDialog(existingFiles); - + // abort the operation altogether if (option == null) { return; } - + // don't replace any files if (option == false) { downloadQueue.removeAll(confirmReplaceDownloadQueue); } } - + // start download if (downloadQueue.size() > 0) { downloadService = Executors.newFixedThreadPool(2); - + for (DownloadTask downloadTask : downloadQueue) { downloadTask.getSubtitleBean().setState(StateValue.PENDING); downloadService.execute(downloadTask); } - + // terminate after all downloads have been completed downloadService.shutdown(); } } }; - + private final Action finishAction = new AbstractAction("Close", ResourceManager.getIcon("dialog.cancel")) { - + @Override public void actionPerformed(ActionEvent evt) { if (queryService != null) { queryService.shutdownNow(); } - + if (downloadService != null) { downloadService.shutdownNow(); } - + setVisible(false); dispose(); } }; - - + private static class SubtitleMappingOptionRenderer extends DefaultTableCellRenderer { - + private final JComboBox optionComboBox = new SimpleComboBox(); - - + public SubtitleMappingOptionRenderer() { optionComboBox.setRenderer(new SubtitleOptionRenderer()); } - - + @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { SubtitleMapping mapping = (SubtitleMapping) value; SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); - + // render combobox for subtitle options if (mapping.isEditable()) { optionComboBox.setModel(new DefaultComboBoxModel(new Object[] { subtitleBean })); return optionComboBox; } - + // render status label super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); setForeground(table.getForeground()); - + if (subtitleBean == null) { // no subtitles found setText("No subtitles found"); @@ -407,26 +391,24 @@ class SubtitleAutoMatchDialog extends JDialog { setText(null); setIcon(null); } - + return this; } } - - + private static class SubtitleOptionRenderer extends DefaultListCellRenderer { - + private final Border padding = createEmptyBorder(3, 3, 3, 3); - - + @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { super.getListCellRendererComponent(list, null, index, isSelected, cellHasFocus); setBorder(padding); - + SubtitleDescriptorBean subtitleBean = (SubtitleDescriptorBean) value; setText(subtitleBean.getText()); setIcon(subtitleBean.getError() == null ? subtitleBean.getIcon() : ResourceManager.getIcon("status.warning")); - + if (!isSelected) { float f = subtitleBean.getMatchProbability(); if (f < 1) { @@ -438,136 +420,120 @@ class SubtitleAutoMatchDialog extends JDialog { setBackground(derive(Color.RED, (1 - f) * 0.5f)); } } - + return this; } } - - + private static class SubtitleMappingTableModel extends AbstractTableModel implements Iterable { - + private final SubtitleMapping[] data; - + private boolean optionColumnVisible = false; - - + public SubtitleMappingTableModel(File... videoFiles) { data = new SubtitleMapping[videoFiles.length]; - + for (int i = 0; i < videoFiles.length; i++) { data[i] = new SubtitleMapping(videoFiles[i]); data[i].addPropertyChangeListener(new SubtitleMappingListener(i)); } } - - + public List getVideoFiles() { return new AbstractList() { - + @Override public File get(int index) { return data[index].getVideoFile(); } - - + @Override public int size() { return data.length; } }; } - - + @Override public Iterator iterator() { return Arrays.asList(data).iterator(); } - - + public void setOptionColumnVisible(boolean optionColumnVisible) { if (this.optionColumnVisible == optionColumnVisible) return; - + this.optionColumnVisible = optionColumnVisible; - + // update columns fireTableStructureChanged(); } - - + @Override public int getColumnCount() { return optionColumnVisible ? 2 : 1; } - - + @Override public String getColumnName(int column) { switch (column) { - case 0: - return "Video"; - case 1: - return "Subtitle"; + case 0: + return "Video"; + case 1: + return "Subtitle"; } - + return null; } - - + @Override public int getRowCount() { return data.length; } - - + @Override public Object getValueAt(int row, int column) { switch (column) { - case 0: - return data[row].getVideoFile().getName(); - case 1: - return data[row]; + case 0: + return data[row].getVideoFile().getName(); + case 1: + return data[row]; } - + return null; } - - + @Override public void setValueAt(Object value, int row, int column) { data[row].setSelectedOption((SubtitleDescriptorBean) value); } - - + @Override public boolean isCellEditable(int row, int column) { return column == 1 && data[row].isEditable(); } - - + @Override public Class getColumnClass(int column) { switch (column) { - case 0: - return String.class; - case 1: - return SubtitleMapping.class; + case 0: + return String.class; + case 1: + return SubtitleMapping.class; } - + return null; } - - + private class SubtitleMappingListener implements PropertyChangeListener { - + private final int index; - - + public SubtitleMappingListener(int index) { this.index = index; } - - + @Override public void propertyChange(PropertyChangeEvent evt) { // update state and subtitle options @@ -575,216 +541,191 @@ class SubtitleAutoMatchDialog extends JDialog { } } } - - + private static class SubtitleMapping extends AbstractBean { - + private File videoFile; private File subtitleFile; - + private SubtitleDescriptorBean selectedOption; private List options = new ArrayList(); - - + public SubtitleMapping(File videoFile) { this.videoFile = videoFile; } - - + public File getVideoFile() { return videoFile; } - - + public File getSubtitleFile() { return subtitleFile; } - - + public void setSubtitleFile(File subtitleFile) { this.subtitleFile = subtitleFile; firePropertyChange("subtitleFile", null, this.subtitleFile); } - - + public boolean isEditable() { return subtitleFile == null && selectedOption != null && (selectedOption.getState() == null || selectedOption.getError() != null); } - - + public SubtitleDescriptorBean getSelectedOption() { return selectedOption; } - - + public void setSelectedOption(SubtitleDescriptorBean selectedOption) { if (this.selectedOption != null) { this.selectedOption.removePropertyChangeListener(selectedOptionListener); } - + this.selectedOption = selectedOption; this.selectedOption.addPropertyChangeListener(selectedOptionListener); - + firePropertyChange("selectedOption", null, this.selectedOption); } - - + public SubtitleDescriptorBean[] getOptions() { return options.toArray(new SubtitleDescriptorBean[0]); } - - + public void addOptions(List options) { this.options.addAll(options); - + if (selectedOption == null && options.size() > 0) { setSelectedOption(options.get(0)); } } - + private final PropertyChangeListener selectedOptionListener = new PropertyChangeListener() { - + @Override public void propertyChange(PropertyChangeEvent evt) { firePropertyChange("selectedOption", null, selectedOption); } }; } - - + private static class SubtitleDescriptorBean extends AbstractBean { - + private final File videoFile; private final SubtitleDescriptor descriptor; private final SubtitleServiceBean service; - + private StateValue state; private Exception error; - - + public SubtitleDescriptorBean(File videoFile, SubtitleDescriptor descriptor, SubtitleServiceBean service) { this.videoFile = videoFile; this.descriptor = descriptor; this.service = service; } - - + public float getMatchProbability() { return service.getMatchProbabilty(videoFile, descriptor); } - - + public String getText() { return formatSubtitle(descriptor.getName(), getLanguageName(), getType()); } - - + public Icon getIcon() { return service.getIcon(); } - - + public String getLanguageName() { return descriptor.getLanguageName(); } - - + public String getType() { return descriptor.getType(); } - - + public MemoryFile fetch() throws Exception { setState(StateValue.STARTED); - + try { MemoryFile data = fetchSubtitle(descriptor); Analytics.trackEvent(service.getName(), "DownloadSubtitle", descriptor.getLanguageName(), 1); - + return data; } catch (Exception e) { // remember exception error = e; - + // rethrow exception throw e; } finally { setState(StateValue.DONE); } } - - + public Exception getError() { return error; } - - + public StateValue getState() { return state; } - - + public void setState(StateValue state) { this.state = state; firePropertyChange("state", null, this.state); } - - + @Override public String toString() { return getText(); } } - - + private static class QueryTask extends SwingWorker, Map>> { - + private final Component parent; private final Collection services; - + private final Collection remainingVideos; private final String languageName; - - + public QueryTask(Collection services, Collection videoFiles, String languageName, Component parent) { this.parent = parent; this.services = services; this.remainingVideos = new TreeSet(videoFiles); this.languageName = languageName; } - - + @Override protected Collection doInBackground() throws Exception { for (SubtitleServiceBean service : services) { if (isCancelled() || Thread.interrupted()) { throw new CancellationException(); } - + if (remainingVideos.isEmpty()) { break; } - + try { Map> subtitleSet = new HashMap>(); for (final Entry> result : service.lookupSubtitles(remainingVideos, languageName, parent).entrySet()) { List subtitles = new ArrayList(); - + // associate subtitles with services for (SubtitleDescriptor subtitleDescriptor : result.getValue()) { subtitles.add(new SubtitleDescriptorBean(result.getKey(), subtitleDescriptor, service)); } - + subtitleSet.put(result.getKey(), subtitles); } - + // only lookup subtitles for remaining videos for (Entry> it : subtitleSet.entrySet()) { if (it.getValue() != null && it.getValue().size() > 0) { remainingVideos.remove(it.getKey()); } } - + publish(subtitleSet); } catch (CancellationException e) { // don't ignore cancellation @@ -797,183 +738,160 @@ class SubtitleAutoMatchDialog extends JDialog { Logger.getLogger(SubtitleAutoMatchDialog.class.getName()).log(Level.WARNING, e.getMessage()); } } - + return remainingVideos; } } - - + private static class DownloadTask extends SwingWorker { - + private final File video; private final SubtitleDescriptorBean descriptor; - - + public DownloadTask(File video, SubtitleDescriptorBean descriptor) { this.video = video; this.descriptor = descriptor; } - - + public SubtitleDescriptorBean getSubtitleBean() { return descriptor; } - - + public File getDestination(MemoryFile subtitle) { if (descriptor.getType() == null && subtitle == null) return null; - + // prefer type from descriptor because we need to know before we download the actual subtitle file String base = FileUtilities.getName(video); String ext = (descriptor.getType() != null) ? descriptor.getType() : getExtension(subtitle.getName()); return new File(video.getParentFile(), formatSubtitle(base, descriptor.getLanguageName(), ext)); } - - + @Override protected File doInBackground() { try { // fetch subtitle MemoryFile subtitle = descriptor.fetch(); - + if (isCancelled()) return null; - + // save to file File destination = getDestination(subtitle); writeFile(subtitle.getData(), destination); - + return destination; } catch (Exception e) { Logger.getLogger(SubtitleAutoMatchDialog.class.getName()).log(Level.WARNING, e.getMessage(), e); } - + return null; } } - - + protected static abstract class SubtitleServiceBean extends AbstractBean { - + private final String name; private final Icon icon; private final URI link; - + private StateValue state = StateValue.PENDING; private Throwable error = null; - - + public SubtitleServiceBean(String name, Icon icon, URI link) { this.name = name; this.icon = icon; this.link = link; } - - + public String getName() { return name; } - - + public Icon getIcon() { return icon; } - - + public URI getLink() { return link; } - - + public abstract float getMatchProbabilty(File videoFile, SubtitleDescriptor descriptor); - - + protected abstract Map> getSubtitleList(Collection files, String languageName, Component parent) throws Exception; - - + public final Map> lookupSubtitles(Collection files, String languageName, Component parent) throws Exception { setState(StateValue.STARTED); - + try { return getSubtitleList(files, languageName, parent); } catch (Exception e) { // remember error error = e; - + // rethrow error throw e; } finally { setState(StateValue.DONE); } } - - + private void setState(StateValue state) { this.state = state; firePropertyChange("state", null, this.state); } - - + public StateValue getState() { return state; } - - + public Throwable getError() { return error; } } - - + protected static class VideoHashSubtitleServiceBean extends SubtitleServiceBean { - + private VideoHashSubtitleService service; - - + public VideoHashSubtitleServiceBean(VideoHashSubtitleService service) { super(service.getName(), service.getIcon(), service.getLink()); this.service = service; } - - + @Override protected Map> getSubtitleList(Collection files, String languageName, Component parent) throws Exception { return service.getSubtitleList(files.toArray(new File[0]), languageName); } - - + @Override public float getMatchProbabilty(File videoFile, SubtitleDescriptor descriptor) { return 1; } } - - + protected static class SubtitleProviderBean extends SubtitleServiceBean { - + private SubtitleAutoMatchDialog inputProvider; private SubtitleProvider service; - - + public SubtitleProviderBean(SubtitleProvider service, SubtitleAutoMatchDialog inputProvider) { super(service.getName(), service.getIcon(), service.getLink()); this.service = service; this.inputProvider = inputProvider; } - - + @Override protected Map> getSubtitleList(Collection files, String languageName, Component parent) throws Exception { // ignore clutter files from processing files = filter(files, not(getClutterFileFilter())); - + // auto-detect query and search for subtitles Collection querySet = new TreeSet(String.CASE_INSENSITIVE_ORDER); - + // auto-detect series names querySet.addAll(detectSeriesNames(files, Locale.ROOT)); - + // auto-detect movie names for (File f : files) { if (!isEpisode(f.getName(), false)) { @@ -982,40 +900,40 @@ class SubtitleAutoMatchDialog extends JDialog { } } } - + List subtitles = findSubtitles(service, querySet, languageName); - + // if auto-detection fails, ask user for input if (subtitles.isEmpty()) { // dialog may have been cancelled by now if (Thread.interrupted()) { throw new CancellationException(); } - + querySet = inputProvider.getUserQuery(join(querySet, ","), service.getName(), parent); subtitles = findSubtitles(service, querySet, languageName); - + // still no luck... na women ye mei banfa if (subtitles.isEmpty()) { throw new Exception("Unable to lookup subtitles: " + querySet); } } - + // files by possible subtitles matches Map> subtitlesByFile = new HashMap>(); for (File file : files) { subtitlesByFile.put(file, new ArrayList()); } - + // first match everything as best as possible, then filter possibly bad matches for (Entry it : matchSubtitles(files, subtitles, false).entrySet()) { subtitlesByFile.get(it.getKey()).add(it.getValue()); } - + // add other possible matches to the options SimilarityMetric sanity = EpisodeMetrics.verificationMetric(); float minMatchSimilarity = 0.5f; - + for (File file : files) { // add matching subtitles for (SubtitleDescriptor it : subtitles) { @@ -1024,16 +942,15 @@ class SubtitleAutoMatchDialog extends JDialog { } } } - + return subtitlesByFile; } - - + @Override public float getMatchProbabilty(File videoFile, SubtitleDescriptor descriptor) { SimilarityMetric metric = new MetricCascade(EpisodeMetrics.SeasonEpisode, EpisodeMetrics.AirDate, EpisodeMetrics.Name); return 0.9f * metric.getSimilarity(videoFile, descriptor); } } - + } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java index 253c95e8..4f6a39c7 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitleDropTarget.java @@ -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 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 videosMappedBySubtitle) { - // TODO implement upload - throw new UnsupportedOperationException("Not implemented yet"); - } - - - private boolean handleDrop(List 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 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 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 files) { - List 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 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 files); + + protected abstract DropAction getDropAction(List 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 extensions = new ArrayList(); 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 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 input) { + // accept video files and folders + return filter(input, VIDEO_FILES, FOLDERS).size() > 0 ? DropAction.Accept : DropAction.Cancel; + } + + @Override + protected boolean handleDrop(List input) { + // perform a drop action depending on the given files + final Collection videoFiles = new TreeSet(); + + // 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 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 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 input) { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + // perform a drop action depending on the given files + final Collection files = new TreeSet(); + + // video files only + files.addAll(filter(input, FILES)); + files.addAll(listFiles(filter(input, FOLDERS), 5, false)); + + final List videos = filter(files, VIDEO_FILES); + final List subtitles = filter(files, SUBTITLE_FILES); + + final Map uploadPlan = new LinkedHashMap(); + 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 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 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"); + } + } + + } + } diff --git a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java index 2e875a4e..311291b2 100644 --- a/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/subtitle/SubtitlePanel.java @@ -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 { - - 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 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 { - + public SubtitleRequestProcessor(SubtitleRequest request) { super(request, new SubtitleDownloadComponent()); } - - + @Override public Collection search() throws Exception { return request.getProvider().search(request.getSearchText()); } - - + @Override public Collection fetch() throws Exception { List packages = new ArrayList(); - + 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 subtitles) { getComponent().setLanguageVisible(request.getLanguageName() == null); getComponent().getPackageModel().addAll(subtitles); } - - + @Override public SubtitleDownloadComponent getComponent() { return (SubtitleDownloadComponent) super.getComponent(); } - - + @Override public String getStatusMessage(Collection 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 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 0) { + List options = database.searchMovie(input, Locale.ENGLISH); + if (options.size() > 0) { + SelectDialog dialog = new SelectDialog(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 uploadPlan) { + List mappings = new ArrayList(uploadPlan.size()); + for (Entry 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 { + + 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 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 { + + 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; + } + } + +} diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java index 5480b739..61283c90 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java @@ -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 search(String query) throws Exception { List result = getCache().getSearchResult("search", query, null); if (result != null) { return result; } - + // require login login(); - + try { // search for movies / series List 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 getSubtitleList(SearchResult searchResult, String languageName) throws Exception { List 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> 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 hashMap = new HashMap(files.length); Map> resultMap = new HashMap>(files.length); - + // create hash query for each file List queryList = new ArrayList(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 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()); } } - + 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 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 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 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 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 getMovieDescriptors(Collection movieFiles, Locale locale) throws Exception { // create result array Map result = new HashMap(); - + // compute movie hashes Map hashMap = new HashMap(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 hashes = new ArrayList(hashMap.keySet()); int batchSize = 50; for (int bn = 0; bn < ceil((float) hashes.size() / batchSize); bn++) { List batch = hashes.subList(bn * batchSize, min((bn * batchSize) + batchSize, hashes.size())); Set unmatchedHashes = new HashSet(batch); - + int minSeenCount = 20; // make sure we don't get mismatches by making sure the hash has not been confirmed numerous times for (Entry 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 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 getSubLanguageMap() throws Exception { Cache cache = Cache.getCache("web-datasource-lv2"); String cacheKey = getClass().getName() + ".subLanguageMap"; - + Map subLanguageMap = cache.get(cacheKey, Map.class); - + if (subLanguageMap == null) { subLanguageMap = new HashMap(); - + // fetch language data for (Entry 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 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 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 List putSearchResult(String method, String query, Locale locale, List 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 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 putSubtitleDescriptorList(Object key, String locale, List 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 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 getData(Object category, Object key, Locale locale, Class 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; } - + } - + } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java b/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java index 3b548d61..04628026 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java @@ -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 getServerInfo() throws XmlRpcFault { return (Map) invoke("ServerInfo", token); } - - + public List searchSubtitles(int imdbid, String... sublanguageids) throws XmlRpcFault { return searchSubtitles(singleton(Query.forImdbId(imdbid, sublanguageids))); } - - + @SuppressWarnings("unchecked") public List searchSubtitles(Collection queryList) throws XmlRpcFault { List subtitles = new ArrayList(); Map response = invoke("SearchSubtitles", token, queryList); - + try { List> subtitleData = (List>) response.get("data"); - + for (Map propertyMap : subtitleData) { subtitles.add(new OpenSubtitlesSubtitleDescriptor(Property.asEnumMap(propertyMap))); } } catch (ClassCastException e) { // no subtitle have been found } - + return subtitles; } - - + @SuppressWarnings("unchecked") public List searchMoviesOnIMDB(String query) throws XmlRpcFault { Map response = invoke("SearchMoviesOnIMDB", token, query); - + List> movieData = (List>) response.get("data"); List movies = new ArrayList(); - + // title pattern Pattern pattern = Pattern.compile("(.+)[(](\\d{4})([/]I+)?[)]"); - + for (Map 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 struct = new HashMap(); - + // 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> subtitleData = new ArrayList>(); - + if (response.get("data") instanceof Map) { subtitleData.add((Map) response.get("data")); } else if (response.get("data") instanceof List) { subtitleData.addAll((List>) response.get("data")); } - + return new TryUploadResponse(uploadRequired, subtitleData); } - - + public URI uploadSubtitles(BaseInfo baseInfo, SubFile... subtitles) throws XmlRpcFault { Map struct = new HashMap(); - + // 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 detectLanguage(byte[] data) throws XmlRpcFault { // compress and base64 encode String parameter = encodeData(data); - + Map> response = (Map>) invoke("DetectLanguage", token, singleton(parameter)); List languages = new ArrayList(2); - + if (response.containsKey("data")) { languages.addAll(response.get("data").values()); } - + return languages; } - - + @SuppressWarnings("unchecked") public Map checkSubHash(Collection hashes) throws XmlRpcFault { Map response = invoke("CheckSubHash", token, hashes); - + Map subHashData = (Map) response.get("data"); Map subHashMap = new HashMap(); - + for (Entry 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 checkMovieHash(Collection hashes, int minSeenCount) throws XmlRpcFault { Map movieHashMap = new HashMap(); - + Map response = invoke("CheckMovieHash2", token, hashes); Object payload = response.get("data"); - + if (payload instanceof Map) { Map movieHashData = (Map) payload; for (Entry entry : movieHashData.entrySet()) { @@ -242,24 +226,24 @@ public class OpenSubtitlesXmlRpc { if (entry.getValue() instanceof List) { String hash = entry.getKey(); List matches = new ArrayList(); - + List hashMatches = (List) entry.getValue(); for (Object match : hashMatches) { if (match instanceof Map) { Map info = (Map) 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 getSubLanguages() throws XmlRpcFault { return getSubLanguages("en"); } - - + @SuppressWarnings("unchecked") public Movie getIMDBMovieDetails(int imdbid) throws XmlRpcFault { Map response = invoke("GetIMDBMovieDetails", token, imdbid); - + try { Map data = (Map) 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 getSubLanguages(String languageCode) throws XmlRpcFault { Map>> response = (Map>>) invoke("GetSubLanguages", languageCode); - + Map subLanguageMap = new HashMap(); - + for (Map 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 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 { - + 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 { - + 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> subtitleData; - - + private TryUploadResponse(boolean uploadRequired, List> subtitleData) { this.uploadRequired = uploadRequired; this.subtitleData = subtitleData; } - - + public boolean isUploadRequired() { return uploadRequired; } - - + public List> getSubtitleData() { return subtitleData; } - - + @Override public String toString() { return String.format("TryUploadResponse: %s => %s", uploadRequired, subtitleData); } } - + } diff --git a/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java b/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java index 6c71406c..92f67d5d 100644 --- a/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java +++ b/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java @@ -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> 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); + } + } + } diff --git a/source/net/sourceforge/tuned/Timer.java b/source/net/sourceforge/tuned/Timer.java index 4d3fb3ab..430fea48 100644 --- a/source/net/sourceforge/tuned/Timer.java +++ b/source/net/sourceforge/tuned/Timer.java @@ -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 { } } } - + } diff --git a/source/net/sourceforge/tuned/ui/TunedUtilities.java b/source/net/sourceforge/tuned/ui/TunedUtilities.java index 140c8c8b..508cb7b5 100644 --- a/source/net/sourceforge/tuned/ui/TunedUtilities.java +++ b/source/net/sourceforge/tuned/ui/TunedUtilities.java @@ -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 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 values = new ArrayList(); @@ -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(); } - + }