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, E> createRequestProcessor();
public EventList getSearchHistory() {
@@ -104,13 +104,12 @@ public abstract class AbstractSearchPanel extends FileBotPanel {
}
- private void search(RequestProcessor> requestProcessor) {
+ private void search(RequestProcessor, E> 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, E> 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, E> requestProcessor) {
this.requestProcessor = requestProcessor;
}
@Override
- protected Collection doInBackground() throws Exception {
+ protected Collection extends SearchResult> 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, E> requestProcessor;
- public FetchTask(RequestProcessor> requestProcessor) {
+ public FetchTask(RequestProcessor, E> 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 extends SearchResult> 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 extends SearchResult> 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 extends T> 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 {
}