2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
package net.sourceforge.filebot.web;
|
|
|
|
|
|
|
|
|
2009-05-17 13:22:44 -04:00
|
|
|
import static net.sourceforge.filebot.web.EpisodeListUtilities.*;
|
2009-08-18 12:15:58 -04:00
|
|
|
import static net.sourceforge.filebot.web.WebRequest.*;
|
2009-05-17 13:22:44 -04:00
|
|
|
import static net.sourceforge.tuned.XPathUtilities.*;
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
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;
|
2009-07-13 08:41:19 -04:00
|
|
|
import javax.xml.parsers.DocumentBuilderFactory;
|
|
|
|
|
|
|
|
import org.w3c.dom.Document;
|
|
|
|
import org.w3c.dom.Node;
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
import net.sf.ehcache.Cache;
|
|
|
|
import net.sf.ehcache.CacheManager;
|
|
|
|
import net.sf.ehcache.Element;
|
|
|
|
import net.sourceforge.filebot.ResourceManager;
|
|
|
|
|
|
|
|
|
2011-08-08 13:37:45 -04:00
|
|
|
public class TheTVDBClient extends AbstractEpisodeListProvider {
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
private static final String host = "www.thetvdb.com";
|
|
|
|
|
|
|
|
private final String apikey;
|
|
|
|
|
|
|
|
private final Map<MirrorType, String> mirrors = new EnumMap<MirrorType, String>(MirrorType.class);
|
|
|
|
|
|
|
|
private final TheTVDBCache cache = new TheTVDBCache(CacheManager.getInstance().getCache("web"));
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
public TheTVDBClient(String apikey) {
|
2009-01-24 19:08:57 -05:00
|
|
|
if (apikey == null)
|
|
|
|
throw new NullPointerException("apikey must not be null");
|
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
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
|
2011-08-08 13:37:45 -04:00
|
|
|
public boolean hasLocaleSupport() {
|
|
|
|
return true;
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-08-08 13:37:45 -04:00
|
|
|
@Override
|
2009-01-04 13:28:28 -05:00
|
|
|
public List<SearchResult> search(String query, Locale language) throws Exception {
|
2010-11-09 03:04:12 -05:00
|
|
|
// check if the exact series name is already cached
|
|
|
|
Integer cachedResult = cache.getSeriesId(query, language);
|
|
|
|
|
|
|
|
if (cachedResult != null)
|
|
|
|
return Arrays.asList(new SearchResult[] { new TheTVDBSearchResult(query, cachedResult) });
|
|
|
|
|
|
|
|
// perform online search
|
2009-08-18 12:15:58 -04:00
|
|
|
URL url = getResource(null, "/api/GetSeries.php?seriesname=" + URLEncoder.encode(query, "UTF-8") + "&language=" + language.getLanguage());
|
|
|
|
Document dom = getDocument(url);
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
List<Node> nodes = selectNodes("Data/Series", dom);
|
|
|
|
List<SearchResult> searchResults = new ArrayList<SearchResult>(nodes.size());
|
|
|
|
|
|
|
|
for (Node node : nodes) {
|
2010-11-09 03:04:12 -05:00
|
|
|
int seriesId = getIntegerContent("seriesid", node);
|
2009-02-21 07:48:52 -05:00
|
|
|
String seriesName = getTextContent("SeriesName", node);
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
searchResults.add(new TheTVDBSearchResult(seriesName, seriesId));
|
2010-11-09 03:04:12 -05:00
|
|
|
cache.putSeriesId(seriesName, language, seriesId);
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return searchResults;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
2011-08-08 13:37:45 -04:00
|
|
|
public List<Episode> getEpisodeList(SearchResult searchResult, Locale language) throws Exception {
|
|
|
|
TheTVDBSearchResult series = (TheTVDBSearchResult) searchResult;
|
|
|
|
List<Episode> episodes = cache.getEpisodeList(series.getSeriesId(), language);
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
if (episodes != null)
|
|
|
|
return episodes;
|
|
|
|
|
2011-08-08 13:37:45 -04:00
|
|
|
Document seriesRecord = getSeriesRecord(series, language);
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
// 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);
|
2011-10-01 00:08:46 -04:00
|
|
|
Date seriesStartDate = Date.parse(selectString("Data/Series/FirstAired", seriesRecord), "yyyy-MM-dd");
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
List<Node> nodes = selectNodes("Data/Episode", seriesRecord);
|
2010-02-03 16:36:04 -05:00
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
episodes = new ArrayList<Episode>(nodes.size());
|
2010-02-03 16:36:04 -05:00
|
|
|
List<Episode> specials = new ArrayList<Episode>(5);
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
for (Node node : nodes) {
|
|
|
|
String episodeName = getTextContent("EpisodeName", node);
|
2010-11-15 05:06:24 -05:00
|
|
|
String dvdSeasonNumber = getTextContent("DVD_season", node);
|
|
|
|
String dvdEpisodeNumber = getTextContent("DVD_episodenumber", node);
|
2010-11-09 03:04:12 -05:00
|
|
|
Integer absoluteNumber = getIntegerContent("absolute_number", node);
|
2010-10-23 08:47:43 -04:00
|
|
|
Date airdate = Date.parse(getTextContent("FirstAired", node), "yyyy-MM-dd");
|
2009-05-03 11:21:04 -04:00
|
|
|
|
2010-11-15 05:06:24 -05:00
|
|
|
// prefer DVD SxE numbers if available
|
|
|
|
Integer seasonNumber;
|
|
|
|
Integer episodeNumber;
|
|
|
|
|
|
|
|
try {
|
|
|
|
seasonNumber = new Integer(dvdSeasonNumber);
|
|
|
|
episodeNumber = new Float(dvdEpisodeNumber).intValue();
|
|
|
|
} catch (Exception e) {
|
|
|
|
seasonNumber = getIntegerContent("SeasonNumber", node);
|
|
|
|
episodeNumber = getIntegerContent("EpisodeNumber", node);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (seasonNumber == null || seasonNumber == 0) {
|
2010-02-03 16:36:04 -05:00
|
|
|
// handle as special episode
|
2010-10-24 08:10:30 -04:00
|
|
|
Integer airsBefore = getIntegerContent("airsbefore_season", node);
|
|
|
|
if (airsBefore != null) {
|
2010-02-03 16:36:04 -05:00
|
|
|
seasonNumber = airsBefore;
|
|
|
|
}
|
|
|
|
|
2011-10-23 10:09:41 -04:00
|
|
|
// use given episode number as special number or count specials by ourselves
|
|
|
|
Integer specialNumber = (episodeNumber != null) ? episodeNumber : filterBySeason(specials, seasonNumber).size() + 1;
|
2011-10-01 00:08:46 -04:00
|
|
|
specials.add(new Episode(seriesName, seriesStartDate, seasonNumber, null, episodeName, null, specialNumber, airdate));
|
2010-02-03 16:36:04 -05:00
|
|
|
} else {
|
|
|
|
// handle as normal episode
|
2011-10-01 00:08:46 -04:00
|
|
|
episodes.add(new Episode(seriesName, seriesStartDate, seasonNumber, episodeNumber, episodeName, absoluteNumber, null, airdate));
|
2010-02-03 16:36:04 -05:00
|
|
|
}
|
2009-01-04 13:28:28 -05:00
|
|
|
|
2010-10-24 08:10:30 -04:00
|
|
|
if (episodeNumber == 1) {
|
2010-02-03 16:36:04 -05:00
|
|
|
try {
|
|
|
|
// cache seasonId for each season (always when we are at the first episode)
|
|
|
|
// because it might be required by getEpisodeListLink
|
2011-08-08 13:37:45 -04:00
|
|
|
cache.putSeasonId(series.getSeriesId(), seasonNumber, getIntegerContent("seasonid", node));
|
2010-02-03 16:36:04 -05:00
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
// season/episode is not a number, just ignore
|
|
|
|
}
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-15 05:06:24 -05:00
|
|
|
// episodes my not be ordered by DVD episode number
|
|
|
|
sortEpisodes(episodes);
|
|
|
|
|
2010-02-03 16:36:04 -05:00
|
|
|
// add specials at the end
|
|
|
|
episodes.addAll(specials);
|
|
|
|
|
2011-08-08 13:37:45 -04:00
|
|
|
cache.putEpisodeList(series.getSeriesId(), language, episodes);
|
2009-01-04 13:28:28 -05:00
|
|
|
return episodes;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
public Document getSeriesRecord(TheTVDBSearchResult searchResult, Locale language) throws Exception {
|
2009-08-18 12:15:58 -04:00
|
|
|
URL seriesRecord = getResource(MirrorType.ZIP, "/api/" + apikey + "/series/" + searchResult.getSeriesId() + "/all/" + language.getLanguage() + ".zip");
|
2009-01-04 13:28:28 -05:00
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
ZipInputStream zipInputStream = new ZipInputStream(seriesRecord.openStream());
|
2009-01-04 13:28:28 -05:00
|
|
|
ZipEntry zipEntry;
|
|
|
|
|
|
|
|
try {
|
|
|
|
String seriesRecordName = language.getLanguage() + ".xml";
|
|
|
|
|
|
|
|
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
|
|
|
if (seriesRecordName.equals(zipEntry.getName())) {
|
2009-07-13 08:41:19 -04:00
|
|
|
return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(zipInputStream);
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// zip file must contain the series record
|
2009-07-13 08:41:19 -04:00
|
|
|
throw new FileNotFoundException(String.format("Archive must contain %s: %s", seriesRecordName, seriesRecord));
|
2009-01-04 13:28:28 -05:00
|
|
|
} finally {
|
|
|
|
zipInputStream.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public URI getEpisodeListLink(SearchResult searchResult) {
|
|
|
|
int seriesId = ((TheTVDBSearchResult) searchResult).getSeriesId();
|
|
|
|
|
2009-03-17 17:59:19 -04:00
|
|
|
return URI.create("http://" + host + "/?tab=seasonall&id=" + seriesId);
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2009-08-18 12:15:58 -04:00
|
|
|
URL url = getResource(MirrorType.XML, "/api/" + apikey + "/series/" + seriesId + "/default/" + season + "/1/en.xml");
|
|
|
|
Document dom = getDocument(url);
|
2009-01-04 13:28:28 -05:00
|
|
|
|
2009-02-21 07:48:52 -05:00
|
|
|
seasonId = Integer.valueOf(selectString("Data/Episode/seasonid", dom));
|
2009-01-04 13:28:28 -05:00
|
|
|
cache.putSeasonId(seriesId, season, seasonId);
|
|
|
|
}
|
|
|
|
|
2009-02-06 12:57:18 -05:00
|
|
|
return new URI("http://" + host + "/?tab=season&seriesid=" + seriesId + "&seasonid=" + seasonId);
|
2009-03-17 17:59:19 -04:00
|
|
|
} catch (Exception e) {
|
2009-02-06 12:57:18 -05:00
|
|
|
// log and ignore any IOException
|
2009-03-14 06:20:59 -04:00
|
|
|
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Failed to retrieve season id", e);
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
protected String getMirror(MirrorType mirrorType) throws Exception {
|
2009-01-04 13:28:28 -05:00
|
|
|
synchronized (mirrors) {
|
|
|
|
if (mirrors.isEmpty()) {
|
|
|
|
// initialize mirrors
|
2009-08-18 12:15:58 -04:00
|
|
|
Document dom = getDocument(getResource(null, "/api/" + apikey + "/mirrors.xml"));
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
// all mirrors by type
|
|
|
|
Map<MirrorType, List<String>> mirrorListMap = new EnumMap<MirrorType, List<String>>(MirrorType.class);
|
|
|
|
|
|
|
|
// initialize mirror list per type
|
|
|
|
for (MirrorType type : MirrorType.values()) {
|
|
|
|
mirrorListMap.put(type, new ArrayList<String>(5));
|
|
|
|
}
|
|
|
|
|
|
|
|
// traverse all mirrors
|
|
|
|
for (Node node : selectNodes("Mirrors/Mirror", dom)) {
|
|
|
|
// mirror data
|
2009-02-21 07:48:52 -05:00
|
|
|
String mirror = getTextContent("mirrorpath", node);
|
|
|
|
int typeMask = Integer.parseInt(getTextContent("typemask", node));
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
// 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<String> list = mirrorListMap.get(type);
|
|
|
|
|
|
|
|
if (!list.isEmpty()) {
|
|
|
|
mirrors.put(type, list.get(random.nextInt(list.size())));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return mirrors.get(mirrorType);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
|
2009-08-18 12:15:58 -04:00
|
|
|
protected URL getResource(MirrorType mirrorType, String path) throws Exception {
|
|
|
|
// use default server
|
|
|
|
if (mirrorType == null)
|
|
|
|
return new URL("http", host, path);
|
|
|
|
|
|
|
|
// use mirror
|
|
|
|
return new URL(getMirror(mirrorType) + path);
|
2009-07-13 08:41:19 -04:00
|
|
|
}
|
2009-01-04 13:28:28 -05:00
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
public static class TheTVDBSearchResult extends SearchResult {
|
|
|
|
|
|
|
|
private final int seriesId;
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
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;
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
private MirrorType(int bitMask) {
|
|
|
|
this.bitMask = bitMask;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static EnumSet<MirrorType> fromTypeMask(int typeMask) {
|
|
|
|
// initialize enum set with all types
|
|
|
|
EnumSet<MirrorType> 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;
|
|
|
|
|
2009-07-13 08:41:19 -04:00
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
public TheTVDBCache(Cache cache) {
|
|
|
|
this.cache = cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2010-11-09 03:04:12 -05:00
|
|
|
public void putSeriesId(String seriesName, Locale language, int seriesId) {
|
|
|
|
cache.put(new Element(key(host, "SeriesId", seriesName, language.getLanguage()), seriesId));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Integer getSeriesId(String seriesName, Locale language) {
|
|
|
|
Element element = cache.get(key(host, "SeriesId", seriesName, language.getLanguage()));
|
|
|
|
|
|
|
|
if (element != null)
|
|
|
|
return (Integer) element.getValue();
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2009-01-04 13:28:28 -05:00
|
|
|
public void putSeasonId(int seriesId, int seasonNumber, int seasonId) {
|
2010-11-09 03:04:12 -05:00
|
|
|
cache.put(new Element(key(host, "SeasonId", seriesId, seasonNumber), seasonId));
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Integer getSeasonId(int seriesId, int seasonNumber) {
|
2010-11-09 03:04:12 -05:00
|
|
|
Element element = cache.get(key(host, "SeasonId", seriesId, seasonNumber));
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
if (element != null)
|
|
|
|
return (Integer) element.getValue();
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void putEpisodeList(int seriesId, Locale language, List<Episode> episodes) {
|
2010-11-09 03:04:12 -05:00
|
|
|
cache.put(new Element(key(host, "EpisodeList", seriesId, language.getLanguage()), episodes));
|
2009-01-04 13:28:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
public List<Episode> getEpisodeList(int seriesId, Locale language) {
|
2010-11-09 03:04:12 -05:00
|
|
|
Element element = cache.get(key(host, "EpisodeList", seriesId, language.getLanguage()));
|
2009-01-04 13:28:28 -05:00
|
|
|
|
|
|
|
if (element != null)
|
|
|
|
return (List<Episode>) element.getValue();
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private String key(Object... key) {
|
|
|
|
return Arrays.toString(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|