diff --git a/build.xml b/build.xml index 7b56e8f4..5b5684d8 100644 --- a/build.xml +++ b/build.xml @@ -11,9 +11,11 @@ - - + + + + @@ -27,17 +29,26 @@ + + + + + + - - - + - + + + + + + diff --git a/lib/nekohtml.jar b/lib/nekohtml-1.9.6.1.jar similarity index 100% rename from lib/nekohtml.jar rename to lib/nekohtml-1.9.6.1.jar diff --git a/lib/simmetrics.jar b/lib/simmetrics_jar_v1_6_2_d07_02_07.jar similarity index 100% rename from lib/simmetrics.jar rename to lib/simmetrics_jar_v1_6_2_d07_02_07.jar diff --git a/lib/xmlrpc-client-1.1.jar b/lib/xmlrpc-client-1.1.jar new file mode 100644 index 00000000..76d09164 Binary files /dev/null and b/lib/xmlrpc-client-1.1.jar differ diff --git a/source/Main.java b/source/Main.java index 802dd186..0e96fe8d 100644 --- a/source/Main.java +++ b/source/Main.java @@ -1,3 +1,6 @@ +import java.util.logging.Level; +import java.util.logging.Logger; + import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -10,6 +13,8 @@ public class Main { * @param args */ public static void main(String[] args) { + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).setLevel(Level.ALL); + try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/AnalyzePanel.java b/source/net/sourceforge/filebot/ui/panel/analyze/AnalyzePanel.java index 839220f3..075183c0 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/AnalyzePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/AnalyzePanel.java @@ -7,7 +7,7 @@ import java.awt.Dimension; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import javax.swing.BorderFactory; @@ -70,7 +70,7 @@ public class AnalyzePanel extends FileBotPanel { } }; - private LinkedList toolPanels = new LinkedList(); + private List toolPanels = new ArrayList(); public void addTool(ToolPanel toolPanel) { diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/tools/TypePanel.java b/source/net/sourceforge/filebot/ui/panel/analyze/tools/TypePanel.java index a29f2d8b..f851fe0f 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/tools/TypePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/tools/TypePanel.java @@ -4,9 +4,9 @@ package net.sourceforge.filebot.ui.panel.analyze.tools; import java.awt.BorderLayout; import java.io.File; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; -import java.util.LinkedList; import java.util.TreeMap; import javax.swing.BorderFactory; @@ -84,7 +84,7 @@ public class TypePanel extends ToolPanel { if (list != null) list.add(f); else { - list = new LinkedList(); + list = new ArrayList(); list.add(f); map.put(suffix, list); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FilesRenameList.java b/source/net/sourceforge/filebot/ui/panel/rename/FilesRenameList.java index 20fae5ec..c59e1ab0 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/FilesRenameList.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FilesRenameList.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.ui.panel.rename; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import javax.swing.DefaultListModel; @@ -21,7 +21,7 @@ public class FilesRenameList extends RenameList { public List getListEntries() { DefaultListModel model = getModel(); - List files = new LinkedList(); + List files = new ArrayList(); for (int i = 0; i < model.getSize(); ++i) files.add((FileEntry) model.get(i)); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesRenameList.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesRenameList.java index 4d24d5d8..20bfa62a 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesRenameList.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesRenameList.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.ui.panel.rename; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import javax.swing.DefaultListModel; @@ -21,7 +21,7 @@ public class NamesRenameList extends RenameList { public List> getListEntries() { DefaultListModel model = getModel(); - List> entries = new LinkedList>(); + List> entries = new ArrayList>(); for (int i = 0; i < model.getSize(); i++) entries.add((ListEntry>) model.get(i)); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/match/Matcher.java b/source/net/sourceforge/filebot/ui/panel/rename/match/Matcher.java index d1d65605..05cff76f 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/match/Matcher.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/match/Matcher.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.ui.panel.rename.match; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry; @@ -12,7 +12,7 @@ import net.sourceforge.filebot.ui.panel.rename.similarity.SimilarityMetric; public class Matcher { public List match(List extends ListEntry>> listA, List extends ListEntry>> listB, SimilarityMetric similarityMetric) { - LinkedList matches = new LinkedList(); + ArrayList matches = new ArrayList(); for (ListEntry> entryA : listA) { float maxSimilarity = -1; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java index a37bfd87..ef60f6a7 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java @@ -12,7 +12,7 @@ import javax.swing.SwingWorker; public class ChecksumComputationTask extends SwingWorker { - private static final int CHUNK_SIZE = 32 * 1024; + private static final int BUFFER_SIZE = 32 * 1024; private File file; @@ -31,9 +31,7 @@ public class ChecksumComputationTask extends SwingWorker { if (length > 0) { long done = 0; - int bufferLength = (int) Math.min(length, CHUNK_SIZE); - - byte[] buffer = new byte[bufferLength]; + byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = 0; diff --git a/source/net/sourceforge/filebot/web/AnidbClient.java b/source/net/sourceforge/filebot/web/AnidbClient.java index c0be8231..34fb0e4b 100644 --- a/source/net/sourceforge/filebot/web/AnidbClient.java +++ b/source/net/sourceforge/filebot/web/AnidbClient.java @@ -10,7 +10,6 @@ import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -39,9 +38,8 @@ public class AnidbClient extends EpisodeListClient { @Override public List search(String searchterm) throws IOException, SAXException { - if (cache.containsKey(searchterm)) { + if (cache.containsKey(searchterm)) return Arrays.asList(searchterm); - } Document dom = HtmlUtil.getHtmlDocument(getSearchUrl(searchterm)); @@ -97,7 +95,7 @@ public class AnidbClient extends EpisodeListClient { List nodes = XPathUtil.selectNodes("//TABLE[@id='eplist']//TR/TD/SPAN/ancestor::TR", dom); - LinkedList list = new LinkedList(); + ArrayList list = new ArrayList(nodes.size()); NumberFormat f = NumberFormat.getInstance(); f.setMinimumIntegerDigits(Math.max(Integer.toString(nodes.size()).length(), 2)); diff --git a/source/net/sourceforge/filebot/web/DownloadTask.java b/source/net/sourceforge/filebot/web/DownloadTask.java new file mode 100644 index 00000000..4dfc301f --- /dev/null +++ b/source/net/sourceforge/filebot/web/DownloadTask.java @@ -0,0 +1,159 @@ + +package net.sourceforge.filebot.web; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.SwingWorker; + + +public class DownloadTask extends SwingWorker { + + public static final String DOWNLOAD_STATE = "download state"; + public static final String BYTES_READ = "bytes read"; + + + public static enum DownloadState { + PENDING, CONNECTING, DOWNLOADING, DONE; + } + + private static final int BUFFER_SIZE = 4 * 1024; + + private URL url; + private ByteBuffer postdata = null; + + private long size = -1; + private long bytesRead = 0; + private DownloadState state = DownloadState.PENDING; + + + public DownloadTask(URL url) { + this.url = url; + } + + + public DownloadTask(URL url, ByteBuffer postdata) { + this.url = url; + this.postdata = postdata; + } + + + public DownloadTask(URL url, Map postdata) { + this.url = url; + this.postdata = encodeParameters(postdata); + } + + + @Override + protected ByteBuffer doInBackground() throws Exception { + setDownloadState(DownloadState.CONNECTING); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + if (postdata != null) { + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + WritableByteChannel out = Channels.newChannel(connection.getOutputStream()); + out.write(postdata); + out.close(); + } + + size = connection.getContentLength(); + + setDownloadState(DownloadState.DOWNLOADING); + + InputStream in = connection.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + + byte[] buffer = new byte[BUFFER_SIZE]; + int len = 0; + + try { + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + + bytesRead += len; + getPropertyChangeSupport().firePropertyChange(BYTES_READ, null, bytesRead); + } + } catch (IOException e) { + // IOException (Premature EOF) is always thrown when the size of + // the response body is not known in advance, so we ignore it + if (isSizeKnown()) + throw e; + + } finally { + in.close(); + out.close(); + connection.disconnect(); + } + + setDownloadState(DownloadState.DONE); + return ByteBuffer.wrap(out.toByteArray()); + } + + + private void setDownloadState(DownloadState state) { + this.state = state; + getPropertyChangeSupport().firePropertyChange(DOWNLOAD_STATE, null, state); + } + + + public DownloadState getDownloadState() { + return state; + } + + + public long getBytesRead() { + return bytesRead; + } + + + public boolean isSizeKnown() { + return size >= 0; + } + + + public long getSize() { + return size; + } + + + private static ByteBuffer encodeParameters(Map parameters) { + StringBuffer sb = new StringBuffer(); + + int i = 0; + + for (String key : parameters.keySet()) { + if (i > 0) + sb.append("&"); + + sb.append(key); + sb.append("="); + + try { + sb.append(URLEncoder.encode(parameters.get(key), "UTF-8")); + } catch (UnsupportedEncodingException e) { + // will never happen + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, e.getMessage(), e); + } + + i++; + } + + return Charset.forName("UTF-8").encode(sb.toString()); + } + +} diff --git a/source/net/sourceforge/filebot/web/ImdbSearchEngine.java b/source/net/sourceforge/filebot/web/ImdbSearchEngine.java index 81a79e30..28f6763c 100644 --- a/source/net/sourceforge/filebot/web/ImdbSearchEngine.java +++ b/source/net/sourceforge/filebot/web/ImdbSearchEngine.java @@ -29,19 +29,19 @@ public class ImdbSearchEngine { private String host = "www.imdb.com"; - public List search(String searchterm) throws IOException, SAXException { + public List search(String searchterm) throws IOException, SAXException { Document dom = HtmlUtil.getHtmlDocument(getSearchUrl(searchterm)); List nodes = XPathUtil.selectNodes("id('outerbody')//TABLE//P[position() >= 2 and position() <=3 ]//A[count(child::IMG) <= 0]/..", dom); - ArrayList movies = new ArrayList(); + ArrayList movies = new ArrayList(); for (Node node : nodes) { try { - movies.add(parseMovie(node)); + movies.add(parseMovieNode(node)); } catch (Exception e) { - Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Invalid movie node", e); + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Cannot parse movie node", e); } } @@ -49,7 +49,7 @@ public class ImdbSearchEngine { } - private Movie parseMovie(Node node) { + private MovieDescriptor parseMovieNode(Node node) throws MalformedURLException { // ignore javascript links Node linkNode = XPathUtil.selectFirstNode("./A[count(@onclick) <= 0]", node); @@ -58,13 +58,12 @@ public class ImdbSearchEngine { // match /title/tt0379786/ Matcher idMatcher = Pattern.compile(".*/tt(\\d+)/.*").matcher(href); - Integer imdbID = null; + int imdbId; if (idMatcher.matches()) { - imdbID = new Integer(idMatcher.group(1)); - } else { + imdbId = new Integer(idMatcher.group(1)); + } else throw new IllegalArgumentException("Cannot match imdb id: " + href); - } String yearString = XPathUtil.selectString("text()[1]", node); @@ -73,12 +72,13 @@ public class ImdbSearchEngine { Integer year = null; if (yearMatcher.matches()) { - year = new Integer(yearMatcher.group(1)); - } else { + year = Integer.parseInt(yearMatcher.group(1)); + } else throw new IllegalArgumentException("Cannot match year: " + yearString); - } - return new Movie(title, year, imdbID); + URL imdbUrl = new URL("http", host, href); + + return new MovieDescriptor(title, year, imdbId, imdbUrl); } diff --git a/source/net/sourceforge/filebot/web/Movie.java b/source/net/sourceforge/filebot/web/Movie.java deleted file mode 100644 index e7fdc3f3..00000000 --- a/source/net/sourceforge/filebot/web/Movie.java +++ /dev/null @@ -1,43 +0,0 @@ - -package net.sourceforge.filebot.web; - - - - -public class Movie { - - private String title; - private Integer year; - private Integer imdbID; - - - public Movie(String title, Integer year, Integer imdbID) { - this.title = title; - this.imdbID = imdbID; - this.year = year; - } - - - public String getTitle() { - return title; - } - - - public Integer getImdbID() { - return imdbID; - } - - - public Integer getYear() { - return year; - } - - - @Override - public String toString() { - if (year == null) - return title; - - return String.format("%s (%d)", title, year); - } -} diff --git a/source/net/sourceforge/filebot/web/MovieDescriptor.java b/source/net/sourceforge/filebot/web/MovieDescriptor.java new file mode 100644 index 00000000..a5a88deb --- /dev/null +++ b/source/net/sourceforge/filebot/web/MovieDescriptor.java @@ -0,0 +1,48 @@ + +package net.sourceforge.filebot.web; + + +import java.net.URL; + + +public class MovieDescriptor { + + private String title; + private int year; + private int imdbId; + private URL imdbUrl; + + + public MovieDescriptor(String title, int year, int imdbId, URL imdbUrl) { + this.title = title; + this.imdbId = imdbId; + this.year = year; + this.imdbUrl = imdbUrl; + } + + + public String getTitle() { + return title; + } + + + public int getYear() { + return year; + } + + + public int getImdbId() { + return imdbId; + } + + + public URL getImdbUrl() { + return imdbUrl; + } + + + @Override + public String toString() { + return String.format("%s (%d)", title, year); + } +} diff --git a/source/net/sourceforge/filebot/web/OpenSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/OpenSubtitleDescriptor.java new file mode 100644 index 00000000..79f0e5bd --- /dev/null +++ b/source/net/sourceforge/filebot/web/OpenSubtitleDescriptor.java @@ -0,0 +1,64 @@ + +package net.sourceforge.filebot.web; + + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Describes a subtitle on OpenSubtitles. + * + * @see OpenSubtitlesClient + */ +public class OpenSubtitleDescriptor { + + private Map properties; + + + public static enum Properties { + IDSubMovieFile, MovieHash, MovieByteSize, MovieTimeMS, MovieFrames, IDSubtitleFile, SubFileName, SubActualCD, SubSize, SubHash, IDSubtitle, UserID, SubLanguageID, SubFormat, SubSumCD, SubAuthorComment, SubAddDate, SubBad, SubRating, SubDownloadsCnt, MovieReleaseName, IDMovie, IDMovieImdb, MovieName, MovieNameEng, MovieYear, MovieImdbRating, UserNickName, ISO639, LanguageName, SubDownloadLink, ZipDownloadLink, + } + + + public OpenSubtitleDescriptor(Map properties) { + this.properties = properties; + } + + + public Map getPropertyMap() { + return properties; + } + + + public String getProperty(Properties property) { + return properties.get(property.name()); + } + + + public long getSize() { + return Long.parseLong(getProperty(Properties.SubSize)); + } + + + public URL getDownloadLink() { + String link = getProperty(Properties.SubDownloadLink); + + try { + return new URL(link); + } catch (MalformedURLException e) { + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Invalid download link: " + link, e); + return null; + } + } + + + @Override + public String toString() { + return String.format("%s [%s]", getProperty(Properties.SubFileName), getProperty(Properties.LanguageName)); + } + +} diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java new file mode 100644 index 00000000..2bb7bd4f --- /dev/null +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java @@ -0,0 +1,257 @@ + +package net.sourceforge.filebot.web; + + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +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; + + +/** + * Client for the OpenSubtitles XML-RPC API. + * + */ +public class OpenSubtitlesClient { + + /** + * + * + * Main server: + * http://www.opensubtitles.org/xml-rpc + * + * + * Developing tests: + * http://dev.opensubtitles.org/xml-rpc + * + * + */ + private String url = "http://dev.opensubtitles.org/xml-rpc"; + + private String username; + private String password; + private String language; + + private String useragent; + + private String token = null; + + private Timer keepAliveDaemon = null; + + /** + * Interval to call NoOperation to keep the session from expiring + */ + public static final int KEEP_ALIVE_INTERVAL = 12 * 60 * 1000; // 12 minutes + + + public OpenSubtitlesClient(String useragent) { + this.useragent = useragent; + } + + + public boolean isLoggedOn() { + return username != null; + } + + + /** + * login as anonymous user + */ + public synchronized void login() throws XmlRpcFault { + this.login("", "", "en"); + } + + + /** + * This will login user. This method should be called always when starting talking with + * server. + * + * @param username blank for anonymous user. + * @param 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 { + if (isLoggedOn()) + throw new IllegalStateException("User is already logged on"); + + if ((username == null) || (password == null) || (language == null)) + throw new IllegalArgumentException("Username, password and language must not be null"); + + this.username = username; + this.password = password; + this.language = language; + + activate(); + } + + + public synchronized void logout() { + if (!isLoggedOn()) + throw new IllegalStateException("User is not logged on"); + + deactivate(); + + username = null; + password = null; + language = null; + } + + + @SuppressWarnings("unchecked") + private synchronized void activate() throws XmlRpcFault { + if (isActive()) + return; + + if (!isLoggedOn()) + throw new IllegalStateException("User is not logged on"); + + Map response = (Map) invoke("LogIn", username, password, language, useragent); + checkStatus(response.get("status")); + + token = response.get("token"); + + keepAliveDaemon = new Timer(getClass().getSimpleName() + " Keepalive", true); + keepAliveDaemon.schedule(new KeepAliveTimerTask(), KEEP_ALIVE_INTERVAL, KEEP_ALIVE_INTERVAL); + } + + + @SuppressWarnings("unchecked") + private synchronized void deactivate() { + if (!isActive()) + return; + + // anonymous users will always get a 401 Unauthorized when trying to logout + if (!username.isEmpty()) { + try { + Map response = (Map) invoke("LogOut", token); + checkStatus(response.get("status")); + } catch (Exception e) { + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Exception while deactivating connection", e); + } + } + + token = null; + + keepAliveDaemon.cancel(); + keepAliveDaemon = null; + } + + + private boolean isActive() { + return token != null; + } + + + /** + * Check status whether it 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) { + // will never happen + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Invalid xml-rpc url: " + url, e); + return null; + } + } + + + /** + * This simple function returns basic server info. + */ + @SuppressWarnings("unchecked") + public Map getServerInfo() throws XmlRpcFault { + activate(); + + return (Map) invoke("ServerInfo", token); + } + + + @SuppressWarnings("unchecked") + public List searchSubtitles(int... imdbidArray) throws XmlRpcFault { + activate(); + + List> imdbidList = new ArrayList>(imdbidArray.length); + + for (int imdbid : imdbidArray) { + Map map = new HashMap(1); + + // pad id with zeros + map.put("imdbid", String.format("%07d", imdbid)); + + imdbidList.add(map); + } + + Map>> response = (Map>>) invoke("SearchSubtitles", token, imdbidList); + + ArrayList subs = new ArrayList(); + + for (Map subtitle : response.get("data")) + subs.add(new OpenSubtitleDescriptor(subtitle)); + + return subs; + } + + + @SuppressWarnings("unchecked") + public boolean noOperation() { + try { + activate(); + + Map response = (Map) invoke("NoOperation", token); + checkStatus(response.get("status")); + + return true; + } catch (Exception e) { + deactivate(); + return false; + } + } + + + private class KeepAliveTimerTask extends TimerTask { + + @Override + public void run() { + Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + + if (!noOperation()) { + logger.log(Level.INFO, "Connection lost"); + deactivate(); + } else { + logger.log(Level.INFO, "Connection is OK"); + } + }; + }; + +} diff --git a/source/net/sourceforge/filebot/web/SubsceneClient.java b/source/net/sourceforge/filebot/web/SubsceneClient.java new file mode 100644 index 00000000..5bac85b2 --- /dev/null +++ b/source/net/sourceforge/filebot/web/SubsceneClient.java @@ -0,0 +1,125 @@ + +package net.sourceforge.filebot.web; + + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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 net.sourceforge.tuned.XPathUtil; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + + +public class SubsceneClient { + + private Map cache = Collections.synchronizedMap(new TreeMap()); + + private String host = "subscene.com"; + + + public List search(String searchterm) throws IOException, SAXException { + + Document dom = HtmlUtil.getHtmlDocument(getSearchUrl(searchterm)); + + List nodes = XPathUtil.selectNodes("id('filmSearch')/A", dom); + + ArrayList titles = new ArrayList(); + + for (Node node : nodes) { + String title = XPathUtil.selectString("text()", node); + String href = XPathUtil.selectString("@href", node); + + try { + URL url = new URL("http", host, href); + + cache.put(title, url); + titles.add(title); + } catch (MalformedURLException e) { + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Invalid href: " + href, e); + } + } + + return titles; + } + + + public List getSubtitleList(String title) throws IOException, SAXException { + URL url = cache.get(title); + + if (url == null) + throw new IllegalArgumentException("Unknown title: " + title); + + Document dom = HtmlUtil.getHtmlDocument(url); + + String downloadPath = XPathUtil.selectString("id('aspnetForm')/@action", dom); + String viewstate = XPathUtil.selectString("id('__VIEWSTATE')/@value", dom); + + List nodes = XPathUtil.selectNodes("//TABLE[@class='filmSubtitleList']//A[@id]//ancestor::TR", dom); + + ArrayList list = new ArrayList(); + + for (Node node : nodes) { + try { + Node linkNode = XPathUtil.selectFirstNode("./TD[1]/A", node); + + String href = XPathUtil.selectString("@href", linkNode); + + String name = XPathUtil.selectString("./SPAN[2]", linkNode); + String lang = XPathUtil.selectString("./SPAN[1]", linkNode); + + int numberOfCDs = Integer.parseInt(XPathUtil.selectString("./TD[2]", node)); + String author = XPathUtil.selectString("./TD[4]", node); + + URL downloadUrl = new URL("http", host, downloadPath); + + Map downloadParameters = parseParameters(href); + downloadParameters.put("__VIEWSTATE", viewstate); + + list.add(new SubsceneSubtitleDescriptor(name, lang, numberOfCDs, author, downloadUrl, downloadParameters)); + } catch (Exception e) { + Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Cannot parse subtitle node", e); + } + } + + return list; + } + + + private Map parseParameters(String href) { + Matcher matcher = Pattern.compile("javascript:Subtitle\\((\\d+), '(\\w+)', '0', '(\\d+)'\\);").matcher(href); + + if (!matcher.matches()) + throw new IllegalArgumentException("Cannot extract download parameters: " + href); + + Map map = new HashMap(); + + map.put("subtitleId", matcher.group(1)); + map.put("typeId", matcher.group(2)); + map.put("filmId", matcher.group(3)); + + return map; + } + + + private URL getSearchUrl(String searchterm) throws UnsupportedEncodingException, MalformedURLException { + String qs = URLEncoder.encode(searchterm, "UTF-8"); + String file = "/filmsearch.aspx?q=" + qs; + return new URL("http", host, file); + } + +} diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java new file mode 100644 index 00000000..8c862a25 --- /dev/null +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java @@ -0,0 +1,67 @@ + +package net.sourceforge.filebot.web; + + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.util.Map; + + +public class SubsceneSubtitleDescriptor { + + private String title; + private String language; + private int numberOfCDs; + private String author; + + private Map downloadParameters; + private URL downloadUrl; + + + public SubsceneSubtitleDescriptor(String title, String language, int numberOfCDs, String author, URL downloadUrl, Map downloadParameters) { + this.title = title; + this.language = language; + this.numberOfCDs = numberOfCDs; + this.author = author; + + this.downloadUrl = downloadUrl; + this.downloadParameters = downloadParameters; + } + + + public String getTitle() { + return title; + } + + + public String getLanguage() { + return language; + } + + + public int getNumberOfCDs() { + return numberOfCDs; + } + + + public String getAuthor() { + return author; + } + + + public String getArchiveType() { + return downloadParameters.get("typeId"); + } + + + public DownloadTask createDownloadTask() throws UnsupportedEncodingException { + return new DownloadTask(downloadUrl, downloadParameters); + } + + + @Override + public String toString() { + return String.format("%s [%s]", title, language); + } + +} diff --git a/source/net/sourceforge/tuned/ByteBufferInputStream.java b/source/net/sourceforge/tuned/ByteBufferInputStream.java new file mode 100644 index 00000000..8ac1ffbf --- /dev/null +++ b/source/net/sourceforge/tuned/ByteBufferInputStream.java @@ -0,0 +1,41 @@ + +package net.sourceforge.tuned; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + + +public class ByteBufferInputStream extends InputStream { + + private ByteBuffer buffer; + + + public ByteBufferInputStream(ByteBuffer buffer) { + this.buffer = buffer; + } + + + @Override + public synchronized int read() throws IOException { + return buffer.getInt(); + } + + + @Override + public synchronized int read(byte[] b, int off, int len) throws IOException { + int length = Math.min(len, buffer.remaining()); + + buffer.get(b, off, length); + + return length; + } + + + @Override + public synchronized int available() throws IOException { + return buffer.remaining(); + } + +} diff --git a/source/net/sourceforge/tuned/ui/notification/QueueNotificationLayout.java b/source/net/sourceforge/tuned/ui/notification/QueueNotificationLayout.java index d8ddb56c..02f24794 100644 --- a/source/net/sourceforge/tuned/ui/notification/QueueNotificationLayout.java +++ b/source/net/sourceforge/tuned/ui/notification/QueueNotificationLayout.java @@ -11,7 +11,7 @@ import java.awt.GraphicsConfiguration; import java.awt.Insets; import java.awt.Point; import java.awt.Toolkit; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.ListIterator; import javax.swing.SwingConstants; @@ -23,7 +23,7 @@ import javax.swing.SwingConstants; */ public class QueueNotificationLayout implements NotificationLayout, SwingConstants { - private LinkedList notificationList = new LinkedList(); + private ArrayList notificationList = new ArrayList(); private int orientation; private int direction;