From f897837811be08413936572257e73bf84f6cee67 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Fri, 3 Jul 2009 10:06:33 +0000 Subject: [PATCH] * ignore non-subtitles files when extracting archives * download subtitle package on keystroke ENTER * renamed OpenSubtitlesClient to OpenSubtitlesXmlRpc * renamed OpenSubtitlesSubtitleClient to OpenSubtitlesClient * refactoring --- .../filebot/subtitle/MicroDVDReader.java | 7 +- .../filebot/subtitle/SubRipReader.java | 2 +- .../subtitle/SubStationAlphaReader.java | 3 +- .../filebot/subtitle/SubViewerReader.java | 29 +-- .../filebot/subtitle/SubtitleFormat.java | 8 - .../filebot/subtitle/SubtitleReader.java | 8 +- .../subtitle/SubtitleDownloadComponent.java | 80 ++++-- .../ui/panel/subtitle/SubtitlePackage.java | 17 +- .../ui/panel/subtitle/SubtitlePanel.java | 4 +- .../filebot/web/OpenSubtitlesClient.java | 235 +++++------------- .../web/OpenSubtitlesSubtitleClient.java | 102 -------- .../web/OpenSubtitlesSubtitleDescriptor.java | 46 +++- .../filebot/web/OpenSubtitlesXmlRpc.java | 234 +++++++++++++++++ 13 files changed, 421 insertions(+), 354 deletions(-) delete mode 100644 source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java create mode 100644 source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java diff --git a/source/net/sourceforge/filebot/subtitle/MicroDVDReader.java b/source/net/sourceforge/filebot/subtitle/MicroDVDReader.java index 3d7daa14..4b8bd3bd 100644 --- a/source/net/sourceforge/filebot/subtitle/MicroDVDReader.java +++ b/source/net/sourceforge/filebot/subtitle/MicroDVDReader.java @@ -3,7 +3,6 @@ package net.sourceforge.filebot.subtitle; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Scanner; import java.util.regex.Pattern; @@ -40,8 +39,10 @@ public class MicroDVDReader extends SubtitleReader { from = to + 1; } - if (properties.size() < 2) + if (properties.size() < 2) { + // ignore illegal lines return null; + } long startFrame = Long.parseLong(properties.get(0)); long endFrame = Long.parseLong(properties.get(1)); @@ -56,7 +57,7 @@ public class MicroDVDReader extends SubtitleReader { } // translate '|' to new lines - List lines = Arrays.asList(text.split(Pattern.quote("|"))); + String[] lines = text.split(Pattern.quote("|")); // convert frame interval to time interval return new SubtitleElement(Math.round(startFrame * fps), Math.round(endFrame * fps), join(lines, "\n")); diff --git a/source/net/sourceforge/filebot/subtitle/SubRipReader.java b/source/net/sourceforge/filebot/subtitle/SubRipReader.java index f1fb7da0..05b6e3e7 100644 --- a/source/net/sourceforge/filebot/subtitle/SubRipReader.java +++ b/source/net/sourceforge/filebot/subtitle/SubRipReader.java @@ -44,7 +44,7 @@ public class SubRipReader extends SubtitleReader { lines.add(line); } - return new SubtitleElement(t1, t2, join(lines, "\n")); + return new SubtitleElement(t1, t2, join(lines.toArray(), "\n")); } } diff --git a/source/net/sourceforge/filebot/subtitle/SubStationAlphaReader.java b/source/net/sourceforge/filebot/subtitle/SubStationAlphaReader.java index b2850c88..4006ffaf 100644 --- a/source/net/sourceforge/filebot/subtitle/SubStationAlphaReader.java +++ b/source/net/sourceforge/filebot/subtitle/SubStationAlphaReader.java @@ -74,9 +74,10 @@ public class SubStationAlphaReader extends SubtitleReader { long end = timeFormat.parse(row[format.get("End")]).getTime(); String text = row[format.get("Text")].trim(); + // translate "\\n" to new lines String[] lines = Pattern.compile(Pattern.quote("\\N"), Pattern.CASE_INSENSITIVE).split(text); - return new SubtitleElement(start, end, join(Arrays.asList(lines), "\n")); + return new SubtitleElement(start, end, join(lines, "\n")); } } diff --git a/source/net/sourceforge/filebot/subtitle/SubViewerReader.java b/source/net/sourceforge/filebot/subtitle/SubViewerReader.java index 8619be72..029c5b0c 100644 --- a/source/net/sourceforge/filebot/subtitle/SubViewerReader.java +++ b/source/net/sourceforge/filebot/subtitle/SubViewerReader.java @@ -3,8 +3,7 @@ package net.sourceforge.filebot.subtitle; import java.text.DateFormat; -import java.util.ArrayList; -import java.util.List; +import java.util.InputMismatchException; import java.util.Scanner; import java.util.regex.Pattern; @@ -24,20 +23,22 @@ public class SubViewerReader extends SubtitleReader { // element starts with interval (e.g. 00:42:16.33,00:42:19.39) String[] interval = scanner.nextLine().split(",", 2); - if (interval.length < 2 || interval[0].startsWith("[")) + if (interval.length < 2 || interval[0].startsWith("[")) { + // ignore property lines return null; - - long t1 = timeFormat.parse(interval[0]).getTime(); - long t2 = timeFormat.parse(interval[1]).getTime(); - - // append subtitle line - List lines = new ArrayList(2); - - for (String text : scanner.nextLine().split(Pattern.quote("[br]"))) { - lines.add(text); } - return new SubtitleElement(t1, t2, join(lines, "\n")); + try { + long t1 = timeFormat.parse(interval[0]).getTime(); + long t2 = timeFormat.parse(interval[1]).getTime(); + + // translate [br] to new lines + String[] lines = scanner.nextLine().split(Pattern.quote("[br]")); + + return new SubtitleElement(t1, t2, join(lines, "\n")); + } catch (InputMismatchException e) { + // can't parse interval, ignore line + return null; + } } - } diff --git a/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java b/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java index 24cbe2b0..c35878b4 100644 --- a/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java +++ b/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java @@ -40,14 +40,6 @@ public enum SubtitleFormat { public SubtitleReader newReader(Readable readable) { return new SubStationAlphaReader(new Scanner(readable)); } - }, - - SAMI { - - @Override - public SubtitleReader newReader(Readable readable) { - throw new UnsupportedOperationException("SAMI reader not implemented"); - } }; public abstract SubtitleReader newReader(Readable readable); diff --git a/source/net/sourceforge/filebot/subtitle/SubtitleReader.java b/source/net/sourceforge/filebot/subtitle/SubtitleReader.java index 781addbc..c7dd9663 100644 --- a/source/net/sourceforge/filebot/subtitle/SubtitleReader.java +++ b/source/net/sourceforge/filebot/subtitle/SubtitleReader.java @@ -65,13 +65,13 @@ public abstract class SubtitleReader implements Iterator, Close } - protected String join(Iterable values, String delimiter) { + protected String join(Object[] values, String delimiter) { StringBuilder sb = new StringBuilder(); - for (Iterator iterator = values.iterator(); iterator.hasNext();) { - sb.append(iterator.next()); + for (int i = 0; i < values.length; i++) { + sb.append(values[i]); - if (iterator.hasNext()) { + if (i < values.length - 1) { sb.append(delimiter); } } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java index 63369fc2..e50c39a5 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java @@ -18,6 +18,7 @@ import java.io.InputStreamReader; import java.nio.channels.FileChannel; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Comparator; import java.util.Deque; import java.util.List; import java.util.concurrent.CancellationException; @@ -46,6 +47,7 @@ import ca.odell.glazedlists.FilterList; import ca.odell.glazedlists.GlazedLists; import ca.odell.glazedlists.ListSelection; import ca.odell.glazedlists.ObservableElementList; +import ca.odell.glazedlists.SortedList; import ca.odell.glazedlists.TextFilterator; import ca.odell.glazedlists.matchers.MatcherEditor; import ca.odell.glazedlists.swing.EventListModel; @@ -77,7 +79,7 @@ public class SubtitleDownloadComponent extends JComponent { public SubtitleDownloadComponent() { - JList packageList = new JList(createPackageListModel()); + final JList packageList = new JList(createPackageListModel()); packageList.setFixedCellHeight(32); packageList.setCellRenderer(renderer); @@ -122,6 +124,15 @@ public class SubtitleDownloadComponent extends JComponent { scrollPane.setViewportBorder(new LineBorder(fileList.getBackground())); add(scrollPane, "newline, hmin max(80px, 30%)"); + // install fetch action + TunedUtilities.installAction(packageList, KeyStroke.getKeyStroke("ENTER"), new AbstractAction("Fetch") { + + @Override + public void actionPerformed(ActionEvent e) { + fetchAll(packageList.getSelectedValues()); + } + }); + // install open action TunedUtilities.installAction(fileList, KeyStroke.getKeyStroke("ENTER"), new AbstractAction("Open") { @@ -159,8 +170,20 @@ public class SubtitleDownloadComponent extends JComponent { protected ListModel createFileListModel() { + // source list + EventList source = getFileModel(); + + // sort by name + source = new SortedList(source, new Comparator() { + + @Override + public int compare(MemoryFile m1, MemoryFile m2) { + return m1.getName().compareToIgnoreCase(m2.getName()); + } + }); + // as list model - return new EventListModel(getFileModel()); + return new EventListModel(source); } @@ -233,7 +256,12 @@ public class SubtitleDownloadComponent extends JComponent { private void open(Object[] selection) { try { for (Object object : selection) { - open((MemoryFile) object); + MemoryFile file = (MemoryFile) object; + + // only open subtitle files + if (SUBTITLE_FILES.accept(file.getName())) { + open(file); + } } } catch (Exception e) { Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e); @@ -260,7 +288,7 @@ public class SubtitleDownloadComponent extends JComponent { try { if (reader.hasNext()) { - // correct format + // correct format found List list = new ArrayList(500); // read subtitle file @@ -275,15 +303,13 @@ public class SubtitleDownloadComponent extends JComponent { viewer.setData(list); viewer.setVisible(true); - // done + // we're done return; } } finally { reader.close(); } } - - throw new IOException("Cannot read subtitle format"); } @@ -447,27 +473,29 @@ public class SubtitleDownloadComponent extends JComponent { final Object[] selection = list.getSelectedValues(); - JPopupMenu contextMenu = new JPopupMenu(); - - // Open - contextMenu.add(new AbstractAction("Open") { + if (selection.length > 0) { + JPopupMenu contextMenu = new JPopupMenu(); - @Override - public void actionPerformed(ActionEvent evt) { - open(selection); - } - }); - - // Save as ... - contextMenu.add(new AbstractAction("Save as ...") { + // Open + contextMenu.add(new AbstractAction("Open") { + + @Override + public void actionPerformed(ActionEvent evt) { + open(selection); + } + }); - @Override - public void actionPerformed(ActionEvent evt) { - save(selection); - } - }); - - contextMenu.show(e.getComponent(), e.getX(), e.getY()); + // Save as ... + contextMenu.add(new AbstractAction("Save as ...") { + + @Override + public void actionPerformed(ActionEvent evt) { + save(selection); + } + }); + + contextMenu.show(e.getComponent(), e.getX(), e.getY()); + } } } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java index a88128cd..c8c2a5a5 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePackage.java @@ -3,6 +3,7 @@ package net.sourceforge.filebot.ui.panel.subtitle; import static java.util.Collections.*; +import static net.sourceforge.filebot.FileBotUtilities.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -181,15 +182,17 @@ public class SubtitlePackage { List vfs = new ArrayList(); for (MemoryFile file : archiveType.fromData(data)) { - // check if file is a supported archive - ArchiveType type = ArchiveType.forName(FileUtilities.getExtension(file.getName())); - - if (type == ArchiveType.UNDEFINED) { - // add subtitle file + if (SUBTITLE_FILES.accept(file.getName())) { + // add subtitle files, ignore non-subtitle files vfs.add(file); } else { - // extract nested archives recursively - vfs.addAll(extract(type, file.getData())); + // check if file is a supported archive + ArchiveType type = ArchiveType.forName(FileUtilities.getExtension(file.getName())); + + if (type != ArchiveType.UNDEFINED) { + // extract nested archives recursively + vfs.addAll(extract(type, file.getData())); + } } } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java index bfe8c029..76a9ac79 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java @@ -19,7 +19,7 @@ import javax.swing.JComboBox; import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.ui.AbstractSearchPanel; import net.sourceforge.filebot.ui.SelectDialog; -import net.sourceforge.filebot.web.OpenSubtitlesSubtitleClient; +import net.sourceforge.filebot.web.OpenSubtitlesClient; import net.sourceforge.filebot.web.SearchResult; import net.sourceforge.filebot.web.SublightSubtitleClient; import net.sourceforge.filebot.web.SubsceneSubtitleClient; @@ -79,7 +79,7 @@ public class SubtitlePanel extends AbstractSearchPanelISO639 - * 2-letter codes as language and later communication will be done in this - * language if applicable (error codes and so on). - */ - @SuppressWarnings("unchecked") - public synchronized void login(String username, String password, String language) throws XmlRpcFault { + @Override + public List search(String query) throws Exception { + // require login + login(); - Map response = (Map) invoke("LogIn", username, password, language, clientInfo); - checkStatus(response.get("status")); + @SuppressWarnings("unchecked") + List results = (List) xmlrpc.searchMoviesOnIMDB(query); - token = response.get("token"); + return results; } - /** - * This will logout user (ends session id). Good call this function is before ending - * (closing) clients program. - * - * @throws XmlRpcFault - */ - public synchronized void logout() throws XmlRpcFault { - try { - invoke("LogOut", token); - - // anonymous users will always get a 401 Unauthorized when trying to logout - // do not check status for logout response - // checkStatus(response.get("status")); - } finally { - token = null; + @Override + public List getSubtitleList(SearchResult searchResult, String languageName) throws Exception { + // require login + login(); + + @SuppressWarnings("unchecked") + List subtitles = (List) xmlrpc.searchSubtitles(((MovieDescriptor) searchResult).getImdbId(), languageName); + + return subtitles; + } + + + @Override + public URI getSubtitleListLink(SearchResult searchResult, String languageName) { + //TODO provide link + return null; + } + + + protected synchronized void login() throws Exception { + if (!xmlrpc.isLoggedOn()) { + xmlrpc.loginAnonymous(); } + + logoutTimer.set(10, TimeUnit.MINUTES, true); } - public synchronized boolean isLoggedOn() { - return token != null; - } - - - /** - * 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 - */ - private void checkStatus(String status) throws XmlRpcFault { - if (status.equals("200 OK")) - return; - - Matcher m = Pattern.compile("(\\d+).*").matcher(status); - - if (!m.matches()) - throw new XmlRpcException("Illegal status code: " + status); - - throw new XmlRpcFault(Integer.parseInt(m.group(1)), status); - } - - - private Object invoke(String method, Object... arguments) throws XmlRpcFault { - try { - XmlRpcClient rpc = new XmlRpcClient(url, false); - return rpc.invoke(method, arguments); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - - /** - * This simple function returns basic server info. - */ - @SuppressWarnings("unchecked") - public Map getServerInfo() throws XmlRpcFault { - return (Map) invoke("ServerInfo", token); - } - - - @SuppressWarnings("unchecked") - public List searchSubtitles(int imdbid, String languageName) throws XmlRpcFault { - - Map searchListEntry = new HashMap(2); - - // pad imdbId with zeros - //TODO needed??? - searchListEntry.put("imdbid", String.format("%07d", imdbid)); - searchListEntry.put("sublanguageid", getSubLanguageID(languageName)); - - List> searchList = Collections.singletonList(searchListEntry); - - Map>> response = (Map>>) invoke("SearchSubtitles", token, searchList); - - List subs = new ArrayList(); - - try { - for (Map subtitleData : response.get("data")) { - subs.add(new OpenSubtitlesSubtitleDescriptor(Property.asEnumMap(subtitleData))); + protected synchronized void logout() { + if (xmlrpc.isLoggedOn()) { + try { + xmlrpc.logout(); + } catch (Exception e) { + Logger.getLogger(getClass().getName()).log(Level.WARNING, "Logout failed", e); } - } catch (ClassCastException e) { - // if the response is an error message, generic types won't match - throw new XmlRpcException("Illegal response: " + response.toString(), e); } - return subs; + logoutTimer.cancel(); } - private String getSubLanguageID(String languageName) { - //TODO test if sublanguageid is really ISO3 language code - throw new UnsupportedOperationException("Not implemented"); - } - - - @SuppressWarnings("unchecked") - public List searchMoviesOnIMDB(String query) throws XmlRpcFault { + protected final Timer logoutTimer = new Timer() { - Map>> response = (Map>>) invoke("SearchMoviesOnIMDB", token, query); - - ArrayList movies = new ArrayList(); - - for (Map movie : response.get("data")) { - String title = movie.get("title"); - - // get end index of first non-aka title (aka titles are separated by Â) - int endIndex = title.indexOf('\u00C2'); - - if (endIndex > 0) { - title = title.substring(0, endIndex); - } - - movies.add(new MovieDescriptor(title, Integer.parseInt(movie.get("year")), Integer.parseInt(movie.get("id")))); + @Override + public void run() { + logout(); } - - return movies; - } - - - @SuppressWarnings("unchecked") - public boolean noOperation() { - try { - Map response = (Map) invoke("NoOperation", token); - checkStatus(response.get("status")); - - return true; - } catch (Exception e) { - return false; - } - } - + }; } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java deleted file mode 100644 index 22986ee8..00000000 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java +++ /dev/null @@ -1,102 +0,0 @@ - -package net.sourceforge.filebot.web; - - -import java.net.URI; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.swing.Icon; - -import net.sourceforge.filebot.ResourceManager; -import net.sourceforge.tuned.Timer; - - -/** - * SubtitleClient for OpenSubtitles. - */ -public class OpenSubtitlesSubtitleClient implements SubtitleProvider { - - private final OpenSubtitlesClient client; - - - public OpenSubtitlesSubtitleClient(String clientInfo) { - this.client = new OpenSubtitlesClient(clientInfo); - } - - - @Override - public String getName() { - return "OpenSubtitles"; - } - - - @Override - public Icon getIcon() { - return ResourceManager.getIcon("search.opensubtitles"); - } - - - @Override - public List search(String query) throws Exception { - // require login - login(); - - @SuppressWarnings("unchecked") - List results = (List) client.searchMoviesOnIMDB(query); - - return results; - } - - - @Override - public List getSubtitleList(SearchResult searchResult, String languageName) throws Exception { - // require login - login(); - - @SuppressWarnings("unchecked") - List subtitles = (List) client.searchSubtitles(((MovieDescriptor) searchResult).getImdbId(), languageName); - - return subtitles; - } - - - @Override - public URI getSubtitleListLink(SearchResult searchResult, String languageName) { - //TODO provide link - return null; - } - - - protected synchronized void login() throws Exception { - if (!client.isLoggedOn()) { - client.loginAnonymous(); - } - - logoutTimer.set(10, TimeUnit.MINUTES, true); - } - - - protected synchronized void logout() { - if (client.isLoggedOn()) { - try { - client.logout(); - } catch (Exception e) { - Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Logout failed", e); - } - } - - logoutTimer.cancel(); - } - - - protected final Timer logoutTimer = new Timer() { - - @Override - public void run() { - logout(); - } - }; -} diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java index 3f3befe1..940da790 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java @@ -2,18 +2,21 @@ package net.sourceforge.filebot.web; +import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; -import java.util.Collections; import java.util.EnumMap; import java.util.Map; import java.util.Map.Entry; +import java.util.zip.GZIPInputStream; + +import net.sourceforge.tuned.ByteBufferOutputStream; /** * Describes a subtitle on OpenSubtitles. * - * @see OpenSubtitlesClient + * @see OpenSubtitlesXmlRpc */ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { @@ -75,32 +78,49 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { } - public Map getProperties() { - return Collections.unmodifiableMap(properties); + public String getProperty(Property key) { + return properties.get(key); } @Override public String getName() { - return properties.get(Property.SubFileName); + return getProperty(Property.SubFileName); } @Override public String getLanguageName() { - return properties.get(Property.LanguageName); - } - - - @Override - public ByteBuffer fetch() throws Exception { - return WebRequest.fetch(new URL(properties.get(Property.ZipDownloadLink))); + return getProperty(Property.LanguageName); } @Override public String getType() { - return "zip"; + return getProperty(Property.SubFormat); + } + + + public int getSize() { + return Integer.parseInt(getProperty(Property.SubSize)); + } + + + @Override + public ByteBuffer fetch() throws Exception { + URL resource = new URL(getProperty(Property.SubDownloadLink)); + InputStream stream = new GZIPInputStream(resource.openStream()); + + try { + ByteBufferOutputStream buffer = new ByteBufferOutputStream(getSize()); + + // read all + buffer.transferFully(stream); + + return buffer.getByteBuffer(); + } finally { + stream.close(); + } } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java b/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java new file mode 100644 index 00000000..cd67e7cb --- /dev/null +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesXmlRpc.java @@ -0,0 +1,234 @@ + +package net.sourceforge.filebot.web; + + +import static java.util.Collections.*; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.InputMismatchException; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import redstone.xmlrpc.XmlRpcClient; +import redstone.xmlrpc.XmlRpcException; +import redstone.xmlrpc.XmlRpcFault; + +import net.sourceforge.filebot.web.OpenSubtitlesSubtitleDescriptor.Property; + + +/** + * Client for the OpenSubtitles XML-RPC API. + */ +public class OpenSubtitlesXmlRpc { + + private static final String url = "http://www.opensubtitles.org/xml-rpc"; + + private final String useragent; + + private String token; + + + public OpenSubtitlesXmlRpc(String useragent) { + this.useragent = useragent; + } + + + /** + * Login as anonymous user + * + * @throws XmlRpcFault + */ + public void loginAnonymous() throws XmlRpcFault { + login("", ""); + } + + + /** + * Login as user and use English as language + * + * @param username + * @param password + * @throws XmlRpcFault + */ + public void login(String username, String password) throws XmlRpcFault { + login(username, password, "en"); + } + + + /** + * 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). + */ + @SuppressWarnings("unchecked") + public synchronized void login(String username, String password, String language) throws XmlRpcFault { + Map response = (Map) invoke("LogIn", username, password, language, useragent); + checkStatus(response.get("status")); + + token = response.get("token"); + } + + + /** + * This will logout user (ends session id). Good call this function is before ending + * (closing) clients program. + * + * @throws XmlRpcFault + */ + public synchronized void logout() throws XmlRpcFault { + try { + invoke("LogOut", token); + + // anonymous users will always get a 401 Unauthorized when trying to logout + // do not check status for logout response + // checkStatus(response.get("status")); + } finally { + token = null; + } + } + + + public synchronized boolean isLoggedOn() { + return token != null; + } + + + /** + * 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 + */ + private void checkStatus(String status) throws XmlRpcFault { + if (status.equals("200 OK")) + return; + + Matcher m = Pattern.compile("(\\d+).*").matcher(status); + + if (!m.matches()) + throw new XmlRpcException("Illegal status code: " + status); + + throw new XmlRpcFault(Integer.parseInt(m.group(1)), status); + } + + + private Object invoke(String method, Object... arguments) throws XmlRpcFault { + try { + XmlRpcClient rpc = new XmlRpcClient(url, false); + return rpc.invoke(method, arguments); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + + /** + * This simple function returns basic server info. + */ + @SuppressWarnings("unchecked") + public Map getServerInfo() throws XmlRpcFault { + return (Map) invoke("ServerInfo", token); + } + + + @SuppressWarnings("unchecked") + public List searchSubtitles(int imdbid, String languageName) throws XmlRpcFault { + Map query = new HashMap(2); + + query.put("imdbid", String.format("%07d", imdbid)); + query.put("sublanguageid", getSubLanguageID(languageName)); + + Map>> response = (Map>>) invoke("SearchSubtitles", token, singletonList(query)); + + List subs = new ArrayList(); + + try { + for (Map subtitleData : response.get("data")) { + subs.add(new OpenSubtitlesSubtitleDescriptor(Property.asEnumMap(subtitleData))); + } + } catch (ClassCastException e) { + // if the response is an error message, generic types won't match + throw new XmlRpcException("Illegal response: " + response.toString(), e); + } + + return subs; + } + + + private final Map languageMap = new TreeMap(String.CASE_INSENSITIVE_ORDER); + + + @SuppressWarnings("unchecked") + public String getSubLanguageID(String languageName) throws XmlRpcFault { + synchronized (languageMap) { + if (languageMap.isEmpty()) { + Map>> response = (Map>>) invoke("GetSubLanguages", "en"); + + for (Map language : response.get("data")) { + languageMap.put(language.get("LanguageName"), language.get("SubLanguageID")); + } + } + } + + return languageMap.get(languageName); + } + + + @SuppressWarnings("unchecked") + public List searchMoviesOnIMDB(String query) throws XmlRpcFault { + + Map>> response = (Map>>) invoke("SearchMoviesOnIMDB", token, query); + + List movies = new ArrayList(); + Pattern moviePattern = Pattern.compile("(.+) \\((\\d{4})\\).*"); + + for (Map movie : response.get("data")) { + try { + // get non-aka title (aka titles are separated by Â) + Scanner titleScanner = new Scanner(movie.get("title")).useDelimiter("\u00C2"); + + Matcher matcher = moviePattern.matcher(titleScanner.next().trim()); + + if (!matcher.matches()) + throw new InputMismatchException("Cannot parse movie: " + movie); + + String title = matcher.group(1); + String year = matcher.group(2); + + movies.add(new MovieDescriptor(title, Integer.parseInt(year), Integer.parseInt(movie.get("id")))); + } catch (Exception e) { + Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e); + } + } + + return movies; + } + + + @SuppressWarnings("unchecked") + public boolean noOperation() { + try { + Map response = (Map) invoke("NoOperation", token); + checkStatus(response.get("status")); + + return true; + } catch (Exception e) { + return false; + } + } + +}