2007-12-23 14:28:04 -05:00
|
|
|
|
|
|
|
package net.sourceforge.filebot.web;
|
|
|
|
|
|
|
|
|
2011-10-28 04:07:02 -04:00
|
|
|
import static net.sourceforge.filebot.web.EpisodeUtilities.*;
|
2009-05-17 13:22:44 -04:00
|
|
|
import static net.sourceforge.filebot.web.WebRequest.*;
|
|
|
|
import static net.sourceforge.tuned.XPathUtilities.*;
|
2009-01-04 13:28:28 -05:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.io.Serializable;
|
2008-06-21 15:24:18 -04:00
|
|
|
import java.net.URI;
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.net.URISyntaxException;
|
2007-12-23 14:28:04 -05:00
|
|
|
import java.net.URL;
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.util.AbstractList;
|
2007-12-23 14:28:04 -05:00
|
|
|
import java.util.ArrayList;
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.util.Arrays;
|
2010-11-11 09:23:59 -05:00
|
|
|
import java.util.Collection;
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.Comparator;
|
|
|
|
import java.util.HashMap;
|
2007-12-23 14:28:04 -05:00
|
|
|
import java.util.List;
|
2010-11-11 09:23:59 -05:00
|
|
|
import java.util.Locale;
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.util.Map;
|
|
|
|
import java.util.Scanner;
|
|
|
|
import java.util.TreeMap;
|
|
|
|
import java.util.AbstractMap.SimpleEntry;
|
|
|
|
import java.util.Map.Entry;
|
2009-07-13 08:40:27 -04:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
2009-10-28 11:09:47 -04:00
|
|
|
import java.util.zip.GZIPInputStream;
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2008-07-13 13:59:05 -04:00
|
|
|
import javax.swing.Icon;
|
|
|
|
|
2007-12-23 14:28:04 -05:00
|
|
|
import org.w3c.dom.Document;
|
|
|
|
import org.w3c.dom.Node;
|
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
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;
|
2009-06-27 08:03:48 -04:00
|
|
|
import net.sourceforge.filebot.ResourceManager;
|
|
|
|
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2011-08-08 13:37:45 -04:00
|
|
|
public class AnidbClient extends AbstractEpisodeListProvider {
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2008-07-30 18:37:01 -04:00
|
|
|
private static final String host = "anidb.net";
|
2011-08-05 00:38:11 -04:00
|
|
|
private static final AnidbCache cache = new AnidbCache(CacheManager.getInstance().getCache("web-persistent-datasource"));
|
2009-10-28 11:09:47 -04:00
|
|
|
|
2011-01-06 22:51:08 -05:00
|
|
|
private final String client;
|
|
|
|
private final int clientver;
|
|
|
|
|
|
|
|
|
|
|
|
public AnidbClient(String client, int clientver) {
|
|
|
|
this.client = client;
|
|
|
|
this.clientver = clientver;
|
|
|
|
}
|
|
|
|
|
2009-06-27 08:03:48 -04:00
|
|
|
|
2008-07-13 13:59:05 -04:00
|
|
|
@Override
|
|
|
|
public String getName() {
|
|
|
|
return "AniDB";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Icon getIcon() {
|
|
|
|
return ResourceManager.getIcon("search.anidb");
|
|
|
|
}
|
2007-12-23 14:28:04 -05:00
|
|
|
|
|
|
|
|
|
|
|
@Override
|
2011-08-08 13:37:45 -04:00
|
|
|
public boolean hasSingleSeasonSupport() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean hasLocaleSupport() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public List<SearchResult> search(String query, Locale locale) throws Exception {
|
2009-10-28 11:09:47 -04:00
|
|
|
// normalize
|
|
|
|
query = query.toLowerCase();
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
AbstractStringMetric metric = new QGramsDistance();
|
2008-03-29 08:20:01 -04:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
final List<Entry<SearchResult, Float>> resultSet = new ArrayList<Entry<SearchResult, Float>>();
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
for (AnidbSearchResult anime : getAnimeTitles()) {
|
|
|
|
for (String name : new String[] { anime.getMainTitle(), anime.getEnglishTitle() }) {
|
|
|
|
if (name != null) {
|
2009-11-02 18:25:04 -05:00
|
|
|
// normalize
|
|
|
|
name = name.toLowerCase();
|
|
|
|
float similarity = metric.getSimilarity(name, query);
|
|
|
|
|
|
|
|
if (similarity > 0.5 || name.contains(query)) {
|
2009-10-28 11:09:47 -04:00
|
|
|
resultSet.add(new SimpleEntry<SearchResult, Float>(anime, similarity));
|
|
|
|
|
|
|
|
// add only once
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2008-07-07 19:38:17 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
// sort by similarity descending (best matches first)
|
|
|
|
Collections.sort(resultSet, new Comparator<Entry<SearchResult, Float>>() {
|
2009-05-25 16:13:30 -04:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
@Override
|
|
|
|
public int compare(Entry<SearchResult, Float> o1, Entry<SearchResult, Float> o2) {
|
|
|
|
return o2.getValue().compareTo(o1.getValue());
|
2007-12-23 14:28:04 -05:00
|
|
|
}
|
2009-10-28 11:09:47 -04:00
|
|
|
});
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
// view for the first 20 search results
|
|
|
|
return new AbstractList<SearchResult>() {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public SearchResult get(int index) {
|
|
|
|
return resultSet.get(index).getKey();
|
|
|
|
}
|
|
|
|
|
2009-05-25 16:13:30 -04:00
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
@Override
|
|
|
|
public int size() {
|
|
|
|
return Math.min(20, resultSet.size());
|
|
|
|
}
|
|
|
|
};
|
2009-05-25 16:13:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2009-07-13 08:40:27 -04:00
|
|
|
@Override
|
2011-08-08 13:37:45 -04:00
|
|
|
public List<Episode> getEpisodeList(SearchResult searchResult, Locale language) throws Exception {
|
|
|
|
AnidbSearchResult anime = (AnidbSearchResult) searchResult;
|
|
|
|
|
2011-01-06 22:51:08 -05:00
|
|
|
// e.g. http://api.anidb.net:9001/httpapi?request=anime&client=filebot&clientver=1&protover=1&aid=4521
|
|
|
|
URL url = new URL("http", "api." + host, 9001, "/httpapi?request=anime&client=" + client + "&clientver=" + clientver + "&protover=1&aid=" + anime.getAnimeId());
|
2009-10-28 11:09:47 -04:00
|
|
|
|
|
|
|
// try cache first
|
2010-11-11 09:23:59 -05:00
|
|
|
List<Episode> episodes = cache.getEpisodeList(anime.getAnimeId(), language.getLanguage());
|
|
|
|
if (episodes != null)
|
|
|
|
return episodes;
|
2009-07-13 08:40:27 -04:00
|
|
|
|
|
|
|
// get anime page as xml
|
2011-01-06 22:51:08 -05:00
|
|
|
Document dom = getDocument(url);
|
2009-07-13 08:40:27 -04:00
|
|
|
|
2011-10-01 00:08:46 -04:00
|
|
|
// select main title and anime start date
|
|
|
|
Date seriesStartDate = Date.parse(selectString("//startdate", dom), "yyyy-MM-dd");
|
2011-08-08 13:37:45 -04:00
|
|
|
String animeTitle = selectString("//titles/title[@type='official' and @lang='" + language.getLanguage() + "']", dom);
|
|
|
|
if (animeTitle.isEmpty()) {
|
|
|
|
animeTitle = selectString("//titles/title[@type='main']", dom);
|
|
|
|
}
|
2009-07-13 08:40:27 -04:00
|
|
|
|
2010-11-11 09:23:59 -05:00
|
|
|
episodes = new ArrayList<Episode>(25);
|
2009-07-13 08:40:27 -04:00
|
|
|
|
2011-01-06 22:51:08 -05:00
|
|
|
for (Node node : selectNodes("//episode", dom)) {
|
2010-10-24 08:10:30 -04:00
|
|
|
Integer number = getIntegerContent("epno", node);
|
2009-07-13 08:40:27 -04:00
|
|
|
|
2009-11-21 14:21:46 -05:00
|
|
|
// ignore special episodes
|
2010-10-24 08:10:30 -04:00
|
|
|
if (number != null) {
|
2011-10-01 00:08:46 -04:00
|
|
|
Date airdate = Date.parse(getTextContent("airdate", node), "yyyy-MM-dd");
|
2010-11-11 09:23:59 -05:00
|
|
|
String title = selectString(".//title[@lang='" + language.getLanguage() + "']", node);
|
2011-08-08 13:37:45 -04:00
|
|
|
if (title.isEmpty()) { // English language fall-back
|
|
|
|
title = selectString(".//title[@lang='en']", node);
|
|
|
|
}
|
|
|
|
|
2009-07-13 08:40:27 -04:00
|
|
|
// no seasons for anime
|
2011-10-01 00:08:46 -04:00
|
|
|
episodes.add(new Episode(animeTitle, seriesStartDate, null, number, title, number, null, airdate));
|
2009-07-13 08:40:27 -04:00
|
|
|
}
|
|
|
|
}
|
2009-05-25 16:13:30 -04:00
|
|
|
|
2011-01-06 22:51:08 -05:00
|
|
|
// make sure episodes are in ordered correctly
|
|
|
|
sortEpisodes(episodes);
|
|
|
|
|
2009-07-13 08:40:27 -04:00
|
|
|
// sanity check
|
2009-10-28 11:09:47 -04:00
|
|
|
if (episodes.size() > 0) {
|
|
|
|
// populate cache
|
2010-11-11 09:23:59 -05:00
|
|
|
cache.putEpisodeList(episodes, anime.getAnimeId(), language.getLanguage());
|
2009-10-28 11:09:47 -04:00
|
|
|
} else {
|
2009-07-13 08:40:27 -04:00
|
|
|
// anime page xml doesn't work sometimes
|
2010-11-11 09:23:59 -05:00
|
|
|
throw new RuntimeException(String.format("Failed to parse episode data from xml: %s (%d)", anime, anime.getAnimeId()));
|
2009-07-13 08:40:27 -04:00
|
|
|
}
|
2009-05-25 16:13:30 -04:00
|
|
|
|
2009-07-13 08:40:27 -04:00
|
|
|
return episodes;
|
2007-12-23 14:28:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-07-05 23:17:23 -04:00
|
|
|
@Override
|
|
|
|
public URI getEpisodeListLink(SearchResult searchResult) {
|
2009-10-28 11:09:47 -04:00
|
|
|
int aid = ((AnidbSearchResult) searchResult).getAnimeId();
|
|
|
|
|
|
|
|
try {
|
|
|
|
return new URI("http", host, "/a" + aid, null);
|
|
|
|
} catch (URISyntaxException e) {
|
|
|
|
throw new RuntimeException(e);
|
|
|
|
}
|
2008-07-05 23:17:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
2011-08-08 13:37:45 -04:00
|
|
|
public List<Episode> getEpisodeList(SearchResult searchResult, int season, Locale locale) throws Exception {
|
2008-07-05 23:17:23 -04:00
|
|
|
throw new UnsupportedOperationException();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-03-29 08:20:01 -04:00
|
|
|
@Override
|
2008-06-21 15:24:18 -04:00
|
|
|
public URI getEpisodeListLink(SearchResult searchResult, int season) {
|
2009-03-17 17:59:19 -04:00
|
|
|
return null;
|
2007-12-23 14:28:04 -05:00
|
|
|
}
|
|
|
|
|
2009-10-28 11:09:47 -04:00
|
|
|
|
2011-08-05 00:38:11 -04:00
|
|
|
protected List<AnidbSearchResult> getAnimeTitles() throws Exception {
|
2009-10-28 11:09:47 -04:00
|
|
|
URL url = new URL("http", host, "/api/animetitles.dat.gz");
|
|
|
|
|
|
|
|
// try cache first
|
2010-11-11 09:23:59 -05:00
|
|
|
List<AnidbSearchResult> anime = cache.getAnimeList();
|
|
|
|
if (anime != null)
|
|
|
|
return anime;
|
2009-10-28 11:09:47 -04:00
|
|
|
|
|
|
|
// <aid>|<type>|<language>|<title>
|
|
|
|
// type: 1=primary title (one per anime), 2=synonyms (multiple per anime), 3=shorttitles (multiple per anime), 4=official title (one per language)
|
|
|
|
Pattern pattern = Pattern.compile("^(?!#)(\\d+)[|](\\d)[|]([\\w-]+)[|](.+)$");
|
|
|
|
|
|
|
|
Map<Integer, String> primaryTitleMap = new TreeMap<Integer, String>();
|
|
|
|
Map<Integer, String> englishTitleMap = new HashMap<Integer, String>();
|
|
|
|
|
|
|
|
// fetch data
|
|
|
|
Scanner scanner = new Scanner(new GZIPInputStream(url.openStream()), "UTF-8");
|
|
|
|
|
|
|
|
try {
|
|
|
|
while (scanner.hasNextLine()) {
|
|
|
|
Matcher matcher = pattern.matcher(scanner.nextLine());
|
|
|
|
|
|
|
|
if (matcher.matches()) {
|
|
|
|
if (matcher.group(2).equals("1")) {
|
|
|
|
primaryTitleMap.put(Integer.parseInt(matcher.group(1)), matcher.group(4));
|
|
|
|
} else if (matcher.group(2).equals("4") && matcher.group(3).equals("en")) {
|
|
|
|
englishTitleMap.put(Integer.parseInt(matcher.group(1)), matcher.group(4));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
scanner.close();
|
|
|
|
}
|
|
|
|
|
2010-11-11 09:23:59 -05:00
|
|
|
// build up a list of all possible anidb search results
|
|
|
|
anime = new ArrayList<AnidbSearchResult>(primaryTitleMap.size());
|
2009-10-28 11:09:47 -04:00
|
|
|
|
|
|
|
for (Entry<Integer, String> entry : primaryTitleMap.entrySet()) {
|
|
|
|
anime.add(new AnidbSearchResult(entry.getKey(), entry.getValue(), englishTitleMap.get(entry.getKey())));
|
|
|
|
}
|
|
|
|
|
|
|
|
// populate cache
|
2010-11-11 09:23:59 -05:00
|
|
|
cache.putAnimeList(anime);
|
2009-10-28 11:09:47 -04:00
|
|
|
|
2010-11-11 09:23:59 -05:00
|
|
|
return anime;
|
2009-10-28 11:09:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static class AnidbSearchResult extends SearchResult implements Serializable {
|
|
|
|
|
|
|
|
protected int aid;
|
|
|
|
protected String mainTitle;
|
|
|
|
protected String englishTitle;
|
|
|
|
|
|
|
|
|
|
|
|
protected AnidbSearchResult() {
|
|
|
|
// used by serializer
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public AnidbSearchResult(int aid, String mainTitle, String englishTitle) {
|
|
|
|
this.aid = aid;
|
|
|
|
this.mainTitle = mainTitle;
|
|
|
|
this.englishTitle = englishTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int getAnimeId() {
|
|
|
|
return aid;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getName() {
|
|
|
|
return mainTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public String getMainTitle() {
|
|
|
|
return mainTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public String getEnglishTitle() {
|
|
|
|
return englishTitle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-11 09:23:59 -05:00
|
|
|
|
|
|
|
private static class AnidbCache {
|
|
|
|
|
|
|
|
private final Cache cache;
|
|
|
|
|
|
|
|
|
|
|
|
public AnidbCache(Cache cache) {
|
|
|
|
this.cache = cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void putAnimeList(Collection<AnidbSearchResult> anime) {
|
|
|
|
cache.put(new Element(host + "AnimeList", anime.toArray(new AnidbSearchResult[0])));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public List<AnidbSearchResult> getAnimeList() {
|
|
|
|
Element element = cache.get(host + "AnimeList");
|
|
|
|
|
|
|
|
if (element != null)
|
|
|
|
return Arrays.asList((AnidbSearchResult[]) element.getValue());
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void putEpisodeList(Collection<Episode> episodes, int aid, String lang) {
|
|
|
|
cache.put(new Element(host + "EpisodeList" + aid + lang, episodes.toArray(new Episode[0])));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public List<Episode> getEpisodeList(int aid, String lang) {
|
|
|
|
Element element = cache.get(host + "EpisodeList" + aid + lang);
|
|
|
|
|
|
|
|
if (element != null)
|
|
|
|
return Arrays.asList((Episode[]) element.getValue());
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2007-12-23 14:28:04 -05:00
|
|
|
}
|