diff --git a/build.xml b/build.xml index d3cbef28..19f338cb 100644 --- a/build.xml +++ b/build.xml @@ -111,6 +111,7 @@ + diff --git a/lib/ehcache.jar b/lib/ehcache.jar new file mode 100644 index 00000000..0ddab5be Binary files /dev/null and b/lib/ehcache.jar differ diff --git a/source/ehcache.xml b/source/ehcache.xml new file mode 100644 index 00000000..28b94cb0 --- /dev/null +++ b/source/ehcache.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + diff --git a/source/net/sourceforge/filebot/FileBotUtil.java b/source/net/sourceforge/filebot/FileBotUtil.java index fbef0680..f78cd352 100644 --- a/source/net/sourceforge/filebot/FileBotUtil.java +++ b/source/net/sourceforge/filebot/FileBotUtil.java @@ -14,12 +14,12 @@ import net.sourceforge.tuned.FileUtil; public final class FileBotUtil { - public static final String getApplicationName() { + public static String getApplicationName() { return "FileBot"; }; - public static final String getApplicationVersion() { + public static String getApplicationVersion() { return "1.9"; }; @@ -55,7 +55,7 @@ public final class FileBotUtil { public static String getEmbeddedChecksum(String string) { - Matcher matcher = FileBotUtil.EMBEDDED_CHECKSUM_PATTERN.matcher(string); + Matcher matcher = EMBEDDED_CHECKSUM_PATTERN.matcher(string); String embeddedChecksum = null; // get last match diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index 46a84be7..d14fd32c 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -1,4 +1,7 @@ + package net.sourceforge.filebot; + + import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.BackingStoreException; @@ -21,10 +24,10 @@ public class Main { */ public static void main(String... args) { - setupLogging(); - final ArgumentBean argumentBean = handleArguments(args); + setupLogging(); + try { // UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); // UIManager.setLookAndFeel("a03.swing.plaf.A03LookAndFeel"); @@ -82,7 +85,7 @@ public class Main { if (argumentBean.isClear()) { // clear preferences try { - Preferences.userNodeForPackage(FileBotUtil.class).removeNode(); + Preferences.userNodeForPackage(Main.class).removeNode(); } catch (BackingStoreException e) { Logger.getLogger("global").log(Level.SEVERE, e.toString(), e); } diff --git a/source/net/sourceforge/filebot/resources/search.thetvdb.png b/source/net/sourceforge/filebot/resources/search.thetvdb.png new file mode 100644 index 00000000..e662966f Binary files /dev/null and b/source/net/sourceforge/filebot/resources/search.thetvdb.png differ diff --git a/source/net/sourceforge/filebot/torrent/Torrent.java b/source/net/sourceforge/filebot/torrent/Torrent.java index 969cbdcd..47f966be 100644 --- a/source/net/sourceforge/filebot/torrent/Torrent.java +++ b/source/net/sourceforge/filebot/torrent/Torrent.java @@ -69,7 +69,7 @@ public class Torrent { Map fileMap = (Map) fileMapObject; List pathList = (List) fileMap.get("path"); - StringBuilder pathBuilder = new StringBuilder(); + StringBuilder pathBuilder = new StringBuilder(80); String entryName = null; Iterator iterator = pathList.iterator(); diff --git a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java index ef93fd90..8a64f4bf 100644 --- a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java +++ b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java @@ -2,7 +2,7 @@ package net.sourceforge.filebot.ui; -import static javax.swing.JTabbedPane.WRAP_TAB_LAYOUT; +import static javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT; import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; import static javax.swing.SwingConstants.TOP; @@ -43,7 +43,7 @@ public abstract class AbstractSearchPanel extends FileBotPanel { protected final JPanel tabbedPaneGroup = new JPanel(new MigLayout("nogrid, fill, insets 0")); - protected final JTabbedPane tabbedPane = new JTabbedPane(TOP, WRAP_TAB_LAYOUT); + protected final JTabbedPane tabbedPane = new JTabbedPane(TOP, SCROLL_TAB_LAYOUT); protected final HistoryPanel historyPanel = new HistoryPanel(); @@ -96,7 +96,7 @@ public abstract class AbstractSearchPanel extends FileBotPanel { protected abstract LabelProvider createSearchEngineLabelProvider(); - protected abstract RequestProcessor createRequestProcessor(); + protected abstract RequestProcessor createRequestProcessor(); public EventList getSearchHistory() { @@ -104,13 +104,12 @@ public abstract class AbstractSearchPanel extends FileBotPanel { } - private void search(RequestProcessor requestProcessor) { + private void search(RequestProcessor requestProcessor) { FileBotTab tab = requestProcessor.tab; - Request request = requestProcessor.request; tab.setTitle(requestProcessor.getTitle()); tab.setLoading(true); - tab.setIcon(searchTextField.getSelectButton().getLabelProvider().getIcon(request.getClient())); + tab.setIcon(requestProcessor.getIcon()); tab.addTo(tabbedPane); @@ -133,141 +132,18 @@ public abstract class AbstractSearchPanel extends FileBotPanel { }; - protected class Request { + private class SearchTask extends SwingWorker, Void> { - private final S client; - private final String searchText; + private final RequestProcessor requestProcessor; - public Request(S client, String searchText) { - this.client = client; - this.searchText = searchText; - } - - - public S getClient() { - return client; - } - - - public String getSearchText() { - return searchText; - } - - } - - - protected abstract class RequestProcessor { - - protected final R request; - - private FileBotTab tab; - - private SearchResult searchResult = null; - - 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; - } - - - private 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(); - } - - - protected SearchResult chooseSearchResult(Collection searchResults) throws Exception { - - switch (searchResults.size()) { - case 0: - Logger.getLogger("ui").warning(String.format("\"%s\" has not been found.", request.getSearchText())); - return null; - case 1: - return searchResults.iterator().next(); - } - - // check if an exact match has been found - for (SearchResult searchResult : searchResults) { - if (request.getSearchText().equalsIgnoreCase(searchResult.getName())) - return searchResult; - } - - // multiple results have been found, user must select one - Window window = SwingUtilities.getWindowAncestor(AbstractSearchPanel.this); - - 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.setIconImage(TunedUtil.getImage(searchTextField.getSelectButton().getLabelProvider().getIcon(request.getClient()))); - } - - - public long getDuration() { - return duration; - } - - } - - - private class SearchTask extends SwingWorker, Void> { - - private final RequestProcessor requestProcessor; - - - public SearchTask(RequestProcessor requestProcessor) { + public SearchTask(RequestProcessor requestProcessor) { this.requestProcessor = requestProcessor; } @Override - protected Collection doInBackground() throws Exception { + protected Collection doInBackground() throws Exception { long start = System.currentTimeMillis(); try { @@ -288,7 +164,7 @@ public abstract class AbstractSearchPanel extends FileBotPanel { try { // choose search result - requestProcessor.setSearchResult(requestProcessor.chooseSearchResult(get())); + requestProcessor.setSearchResult(requestProcessor.chooseSearchResult(get(), SwingUtilities.getWindowAncestor(AbstractSearchPanel.this))); if (requestProcessor.getSearchResult() == null) { tab.close(); @@ -313,16 +189,15 @@ public abstract class AbstractSearchPanel extends FileBotPanel { } } - } private class FetchTask extends SwingWorker, Void> { - private final RequestProcessor requestProcessor; + private final RequestProcessor requestProcessor; - public FetchTask(RequestProcessor requestProcessor) { + public FetchTask(RequestProcessor requestProcessor) { this.requestProcessor = requestProcessor; } @@ -342,7 +217,6 @@ public abstract class AbstractSearchPanel extends FileBotPanel { @Override public void done() { FileBotTab tab = requestProcessor.tab; - Request request = requestProcessor.request; if (tab.isClosed()) return; @@ -353,8 +227,8 @@ public abstract class AbstractSearchPanel extends FileBotPanel { requestProcessor.process(elements); - String title = requestProcessor.getSearchResult().toString(); - Icon icon = searchTextField.getSelectButton().getLabelProvider().getIcon(request.getClient()); + 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())); @@ -375,4 +249,123 @@ public abstract class AbstractSearchPanel extends FileBotPanel { } } + + 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 Icon getIcon() { + return null; + } + + + protected SearchResult chooseSearchResult(Collection searchResults, Window window) throws Exception { + + switch (searchResults.size()) { + case 0: + Logger.getLogger("ui").warning(String.format("\"%s\" has not been found.", request.getSearchText())); + return null; + case 1: + return searchResults.iterator().next(); + } + + // check if an exact match has been found + for (SearchResult searchResult : searchResults) { + if (request.getSearchText().equalsIgnoreCase(searchResult.getName())) + return searchResult; + } + + // 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.setIconImage(TunedUtil.getImage(getIcon())); + } + + + public long getDuration() { + return duration; + } + + } + } diff --git a/source/net/sourceforge/filebot/ui/FileBotWindow.java b/source/net/sourceforge/filebot/ui/FileBotWindow.java index e0043a85..51558a1e 100644 --- a/source/net/sourceforge/filebot/ui/FileBotWindow.java +++ b/source/net/sourceforge/filebot/ui/FileBotWindow.java @@ -2,6 +2,8 @@ package net.sourceforge.filebot.ui; +import static net.sourceforge.filebot.FileBotUtil.getApplicationName; + import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Image; @@ -19,7 +21,6 @@ import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import net.sourceforge.filebot.FileBotUtil; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ui.panel.analyze.AnalyzePanel; import net.sourceforge.filebot.ui.panel.episodelist.EpisodeListPanel; @@ -42,7 +43,7 @@ public class FileBotWindow extends JFrame implements ListSelectionListener { public FileBotWindow() { - super(FileBotUtil.getApplicationName()); + super(getApplicationName()); setLocationByPlatform(true); setDefaultCloseOperation(EXIT_ON_CLOSE); diff --git a/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java b/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java index 686653f9..3e039bf7 100644 --- a/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java +++ b/source/net/sourceforge/filebot/ui/NotificationLoggingHandler.java @@ -2,6 +2,8 @@ package net.sourceforge.filebot.ui; +import static net.sourceforge.filebot.FileBotUtil.getApplicationName; + import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -10,7 +12,6 @@ import javax.swing.Icon; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; -import net.sourceforge.filebot.FileBotUtil; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.ui.notification.MessageNotification; import net.sourceforge.tuned.ui.notification.NotificationManager; @@ -54,7 +55,7 @@ public class NotificationLoggingHandler extends Handler { private void show(String message, Icon icon, int timeout) { - notificationManager.show(new MessageNotification(FileBotUtil.getApplicationName(), message, icon, timeout)); + notificationManager.show(new MessageNotification(getApplicationName(), message, icon, timeout)); } diff --git a/source/net/sourceforge/filebot/ui/SelectDialog.java b/source/net/sourceforge/filebot/ui/SelectDialog.java index e4ddcdfd..74a8d73d 100644 --- a/source/net/sourceforge/filebot/ui/SelectDialog.java +++ b/source/net/sourceforge/filebot/ui/SelectDialog.java @@ -35,7 +35,7 @@ public class SelectDialog extends JDialog { private boolean valueSelected = false; - public SelectDialog(Window owner, Collection options) { + public SelectDialog(Window owner, Collection options) { super(owner, "Select", ModalityType.DOCUMENT_MODAL); setDefaultCloseOperation(DISPOSE_ON_CLOSE); diff --git a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java index 127e8752..6b9d9dca 100644 --- a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.List; import javax.swing.AbstractAction; +import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JSpinner; import javax.swing.KeyStroke; @@ -33,6 +34,7 @@ import net.sourceforge.filebot.web.EpisodeListClient; import net.sourceforge.filebot.web.SearchResult; import net.sourceforge.filebot.web.TVDotComClient; import net.sourceforge.filebot.web.TVRageClient; +import net.sourceforge.filebot.web.TheTVDBClient; import net.sourceforge.tuned.ui.LabelProvider; import net.sourceforge.tuned.ui.SelectButton; import net.sourceforge.tuned.ui.SimpleLabelProvider; @@ -74,6 +76,7 @@ public class EpisodeListPanel extends AbstractSearchPanel { + protected static class EpisodeListRequestProcessor extends RequestProcessor { public EpisodeListRequestProcessor(EpisodeListRequest request) { super(request, new EpisodeListTab()); @@ -194,10 +208,37 @@ public class EpisodeListPanel extends AbstractSearchPanel fetch() throws Exception { - if (request.getSeason() != ALL_SEASONS) - return request.getClient().getEpisodeList(getSearchResult(), request.getSeason()); - else - return request.getClient().getEpisodeList(getSearchResult()); + Collection episodes; + + if (request.getSeason() != ALL_SEASONS) { + episodes = request.getClient().getEpisodeList(getSearchResult(), request.getSeason()); + } else { + episodes = request.getClient().getEpisodeList(getSearchResult()); + } + + // find max. episode number string length + int maxLength = 1; + + for (Episode episode : episodes) { + String num = episode.getEpisodeNumber(); + + if (num.matches("\\d+") && num.length() > maxLength) { + maxLength = num.length(); + } + } + + // pad episode numbers with zeros (e.g. %02d) so all episode numbers have the same number of digits + String format = "%0" + maxLength + "d"; + for (Episode episode : episodes) { + + try { + episode.setEpisodeNumber(String.format(format, Integer.parseInt(episode.getEpisodeNumber()))); + } catch (NumberFormatException e) { + // ignore + } + } + + return episodes; } @@ -241,6 +282,12 @@ public class EpisodeListPanel extends AbstractSearchPanel selectDialog) { super.configureSelectDialog(selectDialog); @@ -250,7 +297,7 @@ public class EpisodeListPanel extends AbstractSearchPanel { + protected static class EpisodeListTab extends FileBotList { public EpisodeListTab() { // set export handler for episode list diff --git a/source/net/sourceforge/filebot/ui/panel/episodelist/SeasonSpinnerModel.java b/source/net/sourceforge/filebot/ui/panel/episodelist/SeasonSpinnerModel.java index c6a87368..b59b5ce0 100644 --- a/source/net/sourceforge/filebot/ui/panel/episodelist/SeasonSpinnerModel.java +++ b/source/net/sourceforge/filebot/ui/panel/episodelist/SeasonSpinnerModel.java @@ -9,13 +9,17 @@ public class SeasonSpinnerModel extends SpinnerNumberModel { public static final int ALL_SEASONS = 0; + public static final int MAX_VALUE = 99; + + private Number valueBeforeLock = null; + public SeasonSpinnerModel() { - super(ALL_SEASONS, ALL_SEASONS, Integer.MAX_VALUE, 1); + super(ALL_SEASONS, ALL_SEASONS, MAX_VALUE, 1); } - public Integer getSeason() { + public int getSeason() { return getNumber().intValue(); } @@ -25,18 +29,28 @@ public class SeasonSpinnerModel extends SpinnerNumberModel { if (next < ALL_SEASONS) next = ALL_SEASONS; + else if (next > MAX_VALUE) + next = MAX_VALUE; setValue(next); } - public void lock(boolean lock) { - if (lock) { - setValue(ALL_SEASONS); - setMaximum(ALL_SEASONS); - } else { - setMaximum(Integer.MAX_VALUE); - } + public void lock(int value) { + valueBeforeLock = getNumber(); + setMinimum(value); + setMaximum(value); + setValue(value); } + + public void unlock() { + setMinimum(ALL_SEASONS); + setMaximum(MAX_VALUE); + + if (valueBeforeLock != null) { + setValue(valueBeforeLock); + valueBeforeLock = null; + } + } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java index 9ab068e7..7eb7da14 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java @@ -5,6 +5,7 @@ package net.sourceforge.filebot.ui.panel.rename; import static net.sourceforge.filebot.FileBotUtil.LIST_FILE_EXTENSIONS; import static net.sourceforge.filebot.FileBotUtil.TORRENT_FILE_EXTENSIONS; import static net.sourceforge.filebot.FileBotUtil.containsOnly; +import static net.sourceforge.filebot.FileBotUtil.isInvalidFileName; import java.awt.datatransfer.Transferable; import java.io.BufferedReader; @@ -19,7 +20,6 @@ import java.util.logging.Logger; import javax.swing.SwingUtilities; -import net.sourceforge.filebot.FileBotUtil; import net.sourceforge.filebot.torrent.Torrent; import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry; import net.sourceforge.filebot.ui.panel.rename.entry.StringEntry; @@ -60,7 +60,7 @@ class NamesListTransferablePolicy extends FilesListTransferablePolicy { List invalidEntries = new ArrayList(); for (ListEntry entry : entries) { - if (FileBotUtil.isInvalidFileName(entry.getName())) + if (isInvalidFileName(entry.getName())) invalidEntries.add(entry); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java index a9e5f7c9..df8d4e41 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java @@ -2,6 +2,9 @@ package net.sourceforge.filebot.ui.panel.rename; +import static net.sourceforge.filebot.FileBotUtil.INVALID_CHARACTERS_PATTERN; +import static net.sourceforge.filebot.FileBotUtil.validateFileName; + import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; @@ -21,7 +24,6 @@ import javax.swing.JScrollPane; import javax.swing.KeyStroke; import net.miginfocom.swing.MigLayout; -import net.sourceforge.filebot.FileBotUtil; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ui.panel.rename.entry.ListEntry; import net.sourceforge.tuned.ui.ArrayListModel; @@ -49,7 +51,7 @@ public class ValidateNamesDialog extends JDialog { JList list = new JList(new ArrayListModel(entries)); list.setEnabled(false); - list.setCellRenderer(new HighlightListCellRenderer(FileBotUtil.INVALID_CHARACTERS_PATTERN, new CharacterHighlightPainter(new Color(0xFF4200), new Color(0xFF1200)), 4)); + list.setCellRenderer(new HighlightListCellRenderer(INVALID_CHARACTERS_PATTERN, new CharacterHighlightPainter(new Color(0xFF4200), new Color(0xFF1200)), 4)); JLabel label = new JLabel("Some names contain invalid characters:"); @@ -94,8 +96,7 @@ public class ValidateNamesDialog extends JDialog { @Override public void actionPerformed(ActionEvent e) { for (ListEntry entry : entries) { - String validatedName = FileBotUtil.validateFileName(entry.getName()); - entry.setName(validatedName); + entry.setName(validateFileName(entry.getName())); } setEnabled(false); diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java index 95fb6a21..fd4a3ef5 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java @@ -2,6 +2,9 @@ package net.sourceforge.filebot.ui.panel.subtitle; +import static net.sourceforge.filebot.FileBotUtil.getApplicationName; +import static net.sourceforge.filebot.FileBotUtil.getApplicationVersion; + import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -49,7 +52,7 @@ public class SubtitlePanel extends AbstractSearchPanel createSearchEngines() { List engines = new ArrayList(2); - engines.add(new OpenSubtitlesSubtitleClient()); + engines.add(new OpenSubtitlesSubtitleClient(String.format("%s v%s", getApplicationName(), getApplicationVersion()))); engines.add(new SubsceneSubtitleClient()); return engines; @@ -74,17 +77,24 @@ public class SubtitlePanel extends AbstractSearchPanel { + protected static class SubtitleRequestProcessor extends RequestProcessor { public SubtitleRequestProcessor(SubtitleRequest request) { super(request, new SubtitleDownloadPanel()); diff --git a/source/net/sourceforge/filebot/ui/transfer/FileTransferable.java b/source/net/sourceforge/filebot/ui/transfer/FileTransferable.java index 58324938..e18d725d 100644 --- a/source/net/sourceforge/filebot/ui/transfer/FileTransferable.java +++ b/source/net/sourceforge/filebot/ui/transfer/FileTransferable.java @@ -57,7 +57,7 @@ public class FileTransferable implements Transferable { * @return line separated list of file URIs */ private String getUriList() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(80 * files.size()); for (File file : files) { sb.append("file://" + file.toURI().getPath()); diff --git a/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java b/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java index 4e56aff7..5c961626 100644 --- a/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java +++ b/source/net/sourceforge/filebot/ui/transfer/LazyTextFileTransferable.java @@ -2,6 +2,9 @@ package net.sourceforge.filebot.ui.transfer; +import static net.sourceforge.filebot.FileBotUtil.getApplicationName; +import static net.sourceforge.filebot.FileBotUtil.validateFileName; + import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; @@ -11,7 +14,6 @@ import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.charset.Charset; -import net.sourceforge.filebot.FileBotUtil; import net.sourceforge.tuned.TemporaryFolder; @@ -52,10 +54,10 @@ public class LazyTextFileTransferable implements Transferable { private FileTransferable createFileTransferable() throws IOException { // remove invalid characters from file name - String validFileName = FileBotUtil.validateFileName(defaultFileName); + String validFileName = validateFileName(defaultFileName); // create new temporary file in TEMP/APP_NAME [UUID]/dnd - File temporaryFile = TemporaryFolder.getFolder(FileBotUtil.getApplicationName()).subFolder("dnd").createFile(validFileName); + File temporaryFile = TemporaryFolder.getFolder(getApplicationName()).subFolder("dnd").createFile(validFileName); // write text to file FileChannel fileChannel = new FileOutputStream(temporaryFile).getChannel(); diff --git a/source/net/sourceforge/filebot/web/AnidbClient.java b/source/net/sourceforge/filebot/web/AnidbClient.java index 93c46f2e..338021f0 100644 --- a/source/net/sourceforge/filebot/web/AnidbClient.java +++ b/source/net/sourceforge/filebot/web/AnidbClient.java @@ -2,24 +2,26 @@ package net.sourceforge.filebot.web; +import static net.sourceforge.filebot.web.WebRequest.getHtmlDocument; +import static net.sourceforge.tuned.XPathUtil.exists; +import static net.sourceforge.tuned.XPathUtil.selectNode; +import static net.sourceforge.tuned.XPathUtil.selectNodes; +import static net.sourceforge.tuned.XPathUtil.selectString; + import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLEncoder; -import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Icon; import net.sourceforge.filebot.ResourceManager; -import net.sourceforge.tuned.XPathUtil; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -44,19 +46,22 @@ public class AnidbClient implements EpisodeListClient { @Override - public List search(String searchterm) throws IOException, SAXException { + public List search(String query) throws IOException, SAXException { - Document dom = HtmlUtil.getHtmlDocument(getSearchUrl(searchterm)); + // type=2 -> only TV Series + URL searchUrl = new URL("http", host, "/perl-bin/animedb.pl?type=2&show=animelist&orderby.name=0.1&orderbar=0&noalias=1&do.search=Search&adb.search=" + URLEncoder.encode(query, "UTF-8")); - List nodes = XPathUtil.selectNodes("//TABLE[@class='animelist']//TR/TD/ancestor::TR", dom); + Document dom = getHtmlDocument(searchUrl); + + List nodes = selectNodes("//TABLE[@class='animelist']//TR/TD/ancestor::TR", dom); List searchResults = new ArrayList(nodes.size()); for (Node node : nodes) { - Node titleNode = XPathUtil.selectNode("./TD[@class='name']/A", node); + Node titleNode = selectNode("./TD[@class='name']/A", node); - String title = XPathUtil.selectString(".", titleNode); - String href = XPathUtil.selectString("@href", titleNode); + String title = selectString(".", titleNode); + String href = selectString("@href", titleNode); String path = "/perl-bin/" + href; @@ -70,12 +75,12 @@ public class AnidbClient implements EpisodeListClient { // we might have been redirected to the episode list page if (searchResults.isEmpty()) { // check if current page contains an episode list - if (XPathUtil.exists("//TABLE[@class='eplist']", dom)) { + if (exists("//TABLE[@class='eplist']", dom)) { // get show's name from the document - String header = XPathUtil.selectString("id('layout-content')//H1[1]", dom); + String header = selectString("id('layout-content')//H1[1]", dom); String name = header.replaceFirst("Anime:\\s*", ""); - String episodeListUrl = XPathUtil.selectString("id('layout-main')//DIV[@class='data']//A[@class='short_link']/@href", dom); + String episodeListUrl = selectString("id('layout-main')//DIV[@class='data']//A[@class='short_link']/@href", dom); try { searchResults.add(new HyperLink(name, new URL(episodeListUrl))); @@ -92,33 +97,25 @@ public class AnidbClient implements EpisodeListClient { @Override public List getEpisodeList(SearchResult searchResult) throws IOException, SAXException { - Document dom = HtmlUtil.getHtmlDocument(getEpisodeListLink(searchResult)); + Document dom = getHtmlDocument(getEpisodeListLink(searchResult).toURL()); - List nodes = XPathUtil.selectNodes("id('eplist')//TR/TD/SPAN/ancestor::TR", dom); - - NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); - numberFormat.setMinimumIntegerDigits(Math.max(Integer.toString(nodes.size()).length(), 2)); - numberFormat.setGroupingUsed(false); + List nodes = selectNodes("id('eplist')//TR/TD/SPAN/ancestor::TR", dom); ArrayList episodes = new ArrayList(nodes.size()); for (Node node : nodes) { - String number = XPathUtil.selectString("./TD[contains(@class,'id')]/A", node); - String title = XPathUtil.selectString("./TD[@class='title']/LABEL/text()", node); + String number = selectString("./TD[contains(@class,'id')]/A", node); + String title = selectString("./TD[@class='title']/LABEL/text()", node); - if (title.startsWith("recap")) + if (title.startsWith("recap")) { title = title.replaceFirst("recap", ""); - - try { - // try to format number of episode - number = numberFormat.format(Integer.parseInt(number)); - - // no seasons for anime - episodes.add(new Episode(searchResult.getName(), number, title)); - } catch (NumberFormatException ex) { - // ignore node, episode is probably some kind of special (S1, S2, ...) } + // if number does not match, episode is probably some kind of special (S1, S2, ...) + if (number.matches("\\d+")) { + // no seasons for anime + episodes.add(new Episode(searchResult.getName(), number, title)); + } } return episodes; @@ -148,14 +145,4 @@ public class AnidbClient implements EpisodeListClient { throw new UnsupportedOperationException(); } - - private URL getSearchUrl(String searchterm) throws UnsupportedEncodingException, MalformedURLException { - String qs = URLEncoder.encode(searchterm, "UTF-8"); - - // type=2 -> only TV Series - String path = "/perl-bin/animedb.pl?type=2&show=animelist&orderby.name=0.1&orderbar=0&noalias=1&do.search=Search&adb.search=" + qs; - - return new URL("http", host, path); - } - } diff --git a/source/net/sourceforge/filebot/web/Episode.java b/source/net/sourceforge/filebot/web/Episode.java index 4f32d72d..9da5bc52 100644 --- a/source/net/sourceforge/filebot/web/Episode.java +++ b/source/net/sourceforge/filebot/web/Episode.java @@ -2,34 +2,37 @@ package net.sourceforge.filebot.web; -public class Episode { +import java.io.Serializable; + + +public class Episode implements Serializable { - private final String showName; - private final String numberOfSeason; - private final String numberOfEpisode; - private final String title; + private String showName; + private String seasonNumber; + private String episodeNumber; + private String title; - public Episode(String showname, String numberOfSeason, String numberOfEpisode, String title) { - this.showName = showname; - this.numberOfSeason = numberOfSeason; - this.numberOfEpisode = numberOfEpisode; + public Episode(String showName, String seasonNumber, String episodeNumber, String title) { + this.showName = showName; + this.seasonNumber = seasonNumber; + this.episodeNumber = episodeNumber; this.title = title; } - public Episode(String showname, String numberOfEpisode, String title) { - this(showname, null, numberOfEpisode, title); + public Episode(String showName, String episodeNumber, String title) { + this(showName, null, episodeNumber, title); } - public String getNumberOfEpisode() { - return numberOfEpisode; + public String getEpisodeNumber() { + return episodeNumber; } - public String getNumberOfSeason() { - return numberOfSeason; + public String getSeasonNumber() { + return seasonNumber; } @@ -43,16 +46,36 @@ public class Episode { } + public void setShowName(String seriesName) { + this.showName = seriesName; + } + + + public void setSeasonNumber(String seasonNumber) { + this.seasonNumber = seasonNumber; + } + + + public void setEpisodeNumber(String episodeNumber) { + this.episodeNumber = episodeNumber; + } + + + public void setTitle(String episodeName) { + this.title = episodeName; + } + + @Override public String toString() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(40); sb.append(showName + " - "); - if (numberOfSeason != null) - sb.append(numberOfSeason + "x"); + if (seasonNumber != null) + sb.append(seasonNumber + "x"); - sb.append(numberOfEpisode); + sb.append(episodeNumber); sb.append(" - " + title); diff --git a/source/net/sourceforge/filebot/web/EpisodeListClient.java b/source/net/sourceforge/filebot/web/EpisodeListClient.java index 3f132dc0..a81671cd 100644 --- a/source/net/sourceforge/filebot/web/EpisodeListClient.java +++ b/source/net/sourceforge/filebot/web/EpisodeListClient.java @@ -10,7 +10,7 @@ import javax.swing.Icon; public interface EpisodeListClient { - public Collection search(String searchterm) throws Exception; + public Collection search(String query) throws Exception; public boolean hasSingleSeasonSupport(); diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java index ba27dd74..c8e6e1a6 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleClient.java @@ -11,7 +11,6 @@ import java.util.logging.Logger; import javax.swing.Icon; -import net.sourceforge.filebot.FileBotUtil; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.Timer; @@ -21,9 +20,14 @@ import net.sourceforge.tuned.Timer; */ public class OpenSubtitlesSubtitleClient implements SubtitleClient { - private final OpenSubtitlesClient client = new OpenSubtitlesClient(String.format("%s v%s", FileBotUtil.getApplicationName(), FileBotUtil.getApplicationVersion())); + private final OpenSubtitlesClient client; + public OpenSubtitlesSubtitleClient(String useragent) { + this.client = new OpenSubtitlesClient(useragent); + } + + @Override public String getName() { return "OpenSubtitles"; @@ -38,10 +42,10 @@ public class OpenSubtitlesSubtitleClient implements SubtitleClient { @SuppressWarnings("unchecked") @Override - public List search(String searchterm) throws Exception { + public List search(String query) throws Exception { login(); - return (List) client.searchMoviesOnIMDB(searchterm); + return (List) client.searchMoviesOnIMDB(query); } diff --git a/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java b/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java new file mode 100644 index 00000000..4822f50c --- /dev/null +++ b/source/net/sourceforge/filebot/web/SeasonOutOfBoundsException.java @@ -0,0 +1,39 @@ + +package net.sourceforge.filebot.web; + + +public class SeasonOutOfBoundsException extends IndexOutOfBoundsException { + + private final String showName; + private final int season; + private final int maxSeason; + + + public SeasonOutOfBoundsException(String showName, int season, int maxSeason) { + this.showName = showName; + this.season = season; + this.maxSeason = maxSeason; + } + + + @Override + public String getMessage() { + return String.format("%s has only %d season%s.", showName, maxSeason, maxSeason != 1 ? "s" : ""); + } + + + public String getShowName() { + return showName; + } + + + public int getSeason() { + return season; + } + + + public int getMaxSeason() { + return maxSeason; + } + +} diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java index 34b4588f..f63b5b6f 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java @@ -2,8 +2,12 @@ package net.sourceforge.filebot.web; +import static net.sourceforge.filebot.web.WebRequest.getHtmlDocument; +import static net.sourceforge.tuned.XPathUtil.selectNode; +import static net.sourceforge.tuned.XPathUtil.selectNodes; +import static net.sourceforge.tuned.XPathUtil.selectString; + import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; @@ -24,7 +28,6 @@ import javax.swing.Icon; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.FileUtil; -import net.sourceforge.tuned.XPathUtil; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -51,18 +54,20 @@ public class SubsceneSubtitleClient implements SubtitleClient { @Override - public List search(String searchterm) throws IOException, SAXException { + public List search(String query) throws IOException, SAXException { - Document dom = HtmlUtil.getHtmlDocument(getSearchUrl(searchterm)); + URL searchUrl = new URL("http", host, "/filmsearch.aspx?q=" + URLEncoder.encode(query, "UTF-8")); - List nodes = XPathUtil.selectNodes("id('filmSearch')/A", dom); + Document dom = getHtmlDocument(searchUrl); + + List nodes = selectNodes("id('filmSearch')/A", dom); List searchResults = new ArrayList(nodes.size()); for (Node node : nodes) { - String title = XPathUtil.selectString("text()", node); - String href = XPathUtil.selectString("@href", node); - String count = XPathUtil.selectString("./DFN", node).replaceAll("\\D+", ""); + String title = selectString("text()", node); + String href = selectString("@href", node); + String count = selectString("./DFN", node).replaceAll("\\D+", ""); try { URL subtitleListUrl = new URL("http", host, href); @@ -82,10 +87,10 @@ public class SubsceneSubtitleClient implements SubtitleClient { if (subtitleNodeCount > 0) { // get name of current search result - String name = XPathUtil.selectString("id('leftWrapperWide')//H1/text()", dom); + String name = selectString("id('leftWrapperWide')//H1/text()", dom); // get current location - String file = XPathUtil.selectString("id('aspnetForm')/@action", dom); + String file = selectString("id('aspnetForm')/@action", dom); try { URL url = new URL("http", host, file); @@ -103,15 +108,15 @@ public class SubsceneSubtitleClient implements SubtitleClient { private void updateLanguageFilterMap(Document subtitleListDocument) { - List nodes = XPathUtil.selectNodes("//DIV[@class='languageList']/DIV", subtitleListDocument); + List nodes = selectNodes("//DIV[@class='languageList']/DIV", subtitleListDocument); for (Node node : nodes) { - String onClick = XPathUtil.selectString("./INPUT/@onclick", node); + String onClick = selectString("./INPUT/@onclick", node); String filter = new Scanner(onClick).findInLine("\\d+"); if (filter != null) { - String name = XPathUtil.selectString("./LABEL/text()", node); + String name = selectString("./LABEL/text()", node); languageFilterMap.put(name.toLowerCase(), Integer.valueOf(filter)); } @@ -175,8 +180,7 @@ public class SubsceneSubtitleClient implements SubtitleClient { private boolean useFilteredDocument(SearchResult searchResult) { - SubsceneSearchResult sr = (SubsceneSearchResult) searchResult; - return sr.getSubtitleCount() > 50; + return ((SubsceneSearchResult) searchResult).getSubtitleCount() > 50; } @@ -187,12 +191,12 @@ public class SubsceneSubtitleClient implements SubtitleClient { connection.addRequestProperty("Cookie", "subscene_sLanguageIds=" + languageFilter); } - return HtmlUtil.getHtmlDocument(connection); + return getHtmlDocument(connection); } private List getSubtitleNodes(Document subtitleListDocument) { - return XPathUtil.selectNodes("//TABLE[@class='filmSubtitleList']//A[@id]//ancestor::TR", subtitleListDocument); + return selectNodes("//TABLE[@class='filmSubtitleList']//A[@id]//ancestor::TR", subtitleListDocument); } @@ -204,14 +208,14 @@ public class SubsceneSubtitleClient implements SubtitleClient { for (Node node : subtitleNodes) { try { - Node linkNode = XPathUtil.selectFirstNode("./TD[1]/A", node); - String lang = XPathUtil.selectString("./SPAN[1]", linkNode); + Node linkNode = selectNode("./TD[1]/A", node); + String lang = selectString("./SPAN[1]", linkNode); if (languageName == null || languageName.equalsIgnoreCase(lang)) { - String href = XPathUtil.selectString("@href", linkNode); - String name = XPathUtil.selectString("./SPAN[2]", linkNode); - String author = XPathUtil.selectString("./TD[4]", node); + String href = selectString("@href", linkNode); + String name = selectString("./SPAN[2]", linkNode); + String author = selectString("./TD[4]", node); Matcher matcher = hrefPattern.matcher(href); @@ -247,13 +251,6 @@ public class SubsceneSubtitleClient implements SubtitleClient { return ((HyperLink) searchResult).toURI(); } - - 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); - } - protected static class SubsceneSearchResult extends HyperLink { diff --git a/source/net/sourceforge/filebot/web/SubtitleClient.java b/source/net/sourceforge/filebot/web/SubtitleClient.java index 0c6e1231..1cba315a 100644 --- a/source/net/sourceforge/filebot/web/SubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SubtitleClient.java @@ -11,7 +11,7 @@ import javax.swing.Icon; public interface SubtitleClient { - public Collection search(String searchterm) throws Exception; + public Collection search(String query) throws Exception; public Collection getSubtitleList(SearchResult searchResult, Locale language) throws Exception; diff --git a/source/net/sourceforge/filebot/web/TVDotComClient.java b/source/net/sourceforge/filebot/web/TVDotComClient.java index 76ec42d8..1f9dbebb 100644 --- a/source/net/sourceforge/filebot/web/TVDotComClient.java +++ b/source/net/sourceforge/filebot/web/TVDotComClient.java @@ -2,17 +2,17 @@ package net.sourceforge.filebot.web; +import static net.sourceforge.filebot.web.WebRequest.getHtmlDocument; +import static net.sourceforge.tuned.XPathUtil.selectNodes; +import static net.sourceforge.tuned.XPathUtil.selectString; + import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLEncoder; -import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -23,7 +23,6 @@ import java.util.logging.Logger; import javax.swing.Icon; import net.sourceforge.filebot.ResourceManager; -import net.sourceforge.tuned.XPathUtil; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -54,17 +53,20 @@ public class TVDotComClient implements EpisodeListClient { @Override - public List search(String searchterm) throws IOException, SAXException { + public List search(String query) throws IOException, SAXException { - Document dom = HtmlUtil.getHtmlDocument(getSearchUrl(searchterm)); + // use ajax search request, because we don't need the whole search result page + URL searchUrl = new URL("http", host, "/search.php?type=Search&stype=ajax_search&search_type=program&qs=" + URLEncoder.encode(query, "UTF-8")); - List nodes = XPathUtil.selectNodes("//H3[@class='title']/A", dom); + Document dom = getHtmlDocument(searchUrl); + + List nodes = selectNodes("//H3[@class='title']/A", dom); List searchResults = new ArrayList(nodes.size()); for (Node node : nodes) { String title = node.getTextContent(); - String href = XPathUtil.selectString("@href", node); + String href = selectString("@href", node); try { URL episodeListingUrl = new URL(href.replaceFirst("summary.html\\?.*", "episode_listings.html")); @@ -83,10 +85,10 @@ public class TVDotComClient implements EpisodeListClient { public List getEpisodeList(SearchResult searchResult) throws Exception { // get document for season 1 - Document dom = HtmlUtil.getHtmlDocument(getEpisodeListLink(searchResult, 1)); + Document dom = getHtmlDocument(getEpisodeListLink(searchResult, 1).toURL()); // seasons are ordered in reverse, first element is latest season - String latestSeasonString = XPathUtil.selectString("id('eps_table')//*[starts-with(text(),'Season:')]/*[1]/text()", dom); + String latestSeasonString = selectString("id('eps_table')//*[starts-with(text(),'Season:')]/*[1]/text()", dom); if (latestSeasonString.isEmpty()) { // assume single season series @@ -129,7 +131,7 @@ public class TVDotComClient implements EpisodeListClient { @Override public List getEpisodeList(SearchResult searchResult, int season) throws IOException, SAXException { - Document dom = HtmlUtil.getHtmlDocument(getEpisodeListLink(searchResult, season)); + Document dom = getHtmlDocument(getEpisodeListLink(searchResult, season).toURL()); return getEpisodeList(searchResult, season, dom); } @@ -137,32 +139,31 @@ public class TVDotComClient implements EpisodeListClient { private List getEpisodeList(SearchResult searchResult, int seasonNumber, Document dom) { - List nodes = XPathUtil.selectNodes("id('eps_table')//TD[@class='ep_title']/parent::TR", dom); + List nodes = selectNodes("id('eps_table')//TD[@class='ep_title']/parent::TR", dom); + + // create mutable list from nodes so we can reverse the list + nodes = new ArrayList(nodes); // episodes are ordered in reverse ... we definitely don't want that! Collections.reverse(nodes); - NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); - numberFormat.setMinimumIntegerDigits(Math.max(Integer.toString(nodes.size()).length(), 2)); - numberFormat.setGroupingUsed(false); - Integer episodeOffset = null; ArrayList episodes = new ArrayList(nodes.size()); for (Node node : nodes) { - String episode = XPathUtil.selectString("./TD[1]", node); - String title = XPathUtil.selectString("./TD[2]//A", node); - String season = Integer.toString(seasonNumber); + String episode = selectString("./TD[1]", node); + String title = selectString("./TD[2]//A", node); + String season = String.valueOf(seasonNumber); try { - // format number of episode + // convert the absolute episode number to the season episode number int n = Integer.parseInt(episode); if (episodeOffset == null) episodeOffset = n - 1; - episode = numberFormat.format(n - episodeOffset); + episode = String.valueOf(n - episodeOffset); } catch (NumberFormatException e) { // episode may be "Pilot", "Special", "TV Movie" ... season = null; @@ -188,14 +189,6 @@ public class TVDotComClient implements EpisodeListClient { return URI.create(episodeListingUrl + "?season=" + season); } - - private URL getSearchUrl(String searchterm) throws UnsupportedEncodingException, MalformedURLException { - String qs = URLEncoder.encode(searchterm, "UTF-8"); - String file = "/search.php?type=Search&stype=ajax_search&search_type=program&qs=" + qs; - - return new URL("http", host, file); - } - private class GetEpisodeList implements Callable> { diff --git a/source/net/sourceforge/filebot/web/TVRageClient.java b/source/net/sourceforge/filebot/web/TVRageClient.java index 1a96bb9a..69d2ef19 100644 --- a/source/net/sourceforge/filebot/web/TVRageClient.java +++ b/source/net/sourceforge/filebot/web/TVRageClient.java @@ -2,18 +2,28 @@ package net.sourceforge.filebot.web; +import static net.sourceforge.filebot.web.WebRequest.getDocument; +import static net.sourceforge.tuned.XPathUtil.getTextContent; +import static net.sourceforge.tuned.XPathUtil.selectInteger; +import static net.sourceforge.tuned.XPathUtil.selectNodes; +import static net.sourceforge.tuned.XPathUtil.selectString; + import java.io.IOException; import java.net.URI; +import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.Icon; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; import net.sourceforge.filebot.ResourceManager; -import net.sourceforge.tuned.XPathUtil; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -24,6 +34,8 @@ public class TVRageClient implements EpisodeListClient { private static final String host = "www.tvrage.com"; + private final Cache cache = CacheManager.getInstance().getCache("web"); + @Override public String getName() { @@ -44,20 +56,20 @@ public class TVRageClient implements EpisodeListClient { @Override - public List search(String searchterm) throws SAXException, IOException, ParserConfigurationException { + public List search(String query) throws SAXException, IOException, ParserConfigurationException { - String searchUri = String.format("http://" + host + "/feeds/search.php?show=" + URLEncoder.encode(searchterm, "UTF-8")); + URL searchUrl = new URL("http", host, "/feeds/full_search.php?show=" + URLEncoder.encode(query, "UTF-8")); - Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(searchUri); + Document dom = getDocument(searchUrl); - List nodes = XPathUtil.selectNodes("Results/show", dom); + List nodes = selectNodes("Results/show", dom); List searchResults = new ArrayList(nodes.size()); for (Node node : nodes) { - int showid = XPathUtil.selectInteger("showid", node); - String name = XPathUtil.selectString("name", node); - String link = XPathUtil.selectString("link", node); + int showid = selectInteger("showid", node); + String name = selectString("name", node); + String link = selectString("link", node); searchResults.add(new TVRageSearchResult(name, showid, link)); } @@ -66,25 +78,71 @@ public class TVRageClient implements EpisodeListClient { } - private EpisodeListFeed getEpisodeListFeed(SearchResult searchResult) throws SAXException, IOException, ParserConfigurationException { - int showId = ((TVRageSearchResult) searchResult).getShowId(); - String episodeListUri = String.format("http://" + host + "/feeds/episode_list.php?sid=" + showId); - - Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(episodeListUri); - - return new EpisodeListFeed(dom); - } - - + @SuppressWarnings("unchecked") @Override - public List getEpisodeList(SearchResult searchResult) throws Exception { - return getEpisodeListFeed(searchResult).getEpisodeList(); + public List getEpisodeList(SearchResult searchResult) throws IOException, SAXException, ParserConfigurationException { + int showId = ((TVRageSearchResult) searchResult).getShowId(); + + URL episodeListUrl = new URL("http", host, "/feeds/episode_list.php?sid=" + showId); + + // try to load from cache + Element cacheEntry = cache.get(episodeListUrl.toString()); + + if (cacheEntry != null) { + return (List) cacheEntry.getValue(); + } + + Document dom = getDocument(episodeListUrl); + + String showName = selectString("Show/name", dom); + List nodes = selectNodes("Show/Episodelist/Season/episode", dom); + + List episodes = new ArrayList(nodes.size()); + + for (Node node : nodes) { + String title = getTextContent("title", node); + String episodeNumber = getTextContent("seasonnum", node); + String seasonNumber = node.getParentNode().getAttributes().getNamedItem("no").getTextContent(); + + episodes.add(new Episode(showName, seasonNumber, episodeNumber, title)); + } + + // populate cache + cache.put(new Element(episodeListUrl.toString(), episodes)); + + return episodes; } @Override public List getEpisodeList(SearchResult searchResult, int season) throws IOException, SAXException, ParserConfigurationException { - return getEpisodeListFeed(searchResult).getEpisodeList(season); + + List episodes = new ArrayList(25); + + // remember max. season, so we can throw a proper exception, in case an illegal season number was requested + int maxSeason = 0; + + // filter given season from all seasons + for (Episode episode : getEpisodeList(searchResult)) { + try { + int seasonNumber = Integer.parseInt(episode.getSeasonNumber()); + + if (season == seasonNumber) { + episodes.add(episode); + } + + if (seasonNumber > maxSeason) { + maxSeason = seasonNumber; + } + } catch (NumberFormatException e) { + Logger.getLogger("global").log(Level.WARNING, "Illegal season number", e); + } + } + + if (episodes.isEmpty()) + throw new SeasonOutOfBoundsException(searchResult.getName(), season, maxSeason); + + return episodes; } @@ -96,7 +154,7 @@ public class TVRageClient implements EpisodeListClient { @Override public URI getEpisodeListLink(SearchResult searchResult, int season) { - return getEpisodeListLink(searchResult, Integer.toString(season)); + return getEpisodeListLink(searchResult, String.valueOf(season)); } @@ -107,7 +165,7 @@ public class TVRageClient implements EpisodeListClient { } - protected static class TVRageSearchResult extends SearchResult { + public static class TVRageSearchResult extends SearchResult { private final int showId; private final String link; @@ -131,63 +189,4 @@ public class TVRageClient implements EpisodeListClient { } - - private static class EpisodeListFeed { - - private final String name; - - private final int totalSeasons; - - private final Document feed; - - - public EpisodeListFeed(Document feed) { - name = XPathUtil.selectString("Show/name", feed); - totalSeasons = XPathUtil.selectInteger("Show/totalseasons", feed); - - this.feed = feed; - } - - - public String getName() { - return name; - } - - - public int getTotalSeasons() { - return totalSeasons; - } - - - public List getEpisodeList() { - List episodes = new ArrayList(150); - - for (int i = 0; i <= getTotalSeasons(); i++) { - episodes.addAll(getEpisodeList(i)); - } - - return episodes; - } - - - public List getEpisodeList(int season) { - if (season > getTotalSeasons() || season < 0) - throw new IndexOutOfBoundsException(String.format("%s has only %d season%s.", getName(), getTotalSeasons(), getTotalSeasons() != 1 ? "s" : "")); - - List nodes = XPathUtil.selectNodes("//Season[@no='" + season + "']/episode", feed); - - List episodes = new ArrayList(nodes.size()); - String numberOfSeason = Integer.toString(season); - - for (Node node : nodes) { - String title = XPathUtil.selectString("title", node); - String episodeNumber = XPathUtil.selectString("seasonnum", node); - - episodes.add(new Episode(getName(), numberOfSeason, episodeNumber, title)); - } - - return episodes; - } - } - } diff --git a/source/net/sourceforge/filebot/web/TheTVDBClient.java b/source/net/sourceforge/filebot/web/TheTVDBClient.java new file mode 100644 index 00000000..1e2de3a5 --- /dev/null +++ b/source/net/sourceforge/filebot/web/TheTVDBClient.java @@ -0,0 +1,373 @@ + +package net.sourceforge.filebot.web; + + +import static net.sourceforge.filebot.web.WebRequest.getDocument; +import static net.sourceforge.tuned.XPathUtil.getTextContent; +import static net.sourceforge.tuned.XPathUtil.selectInteger; +import static net.sourceforge.tuned.XPathUtil.selectNodes; +import static net.sourceforge.tuned.XPathUtil.selectString; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.swing.Icon; +import javax.xml.parsers.ParserConfigurationException; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; +import net.sourceforge.filebot.ResourceManager; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + + +public class TheTVDBClient implements EpisodeListClient { + + private static final String host = "www.thetvdb.com"; + + private final String apikey; + + private final Map mirrors = new EnumMap(MirrorType.class); + + private final TheTVDBCache cache = new TheTVDBCache(CacheManager.getInstance().getCache("web")); + + + public TheTVDBClient(String apikey) { + this.apikey = apikey; + } + + + @Override + public String getName() { + return "TheTVDB"; + } + + + @Override + public Icon getIcon() { + return ResourceManager.getIcon("search.thetvdb"); + } + + + @Override + public boolean hasSingleSeasonSupport() { + return true; + } + + + @Override + public List search(String query) throws Exception { + return search(query, Locale.ENGLISH); + } + + + public List search(String query, Locale language) throws Exception { + + URL searchUrl = new URL("http", host, "/api/GetSeries.php?seriesname=" + URLEncoder.encode(query, "UTF-8") + "&language=" + language.getLanguage()); + + Document dom = getDocument(searchUrl); + + List nodes = selectNodes("Data/Series", dom); + + List searchResults = new ArrayList(nodes.size()); + + for (Node node : nodes) { + int seriesId = selectInteger("seriesid", node); + String seriesName = selectString("SeriesName", node); + + searchResults.add(new TheTVDBSearchResult(seriesName, seriesId)); + } + + return searchResults; + } + + + @Override + public List getEpisodeList(SearchResult searchResult) throws Exception { + return getEpisodeList((TheTVDBSearchResult) searchResult, Locale.ENGLISH); + } + + + @Override + public List getEpisodeList(SearchResult searchResult, int season) throws Exception { + + List episodes = new ArrayList(25); + + // remember max. season, so we can throw a proper exception, in case an illegal season number was requested + int maxSeason = 0; + + // filter given season from all seasons + for (Episode episode : getEpisodeList(searchResult)) { + try { + int seasonNumber = Integer.parseInt(episode.getSeasonNumber()); + + if (season == seasonNumber) { + episodes.add(episode); + } + + if (seasonNumber > maxSeason) { + maxSeason = seasonNumber; + } + } catch (NumberFormatException e) { + Logger.getLogger("global").log(Level.WARNING, "Illegal season number", e); + } + } + + if (episodes.isEmpty()) + throw new SeasonOutOfBoundsException(searchResult.getName(), season, maxSeason); + + return episodes; + } + + + public List getEpisodeList(TheTVDBSearchResult searchResult, Locale language) throws Exception { + + List episodes = cache.getEpisodeList(searchResult.getSeriesId(), language); + + if (episodes != null) + return episodes; + + Document seriesRecord = getSeriesRecord(searchResult, language); + + // we could get the series name from the search result, but the language may not match the given parameter + String seriesName = selectString("Data/Series/SeriesName", seriesRecord); + + List nodes = selectNodes("Data/Episode", seriesRecord); + + episodes = new ArrayList(nodes.size()); + + for (Node node : nodes) { + String episodeName = getTextContent("EpisodeName", node); + String episodeNumber = getTextContent("EpisodeNumber", node); + String seasonNumber = getTextContent("SeasonNumber", node); + + episodes.add(new Episode(seriesName, seasonNumber, episodeNumber, episodeName)); + + if (episodeNumber.equals("1")) { + // cache seasonId for each season (always when we are at the first episode) + // because it might be required by getEpisodeListLink + cache.putSeasonId(searchResult.getSeriesId(), Integer.parseInt(seasonNumber), Integer.parseInt(getTextContent("seasonid", node))); + } + } + + cache.putEpisodeList(searchResult.getSeriesId(), language, episodes); + return episodes; + } + + + public Document getSeriesRecord(TheTVDBSearchResult searchResult, Locale language) throws IOException, SAXException, ParserConfigurationException { + + URL seriesRecordUrl = new URL(getMirror(MirrorType.ZIP) + "/api/" + apikey + "/series/" + searchResult.getSeriesId() + "/all/" + language.getLanguage() + ".zip"); + + ZipInputStream zipInputStream = new ZipInputStream(seriesRecordUrl.openStream()); + ZipEntry zipEntry; + + try { + String seriesRecordName = language.getLanguage() + ".xml"; + + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (seriesRecordName.equals(zipEntry.getName())) { + return getDocument(zipInputStream); + } + } + + // zip file must contain the series record + throw new FileNotFoundException(String.format("Archive must contain %s: %s", seriesRecordName, seriesRecordUrl)); + } finally { + zipInputStream.close(); + } + } + + + @Override + public URI getEpisodeListLink(SearchResult searchResult) { + int seriesId = ((TheTVDBSearchResult) searchResult).getSeriesId(); + + return URI.create("http://www.thetvdb.com/?tab=seasonall&id=" + seriesId); + } + + + @Override + public URI getEpisodeListLink(SearchResult searchResult, int season) { + int seriesId = ((TheTVDBSearchResult) searchResult).getSeriesId(); + + try { + Integer seasonId = cache.getSeasonId(seriesId, season); + + if (seasonId == null) { + // get episode xml from first episode of given season + Document dom = getDocument(new URL("http", host, "/api/" + apikey + "/series/" + seriesId + "/default/" + season + "/1/en.xml")); + + seasonId = selectInteger("Data/Episode/seasonid", dom); + + cache.putSeasonId(seriesId, season, seasonId); + } + + return new URI("http://www.thetvdb.com/?tab=season&seriesid=" + seriesId + "&seasonid=" + seasonId); + } catch (Exception e) { + Logger.getLogger("global").log(Level.WARNING, "Failed to retrieve season id", e); + } + + return null; + } + + + protected String getMirror(MirrorType mirrorType) throws IOException, SAXException, ParserConfigurationException { + synchronized (mirrors) { + if (mirrors.isEmpty()) { + // initialize mirrors + URL mirrorUrl = new URL("http", host, "/api/" + apikey + "/mirrors.xml"); + + Document dom = getDocument(mirrorUrl); + + // all mirrors by type + Map> mirrorListMap = new EnumMap>(MirrorType.class); + + // initialize mirror list per type + for (MirrorType type : MirrorType.values()) { + mirrorListMap.put(type, new ArrayList(5)); + } + + // traverse all mirrors + for (Node node : selectNodes("Mirrors/Mirror", dom)) { + // mirror data + String mirror = selectString("mirrorpath", node); + int typeMask = selectInteger("typemask", node); + + // add mirror to the according type lists + for (MirrorType type : MirrorType.fromTypeMask(typeMask)) { + mirrorListMap.get(type).add(mirror); + } + } + + // put random entry from each type list into mirrors + Random random = new Random(); + + for (MirrorType type : MirrorType.values()) { + List list = mirrorListMap.get(type); + + if (!list.isEmpty()) { + mirrors.put(type, list.get(random.nextInt(list.size()))); + } + } + } + + return mirrors.get(mirrorType); + } + } + + + public static class TheTVDBSearchResult extends SearchResult { + + private final int seriesId; + + + public TheTVDBSearchResult(String seriesName, int seriesId) { + super(seriesName); + this.seriesId = seriesId; + } + + + public int getSeriesId() { + return seriesId; + } + + } + + + protected static enum MirrorType { + XML(1), + BANNER(2), + ZIP(4); + + private final int bitMask; + + + private MirrorType(int bitMask) { + this.bitMask = bitMask; + } + + + public static EnumSet fromTypeMask(int typeMask) { + // initialize enum set with all types + EnumSet enumSet = EnumSet.allOf(MirrorType.class); + + for (MirrorType type : values()) { + if ((typeMask & type.bitMask) == 0) { + // remove types that are not set + enumSet.remove(type); + } + } + + return enumSet; + }; + + } + + + private static class TheTVDBCache { + + private final Cache cache; + + + public TheTVDBCache(Cache cache) { + this.cache = cache; + } + + + public void putSeasonId(int seriesId, int seasonNumber, int seasonId) { + cache.put(new Element(key(host, seriesId, seasonNumber, "SeasonId"), seasonId)); + } + + + public Integer getSeasonId(int seriesId, int seasonNumber) { + Element element = cache.get(key(host, seriesId, seasonNumber, "SeasonId")); + + if (element != null) + return (Integer) element.getValue(); + + return null; + } + + + public void putEpisodeList(int seriesId, Locale language, List episodes) { + cache.put(new Element(key(host, seriesId, language, "EpisodeList"), episodes)); + } + + + @SuppressWarnings("unchecked") + public List getEpisodeList(int seriesId, Locale language) { + Element element = cache.get(key(host, seriesId, language.getLanguage(), "EpisodeList")); + + if (element != null) + return (List) element.getValue(); + + return null; + } + + + private String key(Object... key) { + return Arrays.toString(key); + } + + } + +} diff --git a/source/net/sourceforge/filebot/web/HtmlUtil.java b/source/net/sourceforge/filebot/web/WebRequest.java similarity index 73% rename from source/net/sourceforge/filebot/web/HtmlUtil.java rename to source/net/sourceforge/filebot/web/WebRequest.java index e657605b..8325f672 100644 --- a/source/net/sourceforge/filebot/web/HtmlUtil.java +++ b/source/net/sourceforge/filebot/web/WebRequest.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; @@ -16,41 +15,17 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + import org.cyberneko.html.parsers.DOMParser; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; -public class HtmlUtil { +public final class WebRequest { - private static Charset getCharset(String contentType) { - if (contentType != null) { - // e.g. Content-Type: text/html; charset=iso-8859-1 - Pattern pattern = Pattern.compile(".*;\\s*charset=(\\S+).*", Pattern.CASE_INSENSITIVE); - Matcher matcher = pattern.matcher(contentType); - - if (matcher.matches()) { - String charsetName = matcher.group(1); - - try { - return Charset.forName(charsetName); - } catch (Exception e) { - Logger.getLogger("global").log(Level.WARNING, e.getMessage()); - } - } - } - - // use UTF-8 if charset cannot be determined - return Charset.forName("UTF-8"); - } - - - public static Document getHtmlDocument(URI uri) throws IOException, SAXException { - return getHtmlDocument(uri.toURL()); - } - - public static Document getHtmlDocument(URL url) throws IOException, SAXException { return getHtmlDocument(url.openConnection()); } @@ -81,4 +56,45 @@ public class HtmlUtil { return parser.getDocument(); } + + public static Document getDocument(URL url) throws SAXException, IOException, ParserConfigurationException { + return getDocument(url.toString()); + } + + + public static Document getDocument(String url) throws SAXException, IOException, ParserConfigurationException { + return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(url); + } + + + public static Document getDocument(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException { + return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream); + } + + + private static Charset getCharset(String contentType) { + if (contentType != null) { + // e.g. Content-Type: text/html; charset=iso-8859-1 + Pattern pattern = Pattern.compile(".*;\\s*charset=(\\S+).*", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(contentType); + + if (matcher.matches()) { + String charsetName = matcher.group(1); + + try { + return Charset.forName(charsetName); + } catch (Exception e) { + Logger.getLogger("global").log(Level.WARNING, e.getMessage()); + } + } + } + + // use UTF-8 if charset cannot be determined + return Charset.forName("UTF-8"); + } + + + private WebRequest() { + throw new UnsupportedOperationException(); + } } diff --git a/source/net/sourceforge/tuned/XPathUtil.java b/source/net/sourceforge/tuned/XPathUtil.java index d397d865..0a5bbfb4 100644 --- a/source/net/sourceforge/tuned/XPathUtil.java +++ b/source/net/sourceforge/tuned/XPathUtil.java @@ -2,7 +2,7 @@ package net.sourceforge.tuned; -import java.util.ArrayList; +import java.util.AbstractList; import java.util.List; import javax.xml.xpath.XPathConstants; @@ -25,31 +25,9 @@ public final class XPathUtil { } - public static Node selectFirstNode(String xpath, Object node) { - try { - NodeList nodeList = (NodeList) getXPath(xpath).evaluate(node, XPathConstants.NODESET); - - if (nodeList.getLength() <= 0) - return null; - - return nodeList.item(0); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static List selectNodes(String xpath, Object node) { try { - NodeList nodeList = (NodeList) getXPath(xpath).evaluate(node, XPathConstants.NODESET); - - ArrayList nodes = new ArrayList(nodeList.getLength()); - - for (int i = 0; i < nodeList.getLength(); i++) { - nodes.add(nodeList.item(i)); - } - - return nodes; + return new NodeListDecorator((NodeList) getXPath(xpath).evaluate(node, XPathConstants.NODESET)); } catch (Exception e) { throw new RuntimeException(e); } @@ -65,6 +43,40 @@ public final class XPathUtil { } + /** + * @param nodeName search for nodes with this name + * @param parentNode search in the child nodes of this nodes + * @return text content of the child node or null if no child with the given name was found + */ + public static Node getChild(String nodeName, Node parentNode) { + for (Node child : new NodeListDecorator(parentNode.getChildNodes())) { + if (nodeName.equals(child.getNodeName())) + return child; + } + + return null; + } + + + /** + * Get text content of the first child node matching the given node name. Use this method + * instead of {@link #selectString(String, Object)} whenever xpath support is not required, + * because it is much faster, especially for large documents. + * + * @param nodeName search for nodes with this name + * @param parentNode search in the child nodes of this nodes + * @return text content of the child node or null if no child with the given name was found + */ + public static String getTextContent(String nodeName, Node parentNode) { + Node child = getChild(nodeName, parentNode); + + if (child == null) + return null; + + return child.getTextContent(); + } + + public static int selectInteger(String xpath, Object node) { return Integer.parseInt(selectString(xpath, node)); } @@ -87,4 +99,28 @@ public final class XPathUtil { throw new UnsupportedOperationException(); } + + protected static class NodeListDecorator extends AbstractList { + + private final NodeList nodes; + + + public NodeListDecorator(NodeList nodes) { + this.nodes = nodes; + } + + + @Override + public Node get(int index) { + return nodes.item(index); + } + + + @Override + public int size() { + return nodes.getLength(); + } + + } + } diff --git a/test/net/sourceforge/filebot/web/AnidbClientTest.java b/test/net/sourceforge/filebot/web/AnidbClientTest.java index 412e44db..8fb1468f 100644 --- a/test/net/sourceforge/filebot/web/AnidbClientTest.java +++ b/test/net/sourceforge/filebot/web/AnidbClientTest.java @@ -67,8 +67,8 @@ public class AnidbClientTest { assertEquals("Monster", first.getShowName()); assertEquals("Herr Dr. Tenma", first.getTitle()); - assertEquals("01", first.getNumberOfEpisode()); - assertEquals(null, first.getNumberOfSeason()); + assertEquals("1", first.getEpisodeNumber()); + assertEquals(null, first.getSeasonNumber()); } @@ -82,8 +82,8 @@ public class AnidbClientTest { assertEquals("Juuni Kokuki", first.getShowName()); assertEquals("Shadow of the Moon, The Sea of Shadow - Chapter 1", first.getTitle()); - assertEquals("01", first.getNumberOfEpisode()); - assertEquals(null, first.getNumberOfSeason()); + assertEquals("1", first.getEpisodeNumber()); + assertEquals(null, first.getSeasonNumber()); } diff --git a/test/net/sourceforge/filebot/web/TVDotComClientTest.java b/test/net/sourceforge/filebot/web/TVDotComClientTest.java index 038d901b..97ade34b 100644 --- a/test/net/sourceforge/filebot/web/TVDotComClientTest.java +++ b/test/net/sourceforge/filebot/web/TVDotComClientTest.java @@ -56,16 +56,16 @@ public class TVDotComClientTest { @Test public void getEpisodeList() throws Exception { - List results = tvdotcom.getEpisodeList(buffySearchResult, 7); + List list = tvdotcom.getEpisodeList(buffySearchResult, 7); - assertEquals(22, results.size()); + assertEquals(22, list.size()); - Episode chosen = results.get(21); + Episode chosen = list.get(21); assertEquals("Buffy the Vampire Slayer", chosen.getShowName()); assertEquals("Chosen", chosen.getTitle()); - assertEquals("22", chosen.getNumberOfEpisode()); - assertEquals("7", chosen.getNumberOfSeason()); + assertEquals("22", chosen.getEpisodeNumber()); + assertEquals("7", chosen.getSeasonNumber()); } @@ -79,8 +79,8 @@ public class TVDotComClientTest { assertEquals("Buffy the Vampire Slayer", first.getShowName()); assertEquals("Unaired Pilot", first.getTitle()); - assertEquals("Pilot", first.getNumberOfEpisode()); - assertEquals(null, first.getNumberOfSeason()); + assertEquals("Pilot", first.getEpisodeNumber()); + assertEquals(null, first.getSeasonNumber()); } @@ -94,8 +94,8 @@ public class TVDotComClientTest { assertEquals("Firefly", fourth.getShowName()); assertEquals("Jaynestown", fourth.getTitle()); - assertEquals("04", fourth.getNumberOfEpisode()); - assertEquals("1", fourth.getNumberOfSeason()); + assertEquals("4", fourth.getEpisodeNumber()); + assertEquals("1", fourth.getSeasonNumber()); } @@ -118,8 +118,8 @@ public class TVDotComClientTest { assertEquals("Lost", episode.getShowName()); assertEquals("Exposé", episode.getTitle()); - assertEquals("14", episode.getNumberOfEpisode()); - assertEquals("3", episode.getNumberOfSeason()); + assertEquals("14", episode.getEpisodeNumber()); + assertEquals("3", episode.getSeasonNumber()); } diff --git a/test/net/sourceforge/filebot/web/TVRageClientTest.java b/test/net/sourceforge/filebot/web/TVRageClientTest.java index 79c8891e..14069080 100644 --- a/test/net/sourceforge/filebot/web/TVRageClientTest.java +++ b/test/net/sourceforge/filebot/web/TVRageClientTest.java @@ -43,8 +43,8 @@ public class TVRageClientTest { assertEquals("Buffy the Vampire Slayer", chosen.getShowName()); assertEquals("Chosen", chosen.getTitle()); - assertEquals("22", chosen.getNumberOfEpisode()); - assertEquals("7", chosen.getNumberOfSeason()); + assertEquals("22", chosen.getEpisodeNumber()); + assertEquals("7", chosen.getSeasonNumber()); } @@ -58,12 +58,12 @@ public class TVRageClientTest { assertEquals("Buffy the Vampire Slayer", first.getShowName()); assertEquals("Unaired Pilot", first.getTitle()); - assertEquals("00", first.getNumberOfEpisode()); - assertEquals("0", first.getNumberOfSeason()); + assertEquals("00", first.getEpisodeNumber()); + assertEquals("0", first.getSeasonNumber()); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = SeasonOutOfBoundsException.class) public void getEpisodeListIllegalSeason() throws Exception { tvrage.getEpisodeList(buffySearchResult, 42); } diff --git a/test/net/sourceforge/filebot/web/TheTVDBClientTest.java b/test/net/sourceforge/filebot/web/TheTVDBClientTest.java new file mode 100644 index 00000000..adf6a669 --- /dev/null +++ b/test/net/sourceforge/filebot/web/TheTVDBClientTest.java @@ -0,0 +1,126 @@ + +package net.sourceforge.filebot.web; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; + +import net.sf.ehcache.CacheManager; +import net.sourceforge.filebot.web.TheTVDBClient.MirrorType; +import net.sourceforge.filebot.web.TheTVDBClient.TheTVDBSearchResult; + +import org.junit.After; +import org.junit.Test; + + +public class TheTVDBClientTest { + + private TheTVDBClient thetvdb = new TheTVDBClient("BA864DEE427E384A"); + + + @After + public void clearCache() { + CacheManager.getInstance().clearAll(); + } + + + @Test + public void search() throws Exception { + // test default language and query escaping (blanks) + List results = thetvdb.search("babylon 5"); + + assertEquals(2, results.size()); + + TheTVDBSearchResult first = (TheTVDBSearchResult) results.get(0); + + assertEquals("Babylon 5", first.getName()); + assertEquals(70726, first.getSeriesId()); + } + + + @Test + public void searchGerman() throws Exception { + List results = thetvdb.search("buffy", Locale.GERMAN); + + assertEquals(3, results.size()); + + TheTVDBSearchResult first = (TheTVDBSearchResult) results.get(0); + + // test encoding (umlauts) + assertEquals("Buffy - Im Bann der Dämonen", first.getName()); + assertEquals(70327, first.getSeriesId()); + + TheTVDBSearchResult second = (TheTVDBSearchResult) results.get(1); + + assertEquals("Buffy the Vampire Slayer", second.getName()); + assertEquals(70327, second.getSeriesId()); + } + + + @Test + public void getEpisodeListAll() throws Exception { + List list = thetvdb.getEpisodeList(new TheTVDBSearchResult("Buffy the Vampire Slayer", 70327)); + + assertEquals(147, list.size()); + + Episode first = list.get(0); + + assertEquals("Buffy the Vampire Slayer", first.getShowName()); + assertEquals("Unaired Pilot", first.getTitle()); + assertEquals("1", first.getEpisodeNumber()); + assertEquals("0", first.getSeasonNumber()); + } + + + @Test + public void getEpisodeListSingleSeason() throws Exception { + List list = thetvdb.getEpisodeList(new TheTVDBSearchResult("Buffy the Vampire Slayer", 70327), 7); + + assertEquals(22, list.size()); + + Episode chosen = list.get(21); + + assertEquals("Buffy the Vampire Slayer", chosen.getShowName()); + assertEquals("Chosen", chosen.getTitle()); + assertEquals("22", chosen.getEpisodeNumber()); + assertEquals("7", chosen.getSeasonNumber()); + } + + + @Test + public void getEpisodeListLink() { + assertEquals("http://www.thetvdb.com/?tab=seasonall&id=78874", thetvdb.getEpisodeListLink(new TheTVDBSearchResult("Firefly", 78874)).toString()); + } + + + @Test + public void getEpisodeListLinkSingleSeason() { + assertEquals("http://www.thetvdb.com/?tab=season&seriesid=73965&seasonid=6749", thetvdb.getEpisodeListLink(new TheTVDBSearchResult("Roswell", 73965), 3).toString()); + } + + + @Test + public void getMirror() throws Exception { + assertNotNull(thetvdb.getMirror(MirrorType.XML)); + assertNotNull(thetvdb.getMirror(MirrorType.BANNER)); + assertNotNull(thetvdb.getMirror(MirrorType.ZIP)); + } + + + @Test + public void resolveTypeMask() { + // no flags set + assertEquals(EnumSet.noneOf(MirrorType.class), MirrorType.fromTypeMask(0)); + + // xml and zip flags set + assertEquals(EnumSet.of(MirrorType.ZIP, MirrorType.XML), MirrorType.fromTypeMask(5)); + + // all flags set + assertEquals(EnumSet.allOf(MirrorType.class), MirrorType.fromTypeMask(7)); + } + +} diff --git a/test/net/sourceforge/filebot/web/WebTestSuite.java b/test/net/sourceforge/filebot/web/WebTestSuite.java index 6c2b7174..6dc88543 100644 --- a/test/net/sourceforge/filebot/web/WebTestSuite.java +++ b/test/net/sourceforge/filebot/web/WebTestSuite.java @@ -8,7 +8,7 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, SubsceneSubtitleClientTest.class, OpenSubtitlesHasherTest.class }) +@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, SubsceneSubtitleClientTest.class, OpenSubtitlesHasherTest.class }) public class WebTestSuite { }