diff --git a/build.xml b/build.xml index 992bb7f2..5951575d 100644 --- a/build.xml +++ b/build.xml @@ -2,7 +2,7 @@ - + @@ -65,6 +65,10 @@ + + + + diff --git a/fw/search.serienjunkies.png b/fw/search.serienjunkies.png new file mode 100644 index 00000000..155f0108 Binary files /dev/null and b/fw/search.serienjunkies.png differ diff --git a/lib/json-simple.jar b/lib/json-simple.jar new file mode 100644 index 00000000..f395f414 Binary files /dev/null and b/lib/json-simple.jar differ diff --git a/source/ehcache.xml b/source/ehcache.xml index 6c38d9c0..a2338ded 100644 --- a/source/ehcache.xml +++ b/source/ehcache.xml @@ -133,11 +133,11 @@ /> - getAnimeTitles() throws Exception { + protected List getAnimeTitles() throws Exception { URL url = new URL("http", host, "/api/animetitles.dat.gz"); // try cache first diff --git a/source/net/sourceforge/filebot/web/SerienjunkiesClient.java b/source/net/sourceforge/filebot/web/SerienjunkiesClient.java new file mode 100644 index 00000000..7045539f --- /dev/null +++ b/source/net/sourceforge/filebot/web/SerienjunkiesClient.java @@ -0,0 +1,309 @@ + +package net.sourceforge.filebot.web; + + +import static net.sourceforge.filebot.web.EpisodeListUtilities.*; +import static net.sourceforge.filebot.web.WebRequest.*; + +import java.io.IOException; +import java.io.Reader; +import java.io.Serializable; +import java.net.URI; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.AbstractMap.SimpleEntry; +import java.util.Map.Entry; + +import javax.net.ssl.HttpsURLConnection; +import javax.swing.Icon; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +import uk.ac.shef.wit.simmetrics.similaritymetrics.AbstractStringMetric; +import uk.ac.shef.wit.simmetrics.similaritymetrics.QGramsDistance; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; +import net.sourceforge.filebot.ResourceManager; + + +public class SerienjunkiesClient implements EpisodeListProvider { + + private static final String host = "api.serienjunkies.de"; + private static final SerienjunkiesCache cache = new SerienjunkiesCache(CacheManager.getInstance().getCache("web-persistent-datasource")); + + private final String apikey; + + + public SerienjunkiesClient(String apikey) { + this.apikey = apikey; + } + + + @Override + public List search(String query) throws Exception { + // normalize + query = query.toLowerCase(); + + AbstractStringMetric metric = new QGramsDistance(); + + final List> resultSet = new ArrayList>(); + + for (SerienjunkiesSearchResult anime : getSeriesTitles()) { + for (String name : new String[] { anime.getMainTitle(), anime.getGermanTitle() }) { + if (name != null) { + // normalize + name = name.toLowerCase(); + + float similarity = metric.getSimilarity(name, query); + + if (similarity > 0.5 || name.contains(query)) { + resultSet.add(new SimpleEntry(anime, similarity)); + + // add only once + break; + } + } + } + } + + // sort by similarity descending (best matches first) + Collections.sort(resultSet, new Comparator>() { + + @Override + public int compare(Entry o1, Entry o2) { + return o2.getValue().compareTo(o1.getValue()); + } + }); + + // view for the first 20 search results + return new AbstractList() { + + @Override + public SearchResult get(int index) { + return resultSet.get(index).getKey(); + } + + + @Override + public int size() { + return Math.min(20, resultSet.size()); + } + }; + } + + + protected List getSeriesTitles() throws Exception { + // try cache first + List seriesList = cache.getSeriesList(); + if (seriesList != null) + return seriesList; + + // fetch series data + Reader reader = getReader(createConnection("allseries.php?d=" + apikey)); + seriesList = new ArrayList(); + + try { + JSONObject data = (JSONObject) JSONValue.parse(reader); + JSONArray list = (JSONArray) data.get("allseries"); + + for (Object element : list) { + JSONObject obj = (JSONObject) element; + + String sid = (String) obj.get("id"); + String mainTitle = (String) obj.get("short"); + String germanTitle = (String) obj.get("short_german"); + + seriesList.add(new SerienjunkiesSearchResult(Integer.parseInt(sid), mainTitle, germanTitle != null && germanTitle.length() > 0 ? germanTitle : null)); + } + + // populate cache + cache.putSeriesList(seriesList); + } finally { + reader.close(); + } + + return seriesList; + } + + + @Override + public List getEpisodeList(SearchResult searchResult) throws Exception { + SerienjunkiesSearchResult series = (SerienjunkiesSearchResult) searchResult; + + // try cache first + List episodes = cache.getEpisodeList(series.getSeriesId()); + if (episodes != null) + return episodes; + + // fetch series data + Reader reader = getReader(createConnection("allepisodes.php?d=" + apikey + "&q=" + series.getSeriesId())); + episodes = new ArrayList(25); + + try { + JSONObject data = (JSONObject) JSONValue.parse(reader); + JSONArray list = (JSONArray) data.get("allepisodes"); + + for (int i = 0; i < list.size(); i++) { + JSONObject obj = (JSONObject) list.get(i); + + String season = (String) obj.get("season"); + String episode = (String) obj.get("episode"); + String title = (String) obj.get("german"); + + episodes.add(new Episode(series.getName(), new Integer(season), new Integer(episode), title, i + 1, null, null)); + } + + // populate cache + cache.putEpisodeList(episodes, series.getSeriesId()); + } finally { + reader.close(); + } + + // make sure episodes are in ordered correctly + sortEpisodes(episodes); + + return episodes; + } + + + private HttpsURLConnection createConnection(String resource) throws IOException, GeneralSecurityException { + URL url = new URL("https", host, resource); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + + // disable SSL certificate validation + connection.setSSLSocketFactory(createIgnoreCertificateSocketFactory()); + + return connection; + } + + + @Override + public List getEpisodeList(SearchResult searchResult, int season) throws Exception { + return null; + } + + + @Override + public URI getEpisodeListLink(SearchResult searchResult) { + return null; + } + + + @Override + public URI getEpisodeListLink(SearchResult searchResult, int season) { + return null; + } + + + @Override + public String getName() { + return "Serienjunkies"; + } + + + @Override + public Icon getIcon() { + return ResourceManager.getIcon("search.serienjunkies"); + } + + + @Override + public boolean hasSingleSeasonSupport() { + return false; + } + + + public static class SerienjunkiesSearchResult extends SearchResult implements Serializable { + + protected int sid; + protected String mainTitle; + protected String germanTitle; + + + protected SerienjunkiesSearchResult() { + // used by serializer + } + + + public SerienjunkiesSearchResult(int sid, String mainTitle, String germanTitle) { + this.sid = sid; + this.mainTitle = mainTitle; + this.germanTitle = germanTitle; + } + + + public int getSeriesId() { + return sid; + } + + + @Override + public String getName() { + return germanTitle != null ? germanTitle : mainTitle; // prefer german title + } + + + public String getMainTitle() { + return mainTitle; + } + + + public String getGermanTitle() { + return germanTitle; + } + } + + + private static class SerienjunkiesCache { + + private final Cache cache; + + + public SerienjunkiesCache(Cache cache) { + this.cache = cache; + } + + + public void putSeriesList(Collection anime) { + cache.put(new Element(host + "SeriesList", anime.toArray(new SerienjunkiesSearchResult[0]))); + } + + + public List getSeriesList() { + Element element = cache.get(host + "SeriesList"); + + if (element != null) + return Arrays.asList((SerienjunkiesSearchResult[]) element.getValue()); + + return null; + } + + + public void putEpisodeList(Collection episodes, int sid) { + cache.put(new Element(host + "EpisodeList" + sid, episodes.toArray(new Episode[0]))); + } + + + public List getEpisodeList(int sid) { + Element element = cache.get(host + "EpisodeList" + sid); + + if (element != null) + return Arrays.asList((Episode[]) element.getValue()); + + return null; + } + + } + +} diff --git a/source/net/sourceforge/filebot/web/WebRequest.java b/source/net/sourceforge/filebot/web/WebRequest.java index 40a75f8d..ee9239e6 100644 --- a/source/net/sourceforge/filebot/web/WebRequest.java +++ b/source/net/sourceforge/filebot/web/WebRequest.java @@ -14,6 +14,9 @@ import java.net.URLConnection; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; @@ -24,6 +27,10 @@ import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -219,6 +226,29 @@ public final class WebRequest { } + public static SSLSocketFactory createIgnoreCertificateSocketFactory() throws GeneralSecurityException { + // create a trust manager that does not validate certificate chains + TrustManager trustAnyCertificate = new X509TrustManager() { + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + }; + + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { trustAnyCertificate }, new SecureRandom()); + return sc.getSocketFactory(); + } + + /** * Dummy constructor to prevent instantiation. */ diff --git a/test/net/sourceforge/filebot/web/SerienjunkiesClientTest.java b/test/net/sourceforge/filebot/web/SerienjunkiesClientTest.java new file mode 100644 index 00000000..f8465c4d --- /dev/null +++ b/test/net/sourceforge/filebot/web/SerienjunkiesClientTest.java @@ -0,0 +1,62 @@ + +package net.sourceforge.filebot.web; + + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import net.sf.ehcache.CacheManager; +import net.sourceforge.filebot.web.SerienjunkiesClient.SerienjunkiesSearchResult; + + +public class SerienjunkiesClientTest { + + private SerienjunkiesClient serienjunkies = new SerienjunkiesClient("9fbhw9uebfiwvbefzuwv"); + + + @Test + public void search() throws Exception { + List results = serienjunkies.search("alias die agentin"); + assertEquals(1, results.size()); + + SerienjunkiesSearchResult first = (SerienjunkiesSearchResult) results.get(0); + assertEquals(34, first.getSeriesId()); + assertEquals("Alias - Die Agentin", first.getName()); + assertEquals("Alias", first.getMainTitle()); + assertEquals("Alias - Die Agentin", first.getGermanTitle()); + } + + + @Test + public void getEpisodeListAll() throws Exception { + List list = serienjunkies.getEpisodeList(new SerienjunkiesSearchResult(260, "Grey's Anatomy", null)); + + // check ordinary episode + Episode eps = list.get(0); + assertEquals("Grey's Anatomy", eps.getSeriesName()); + assertEquals("Nur 48 Stunden", eps.getTitle()); + assertEquals("1", eps.getEpisode().toString()); + assertEquals("1", eps.getSeason().toString()); + assertEquals("1", eps.getAbsolute().toString()); + + // check umlaut in title + eps = list.get(2); + assertEquals("Überleben ist alles", eps.getTitle()); + assertEquals("1", eps.getSeason().toString()); + assertEquals("3", eps.getEpisode().toString()); + assertEquals("3", eps.getAbsolute().toString()); + } + + + @BeforeClass + @AfterClass + public static void clearCache() { + CacheManager.getInstance().clearAll(); + } + +} diff --git a/test/net/sourceforge/filebot/web/WebTestSuite.java b/test/net/sourceforge/filebot/web/WebTestSuite.java index 5f90d2cc..fadb03b8 100644 --- a/test/net/sourceforge/filebot/web/WebTestSuite.java +++ b/test/net/sourceforge/filebot/web/WebTestSuite.java @@ -8,8 +8,8 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, TMDbClientTest.class, IMDbClientTest.class, SubsceneSubtitleClientTest.class, SublightSubtitleClientTest.class, - OpenSubtitlesXmlRpcTest.class }) +@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, SerienjunkiesClientTest.class, TMDbClientTest.class, IMDbClientTest.class, SubsceneSubtitleClientTest.class, + SublightSubtitleClientTest.class, OpenSubtitlesXmlRpcTest.class }) public class WebTestSuite { }