package net.filebot.ui.subtitle; import static net.filebot.Logging.*; import static net.filebot.Settings.*; import static net.filebot.ui.LanguageComboBoxModel.*; import static net.filebot.util.FileUtilities.*; import static net.filebot.util.ui.SwingUI.*; import java.awt.Color; import java.awt.Cursor; import java.awt.Dialog.ModalityType; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.geom.Path2D; import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JRootPane; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.border.TitledBorder; import com.google.common.eventbus.Subscribe; import net.filebot.Language; import net.filebot.ResourceManager; import net.filebot.Settings; import net.filebot.WebServices; import net.filebot.media.MediaDetection; import net.filebot.ui.AbstractSearchPanel; import net.filebot.ui.LanguageComboBox; import net.filebot.ui.SelectDialog; import net.filebot.ui.subtitle.SubtitleDropTarget.DropAction; import net.filebot.ui.transfer.FileTransferable; import net.filebot.util.ui.LabelProvider; import net.filebot.util.ui.LinkButton; import net.filebot.util.ui.SimpleLabelProvider; import net.filebot.web.OpenSubtitlesClient; import net.filebot.web.SearchResult; import net.filebot.web.SubtitleDescriptor; import net.filebot.web.SubtitleProvider; import net.filebot.web.SubtitleSearchResult; import net.filebot.web.VideoHashSubtitleService; import net.miginfocom.swing.MigLayout; public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, SubtitlePackage> { private LanguageComboBox languageComboBox = new LanguageComboBox(ALL_LANGUAGES, getSettings()); public SubtitlePanel() { historyPanel.setColumnHeader(0, "Search"); historyPanel.setColumnHeader(1, "Number of Subtitles"); // add after text field add(languageComboBox, "gap indent, sgy button", 1); add(createImageButton(setUserAction), "w pref!, h 2+pref!, gap rel, sgy button", 2); // add at the top right corner 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); } @Subscribe public void handle(Transferable transferable) throws Exception { SubtitleDropTarget target = downloadDropTarget; List<File> files = FileTransferable.getFilesFromTransferable(transferable); if (files != null && files.size() > 0 && target.getDropAction(files) != DropAction.Cancel) { target.handleDrop(files); } } private final SubtitleDropTarget uploadDropTarget = new SubtitleDropTarget.Upload() { @Override 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.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() { public Locale getLocale() { return languageComboBox.getModel().getSelectedItem() == ALL_LANGUAGES ? Locale.ROOT : languageComboBox.getModel().getSelectedItem().getLocale(); } @Override public VideoHashSubtitleService[] getVideoHashSubtitleServices() { return WebServices.getVideoHashSubtitleServices(getLocale()); } @Override public SubtitleProvider[] getSubtitleProviders() { return WebServices.getSubtitleProviders(getLocale()); } @Override public OpenSubtitlesClient getSubtitleService() { return WebServices.OpenSubtitles; }; @Override public Locale getQueryLanguage() { // use currently selected language for drop target return languageComboBox.getModel().getSelectedItem() == ALL_LANGUAGES ? null : languageComboBox.getModel().getSelectedItem().getLocale(); } @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 Collection<String> getHistory(SubtitleProvider engine) throws Exception { List<String> names = new ArrayList<String>(); for (SubtitleSearchResult it : MediaDetection.releaseInfo.getOpenSubtitlesIndex()) { names.add(it.toString()); } return names; }; @Override protected SubtitleProvider[] getSearchEngines() { return WebServices.getSubtitleProviders(getLocale()); } @Override protected LabelProvider<SubtitleProvider> getSearchEngineLabelProvider() { return SimpleLabelProvider.forClass(SubtitleProvider.class); } @Override protected Settings getSettings() { return Settings.forPackage(SubtitlePanel.class); } @Override protected SubtitleRequestProcessor createRequestProcessor() { SubtitleProvider provider = searchTextField.getSelectButton().getSelectedValue(); if (provider instanceof OpenSubtitlesClient && ((OpenSubtitlesClient) provider).isAnonymous() && !isAppStore()) { log.info(String.format("%s: Please enter your login details first.", ((OpenSubtitlesClient) provider).getName())); // automatically open login dialog SwingUtilities.invokeLater(() -> setUserAction.actionPerformed(new ActionEvent(searchTextField, 0, "login"))); return null; } // parse query String query = searchTextField.getText(); int season = seasonFilter.match(query); query = seasonFilter.remove(query).trim(); int episode = episodeFilter.match(query); query = episodeFilter.remove(query).trim(); Language language = languageComboBox.getModel().getSelectedItem(); return new SubtitleRequestProcessor(new SubtitleRequest(provider, query, season, episode, language)); } private final QueryFilter<Integer> seasonFilter = new QueryFilter<Integer>("season", s -> s == null ? -1 : Integer.parseInt(s)); private final QueryFilter<Integer> episodeFilter = new QueryFilter<Integer>("episode", s -> s == null ? -1 : Integer.parseInt(s)); protected static class QueryFilter<T> { private final Pattern pattern; private final Function<String, T> parser; public QueryFilter(String key, Function<String, T> parser) { this.pattern = Pattern.compile("(?:" + key + "):(\\w+)", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS); this.parser = parser; } public T match(String s) { Matcher m = pattern.matcher(s); if (m.find()) { return parser.apply(m.group(m.groupCount())); } return parser.apply(null); } public String remove(String s) { return pattern.matcher(s).replaceAll(""); } } protected static class SubtitleRequest extends Request { private final SubtitleProvider provider; private final Language language; private final int season; private final int episode; public SubtitleRequest(SubtitleProvider provider, String searchText, int season, int episode, Language language) { super(searchText); this.season = season; this.episode = episode; this.provider = provider; this.language = language; } public SubtitleProvider getProvider() { return provider; } public Locale getLanguage() { return language == ALL_LANGUAGES ? null : language.getLocale(); } public int[][] getEpisodeFilter() { return season >= 0 && episode >= 0 ? new int[][] { new int[] { season, episode } } : season >= 0 ? new int[][] { new int[] { season, -1 } } : null; } } protected static class SubtitleRequestProcessor extends RequestProcessor<SubtitleRequest, SubtitlePackage> { public SubtitleRequestProcessor(SubtitleRequest request) { super(request, new SubtitleDownloadComponent()); } @Override public Collection<SubtitleSearchResult> search() throws Exception { return request.getProvider().search(request.getSearchText()); } @Override public SubtitleSearchResult getSearchResult() { return (SubtitleSearchResult) super.getSearchResult(); } @Override public Collection<SubtitlePackage> fetch() throws Exception { List<SubtitlePackage> packages = new ArrayList<SubtitlePackage>(); for (SubtitleDescriptor subtitle : request.getProvider().getSubtitleList(getSearchResult(), request.getEpisodeFilter(), request.getLanguage())) { packages.add(new SubtitlePackage(request.getProvider(), subtitle)); } return packages; } @Override public URI getLink() { return request.getProvider().getSubtitleListLink(getSearchResult(), request.getLanguage()); } @Override public void process(Collection<SubtitlePackage> subtitles) { getComponent().setLanguageVisible(request.getLanguage() == null); getComponent().getPackageModel().addAll(subtitles); } @Override public SubtitleDownloadComponent getComponent() { return (SubtitleDownloadComponent) super.getComponent(); } @Override public String getStatusMessage(Collection<SubtitlePackage> result) { return (result.isEmpty()) ? "No subtitles found" : String.format("%d subtitles", result.size()); } @Override public Icon getIcon() { return request.provider.getIcon(); } @Override protected void configureSelectDialog(SelectDialog<SearchResult> selectDialog) { super.configureSelectDialog(selectDialog); selectDialog.getMessageLabel().setText("Select a Show / Movie:"); } } protected final Action setUserAction = new AbstractAction("Login", 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 unrel"); // restore values String[] osdbAuth = WebServices.getLogin(WebServices.LOGIN_OPENSUBTITLES); osdbUser.setText(osdbAuth[0]); // osdbPass.setText(osdbAuth[1]); // password is stored as MD5 hash so we can't restore it if (osdbUser.getText().isEmpty()) { osdbGroup.add(new LinkButton("Register Account", "Register to increase your download quota", WebServices.OpenSubtitles.getIcon(), URI.create("http://www.opensubtitles.org/en/newuser")), "spanx 2, tag left"); } else { osdbGroup.add(new LinkButton("Upgrade Account", "Upgrade to increase your download quota", WebServices.OpenSubtitles.getIcon(), URI.create("http://www.opensubtitles.org/en/support")), "spanx 2, tag left"); } JRootPane container = authPanel.getRootPane(); container.setLayout(new MigLayout("fill, insets dialog")); container.removeAll(); container.add(osdbGroup, "growx, wrap"); Action ok = new AbstractAction("OK", ResourceManager.getIcon("dialog.continue")) { @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) { final OpenSubtitlesClient osdb = new OpenSubtitlesClient(getApplicationName(), getApplicationVersion()); osdb.setUser(osdbUser.getText(), md5(new String(osdbPass.getPassword()))); osdb.login(); // do some status checks in background (since OpenSubtitles can be really really slow) WebServices.requestThreadPool.submit(() -> { try { // check download quota for the current user Map<?, ?> limits = (Map<?, ?>) osdb.getServerInfo().get("download_limits"); log.log(Level.INFO, String.format("Your daily download quota is at %s of %s.", limits.get("client_24h_download_count"), limits.get("client_24h_download_limit"))); // logout from test session osdb.logout(); } catch (Exception e) { debug.warning(e.getMessage()); } }); } else if (osdbUser.getText().isEmpty()) { WebServices.setLogin(WebServices.LOGIN_OPENSUBTITLES, null, null); // delete login details } } catch (Exception e) { log.log(Level.WARNING, "OpenSubtitles: " + e.getMessage()); approved = false; } authPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); if (approved) { WebServices.setLogin(WebServices.LOGIN_OPENSUBTITLES, osdbUser.getText(), new String(osdbPass.getPassword())); authPanel.setVisible(false); } } }; Action cancel = new AbstractAction("Cancel", ResourceManager.getIcon("dialog.cancel")) { @Override public void actionPerformed(ActionEvent evt) { authPanel.setVisible(false); } }; container.add(new JButton(cancel), "tag cancel, split 2"); container.add(new JButton(ok), "tag ok"); authPanel.pack(); authPanel.setVisible(true); } }; }