614 lines
15 KiB
Java
614 lines
15 KiB
Java
|
|
package net.sourceforge.filebot.web;
|
|
|
|
|
|
import static java.util.Arrays.*;
|
|
import static java.util.Collections.*;
|
|
import static net.sourceforge.filebot.web.WebRequest.*;
|
|
|
|
import java.io.File;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.Serializable;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.Charset;
|
|
import java.util.AbstractList;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.EnumMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Scanner;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import javax.swing.Icon;
|
|
|
|
import net.sf.ehcache.Cache;
|
|
import net.sf.ehcache.CacheManager;
|
|
import net.sourceforge.filebot.ResourceManager;
|
|
import net.sourceforge.filebot.web.TMDbClient.MovieInfo.MovieProperty;
|
|
import net.sourceforge.filebot.web.TMDbClient.Person.PersonProperty;
|
|
|
|
import org.json.simple.JSONArray;
|
|
import org.json.simple.JSONObject;
|
|
import org.json.simple.JSONValue;
|
|
|
|
|
|
public class TMDbClient implements MovieIdentificationService {
|
|
|
|
private static final String host = "api.themoviedb.org";
|
|
private static final String version = "3";
|
|
|
|
private static final FloodLimit SEARCH_LIMIT = new FloodLimit(10, 12, TimeUnit.SECONDS);
|
|
private static final FloodLimit REQUEST_LIMIT = new FloodLimit(20, 12, TimeUnit.SECONDS);
|
|
|
|
private final String apikey;
|
|
|
|
|
|
public TMDbClient(String apikey) {
|
|
this.apikey = apikey;
|
|
}
|
|
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "TheMovieDB";
|
|
}
|
|
|
|
|
|
@Override
|
|
public Icon getIcon() {
|
|
return ResourceManager.getIcon("search.themoviedb");
|
|
}
|
|
|
|
|
|
@Override
|
|
public List<Movie> searchMovie(String query, Locale locale) throws IOException {
|
|
JSONObject response = request("search/movie", singletonMap("query", query), locale, SEARCH_LIMIT);
|
|
List<Movie> result = new ArrayList<Movie>();
|
|
|
|
for (JSONObject it : jsonList(response.get("results"))) {
|
|
// e.g. {"id":16320,"title":"冲出宁静号","release_date":"2005-09-30","original_title":"Serenity"}
|
|
String title = (String) it.get("title");
|
|
if (title == null || title.isEmpty()) {
|
|
title = (String) it.get("original_title");
|
|
}
|
|
|
|
try {
|
|
long id = (Long) it.get("id");
|
|
int year = -1;
|
|
try {
|
|
String release = (String) it.get("release_date");
|
|
year = new Scanner(release).useDelimiter("\\D+").nextInt();
|
|
} catch (Exception e) {
|
|
throw new IllegalArgumentException("Missing data: year");
|
|
}
|
|
result.add(new Movie(title, year, -1, (int) id));
|
|
} catch (Exception e) {
|
|
Logger.getLogger(TMDbClient.class.getName()).log(Level.FINE, String.format("Ignore movie [%s]: %s", title, e.getMessage()));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
public URI getMoviePageLink(int tmdbid) {
|
|
return URI.create("http://www.themoviedb.org/movie/" + tmdbid);
|
|
}
|
|
|
|
|
|
@Override
|
|
public Movie getMovieDescriptor(int imdbid, Locale locale) throws IOException {
|
|
String id = String.format("tt%07d", imdbid);
|
|
try {
|
|
MovieInfo info = getMovieInfo(id, locale, false);
|
|
return new Movie(info.getName(), info.getReleased().getYear(), info.getImdbId(), info.getId());
|
|
} catch (FileNotFoundException e) {
|
|
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Movie not found: " + id);
|
|
return null;
|
|
} catch (NullPointerException e) {
|
|
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Movie data missing: " + id);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
public Map<File, Movie> getMovieDescriptors(Collection<File> movieFiles, Locale locale) throws Exception {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
|
|
public MovieInfo getMovieInfo(Movie movie, Locale locale) throws IOException {
|
|
if (movie.getTmdbId() >= 0) {
|
|
return getMovieInfo(String.valueOf(movie.getTmdbId()), locale, true);
|
|
} else if (movie.getImdbId() >= 0) {
|
|
return getMovieInfo(String.format("tt%07d", movie.getImdbId()), locale, true);
|
|
} else {
|
|
for (Movie result : searchMovie(movie.getName(), locale)) {
|
|
if (movie.getName().equalsIgnoreCase(result.getName()) && movie.getYear() == result.getYear()) {
|
|
return getMovieInfo(String.valueOf(result.getTmdbId()), locale, true);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
public MovieInfo getMovieInfo(String id, Locale locale, boolean extendedInfo) throws IOException {
|
|
JSONObject response = request("movie/" + id, null, locale, REQUEST_LIMIT);
|
|
|
|
Map<MovieProperty, String> fields = new EnumMap<MovieProperty, String>(MovieProperty.class);
|
|
for (MovieProperty key : MovieProperty.values()) {
|
|
Object value = response.get(key.name());
|
|
if (value != null) {
|
|
fields.put(key, value.toString());
|
|
}
|
|
}
|
|
|
|
try {
|
|
JSONObject collection = (JSONObject) response.get("belongs_to_collection");
|
|
fields.put(MovieProperty.collection, (String) collection.get("name"));
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
|
|
List<String> genres = new ArrayList<String>();
|
|
for (JSONObject it : jsonList(response.get("genres"))) {
|
|
genres.add((String) it.get("name"));
|
|
}
|
|
|
|
List<String> spokenLanguages = new ArrayList<String>();
|
|
for (JSONObject it : jsonList(response.get("spoken_languages"))) {
|
|
spokenLanguages.add((String) it.get("iso_639_1"));
|
|
}
|
|
|
|
if (extendedInfo) {
|
|
JSONObject releases = request("movie/" + fields.get(MovieProperty.id) + "/releases", null, null, REQUEST_LIMIT);
|
|
for (JSONObject it : jsonList(releases.get("countries"))) {
|
|
if ("US".equals(it.get("iso_3166_1"))) {
|
|
fields.put(MovieProperty.certification, (String) it.get("certification"));
|
|
}
|
|
}
|
|
}
|
|
|
|
List<Person> cast = new ArrayList<Person>();
|
|
if (extendedInfo) {
|
|
JSONObject castResponse = request("movie/" + fields.get(MovieProperty.id) + "/casts", null, null, REQUEST_LIMIT);
|
|
for (String section : new String[] { "cast", "crew" }) {
|
|
for (JSONObject it : jsonList(castResponse.get(section))) {
|
|
Map<PersonProperty, String> person = new EnumMap<PersonProperty, String>(PersonProperty.class);
|
|
for (PersonProperty key : PersonProperty.values()) {
|
|
Object value = it.get(key.name());
|
|
if (value != null) {
|
|
person.put(key, value.toString());
|
|
}
|
|
}
|
|
cast.add(new Person(person));
|
|
}
|
|
}
|
|
}
|
|
|
|
return new MovieInfo(fields, genres, spokenLanguages, cast);
|
|
}
|
|
|
|
|
|
public List<Artwork> getArtwork(String id) throws IOException {
|
|
// http://api.themoviedb.org/3/movie/11/images
|
|
JSONObject config = request("configuration", null, null, REQUEST_LIMIT);
|
|
String baseUrl = (String) ((JSONObject) config.get("images")).get("base_url");
|
|
|
|
JSONObject images = request("movie/" + id + "/images", null, null, REQUEST_LIMIT);
|
|
List<Artwork> artwork = new ArrayList<Artwork>();
|
|
|
|
for (String section : new String[] { "backdrops", "posters" }) {
|
|
for (JSONObject it : jsonList(images.get(section))) {
|
|
try {
|
|
String url = baseUrl + "original" + (String) it.get("file_path");
|
|
long width = (Long) it.get("width");
|
|
long height = (Long) it.get("height");
|
|
String lang = (String) it.get("iso_639_1");
|
|
artwork.add(new Artwork(section, new URL(url), (int) width, (int) height, lang));
|
|
} catch (Exception e) {
|
|
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Invalid artwork: " + it, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return artwork;
|
|
}
|
|
|
|
|
|
public JSONObject request(String resource, Map<String, String> parameters, Locale locale, final FloodLimit limit) throws IOException {
|
|
// default parameters
|
|
LinkedHashMap<String, String> data = new LinkedHashMap<String, String>();
|
|
if (parameters != null) {
|
|
data.putAll(parameters);
|
|
}
|
|
if (locale != null && !locale.getLanguage().isEmpty()) {
|
|
data.put("language", locale.getLanguage());
|
|
}
|
|
data.put("api_key", apikey);
|
|
|
|
URL url = new URL("http", host, "/" + version + "/" + resource + "?" + encodeParameters(data));
|
|
|
|
CachedResource<String> json = new CachedResource<String>(url.toString(), String.class, 7 * 24 * 60 * 60 * 1000) {
|
|
|
|
@Override
|
|
public String process(ByteBuffer data) throws Exception {
|
|
return Charset.forName("UTF-8").decode(data).toString();
|
|
}
|
|
|
|
|
|
@Override
|
|
protected ByteBuffer fetchData(URL url, long lastModified) throws IOException {
|
|
try {
|
|
if (limit != null) {
|
|
limit.acquirePermit();
|
|
}
|
|
return super.fetchData(url, lastModified);
|
|
} catch (FileNotFoundException e) {
|
|
return ByteBuffer.allocate(0);
|
|
} catch (InterruptedException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
protected Cache getCache() {
|
|
return CacheManager.getInstance().getCache("web-data-diskcache");
|
|
}
|
|
};
|
|
|
|
JSONObject object = (JSONObject) JSONValue.parse(json.get());
|
|
if (object == null || object.isEmpty()) {
|
|
throw new FileNotFoundException("Resource not found: " + url);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
|
|
protected List<JSONObject> jsonList(final Object array) {
|
|
return new AbstractList<JSONObject>() {
|
|
|
|
@Override
|
|
public JSONObject get(int index) {
|
|
return (JSONObject) ((JSONArray) array).get(index);
|
|
}
|
|
|
|
|
|
@Override
|
|
public int size() {
|
|
return ((JSONArray) array).size();
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
public static class MovieInfo implements Serializable {
|
|
|
|
public static enum MovieProperty {
|
|
adult, backdrop_path, budget, homepage, id, imdb_id, original_title, overview, popularity, poster_path, release_date, revenue, runtime, tagline, title, vote_average, vote_count, certification, collection
|
|
}
|
|
|
|
protected Map<MovieProperty, String> fields;
|
|
|
|
protected String[] genres;
|
|
protected String[] spokenLanguages;
|
|
|
|
protected Person[] people;
|
|
|
|
|
|
protected MovieInfo() {
|
|
// used by serializer
|
|
}
|
|
|
|
|
|
protected MovieInfo(Map<MovieProperty, String> fields, List<String> genres, List<String> spokenLanguages, List<Person> people) {
|
|
this.fields = new EnumMap<MovieProperty, String>(fields);
|
|
this.genres = genres.toArray(new String[0]);
|
|
this.spokenLanguages = spokenLanguages.toArray(new String[0]);
|
|
this.people = people.toArray(new Person[0]);
|
|
}
|
|
|
|
|
|
public String get(Object key) {
|
|
return fields.get(MovieProperty.valueOf(key.toString()));
|
|
}
|
|
|
|
|
|
public String get(MovieProperty key) {
|
|
return fields.get(key);
|
|
}
|
|
|
|
|
|
public boolean isAdult() {
|
|
return Boolean.valueOf(get(MovieProperty.adult));
|
|
}
|
|
|
|
|
|
public List<Locale> getSpokenLanguages() {
|
|
try {
|
|
List<Locale> locales = new ArrayList<Locale>();
|
|
for (String it : spokenLanguages) {
|
|
locales.add(new Locale(it));
|
|
}
|
|
return locales;
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public String getOriginalName() {
|
|
return get(MovieProperty.original_title);
|
|
}
|
|
|
|
|
|
public String getName() {
|
|
return get(MovieProperty.title);
|
|
}
|
|
|
|
|
|
public Integer getId() {
|
|
try {
|
|
return new Integer(get(MovieProperty.id));
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public Integer getImdbId() {
|
|
// e.g. tt0379786
|
|
try {
|
|
return new Integer(get(MovieProperty.imdb_id).substring(2));
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public URL getHomepage() {
|
|
try {
|
|
return new URL(get(MovieProperty.homepage));
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public String getOverview() {
|
|
return get(MovieProperty.overview);
|
|
}
|
|
|
|
|
|
public Integer getVotes() {
|
|
try {
|
|
return new Integer(get(MovieProperty.vote_count));
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public Double getRating() {
|
|
try {
|
|
return new Double(get(MovieProperty.vote_average));
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public String getTagline() {
|
|
return get(MovieProperty.tagline);
|
|
}
|
|
|
|
|
|
public String getCertification() {
|
|
// e.g. PG-13
|
|
return get(MovieProperty.certification);
|
|
}
|
|
|
|
|
|
public String getCollection() {
|
|
// e.g. Star Wars Collection
|
|
return get(MovieProperty.collection);
|
|
}
|
|
|
|
|
|
public Date getReleased() {
|
|
// e.g. 2005-09-30
|
|
try {
|
|
return Date.parse(get(MovieProperty.release_date), "yyyy-MM-dd");
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public Integer getRuntime() {
|
|
try {
|
|
return new Integer(get(MovieProperty.runtime));
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
public List<String> getGenres() {
|
|
return unmodifiableList(asList(genres));
|
|
}
|
|
|
|
|
|
public List<Person> getPeope() {
|
|
return unmodifiableList(asList(people));
|
|
}
|
|
|
|
|
|
public List<Person> getCast() {
|
|
List<Person> actors = new ArrayList<Person>();
|
|
for (Person person : people) {
|
|
if (person.isActor()) {
|
|
actors.add(person);
|
|
}
|
|
}
|
|
return actors;
|
|
}
|
|
|
|
|
|
public String getDirector() {
|
|
for (Person person : people) {
|
|
if (person.isDirector())
|
|
return person.getName();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
public List<String> getActors() {
|
|
List<String> actors = new ArrayList<String>();
|
|
for (Person actor : getCast()) {
|
|
actors.add(actor.getName());
|
|
}
|
|
return actors;
|
|
}
|
|
|
|
|
|
@Override
|
|
public String toString() {
|
|
return fields.toString();
|
|
}
|
|
}
|
|
|
|
|
|
public static class Person implements Serializable {
|
|
|
|
public static enum PersonProperty {
|
|
name, character, job
|
|
}
|
|
|
|
protected Map<PersonProperty, String> fields;
|
|
|
|
|
|
protected Person() {
|
|
// used by serializer
|
|
}
|
|
|
|
|
|
public Person(Map<PersonProperty, String> fields) {
|
|
this.fields = new EnumMap<PersonProperty, String>(fields);
|
|
}
|
|
|
|
|
|
public Person(String name, String character, String job) {
|
|
fields = new EnumMap<PersonProperty, String>(PersonProperty.class);
|
|
fields.put(PersonProperty.name, name);
|
|
fields.put(PersonProperty.character, character);
|
|
fields.put(PersonProperty.job, job);
|
|
}
|
|
|
|
|
|
public String get(Object key) {
|
|
return fields.get(PersonProperty.valueOf(key.toString()));
|
|
}
|
|
|
|
|
|
public String get(PersonProperty key) {
|
|
return fields.get(key);
|
|
}
|
|
|
|
|
|
public String getName() {
|
|
return get(PersonProperty.name);
|
|
}
|
|
|
|
|
|
public String getCharacter() {
|
|
return get(PersonProperty.character);
|
|
}
|
|
|
|
|
|
public String getJob() {
|
|
return get(PersonProperty.job);
|
|
}
|
|
|
|
|
|
public boolean isActor() {
|
|
return getJob() == null;
|
|
}
|
|
|
|
|
|
public boolean isDirector() {
|
|
return "Director".equals(getJob());
|
|
}
|
|
|
|
|
|
@Override
|
|
public String toString() {
|
|
return fields.toString();
|
|
}
|
|
}
|
|
|
|
|
|
public static class Artwork {
|
|
|
|
private String category;
|
|
private String language;
|
|
|
|
private int width;
|
|
private int height;
|
|
|
|
private URL url;
|
|
|
|
|
|
public Artwork(String category, URL url, int width, int height, String language) {
|
|
this.category = category;
|
|
this.url = url;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.language = language;
|
|
}
|
|
|
|
|
|
public String getCategory() {
|
|
return category;
|
|
}
|
|
|
|
|
|
public String getLanguage() {
|
|
return language;
|
|
}
|
|
|
|
|
|
public int getWidth() {
|
|
return width;
|
|
}
|
|
|
|
|
|
public int getHeight() {
|
|
return height;
|
|
}
|
|
|
|
|
|
public URL getUrl() {
|
|
return url;
|
|
}
|
|
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("{category: %s, width: %s, height: %s, language: %s, url: %s}", category, width, height, language, url);
|
|
}
|
|
}
|
|
|
|
}
|