From ac069f5a1cd0079783d0117d42e2f132d7f9a900 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Sat, 26 Mar 2016 17:40:59 +0000 Subject: [PATCH] Support TheMovieDB in Episode mode --- source/net/filebot/WebServices.java | 8 +- source/net/filebot/web/AnidbClient.java | 2 +- source/net/filebot/web/SeriesInfo.java | 7 +- source/net/filebot/web/TMDbClient.java | 167 ++++++++++++++++++---- source/net/filebot/web/TheTVDBClient.java | 3 +- test/net/filebot/web/TMDbClientTest.java | 60 +++++++- 6 files changed, 211 insertions(+), 36 deletions(-) diff --git a/source/net/filebot/WebServices.java b/source/net/filebot/WebServices.java index 85b0b737..1fd64b87 100644 --- a/source/net/filebot/WebServices.java +++ b/source/net/filebot/WebServices.java @@ -66,7 +66,7 @@ public final class WebServices { public static final XattrMetaInfoProvider XattrMetaData = new XattrMetaInfoProvider(); public static EpisodeListProvider[] getEpisodeListProviders() { - return new EpisodeListProvider[] { TheTVDB, AniDB, TVmaze }; + return new EpisodeListProvider[] { TheTVDB, TheMovieDB, AniDB, TVmaze }; } public static MovieIdentificationService[] getMovieIdentificationServices() { @@ -89,6 +89,12 @@ public final class WebServices { } public static EpisodeListProvider getEpisodeListProvider(String name) { + // special handling for TheMovieDB which is the only datasource that supports both series and movie mode + if (name.equalsIgnoreCase(TheMovieDB.getName())) + return null; + if (name.equalsIgnoreCase(TheMovieDB.getName() + "::TV")) + return TheMovieDB; + return getService(name, getEpisodeListProviders()); } diff --git a/source/net/filebot/web/AnidbClient.java b/source/net/filebot/web/AnidbClient.java index 4e9db571..7094d815 100644 --- a/source/net/filebot/web/AnidbClient.java +++ b/source/net/filebot/web/AnidbClient.java @@ -161,7 +161,7 @@ public class AnidbClient extends AbstractEpisodeListProvider { } // make sure episodes are in ordered correctly - sort(episodes, episodeComparator()); + episodes.sort(episodeComparator()); // sanity check if (episodes.isEmpty()) { diff --git a/source/net/filebot/web/SeriesInfo.java b/source/net/filebot/web/SeriesInfo.java index b16b3e7d..40eb5c5f 100644 --- a/source/net/filebot/web/SeriesInfo.java +++ b/source/net/filebot/web/SeriesInfo.java @@ -1,6 +1,7 @@ package net.filebot.web; import static java.util.Arrays.*; +import static java.util.Collections.*; import java.io.Serializable; import java.util.List; @@ -97,7 +98,7 @@ public class SeriesInfo implements Serializable { } public List getAliasNames() { - return aliasNames == null ? asList() : asList(aliasNames.clone()); + return aliasNames == null ? emptyList() : asList(aliasNames.clone()); } public void setAliasNames(String... aliasNames) { @@ -105,7 +106,7 @@ public class SeriesInfo implements Serializable { } public List getActors() { - return actors == null ? asList() : asList(actors.clone()); + return actors == null ? emptyList() : asList(actors.clone()); } public void setActors(List actors) { @@ -129,7 +130,7 @@ public class SeriesInfo implements Serializable { } public List getGenres() { - return genres == null ? asList() : asList(genres.clone()); + return genres == null ? emptyList() : asList(genres.clone()); } public void setGenres(List genres) { diff --git a/source/net/filebot/web/TMDbClient.java b/source/net/filebot/web/TMDbClient.java index ddbe52da..4a598a0b 100644 --- a/source/net/filebot/web/TMDbClient.java +++ b/source/net/filebot/web/TMDbClient.java @@ -7,6 +7,7 @@ import static net.filebot.CachedResource.*; import static net.filebot.Logging.*; import static net.filebot.util.JsonUtilities.*; import static net.filebot.util.StringUtilities.*; +import static net.filebot.web.EpisodeUtilities.*; import static net.filebot.web.WebRequest.*; import java.io.File; @@ -40,7 +41,7 @@ import net.filebot.ResourceManager; import net.filebot.web.TMDbClient.MovieInfo.MovieProperty; import net.filebot.web.TMDbClient.Person.PersonProperty; -public class TMDbClient implements MovieIdentificationService { +public class TMDbClient extends AbstractEpisodeListProvider implements MovieIdentificationService { private static final String host = "api.themoviedb.org"; private static final String version = "3"; @@ -64,14 +65,18 @@ public class TMDbClient implements MovieIdentificationService { return ResourceManager.getIcon("search.themoviedb"); } + private Matcher getNameYearMatcher(String query) { + return Pattern.compile("(.+)\\b[(]?((?:19|20)\\d{2})[)]?$").matcher(query.trim()); + } + @Override public List searchMovie(String query, Locale locale) throws Exception { // query by name with year filter if possible - Matcher nameYear = Pattern.compile("(.+)\\b\\(?(19\\d{2}|20\\d{2})\\)?$").matcher(query.trim()); + Matcher nameYear = getNameYearMatcher(query); if (nameYear.matches()) { return searchMovie(nameYear.group(1).trim(), Integer.parseInt(nameYear.group(2)), locale, false); } else { - return searchMovie(query, -1, locale, false); + return searchMovie(query.trim(), -1, locale, false); } } @@ -81,13 +86,12 @@ public class TMDbClient implements MovieIdentificationService { return emptyList(); } - Map param = new LinkedHashMap(2); - param.put("query", movieName); + Map query = new LinkedHashMap(2); + query.put("query", movieName); if (movieYear > 0) { - param.put("year", movieYear); + query.put("year", movieYear); } - - Object response = request("search/movie", param, locale, SEARCH_LIMIT); + Object response = request("search/movie", query, locale, SEARCH_LIMIT); // e.g. {"id":16320,"title":"冲出宁静号","release_date":"2005-09-30","original_title":"Serenity"} return streamJsonObjects(response, "results").map(it -> { @@ -106,31 +110,37 @@ public class TMDbClient implements MovieIdentificationService { title = originalTitle; } - Set alternativeTitles = new LinkedHashSet(); - if (originalTitle != null) { - alternativeTitles.add(originalTitle); - } - - if (extendedInfo) { - try { - Object titles = request("movie/" + id + "/alternative_titles", emptyMap(), Locale.ENGLISH, REQUEST_LIMIT); - streamJsonObjects(titles, "titles").map(n -> { - return getString(n, "title"); - }).filter(t -> t != null && t.length() >= 3).forEach(alternativeTitles::add); - } catch (Exception e) { - debug.warning(format("Failed to fetch alternative titles for %s [%d] => %s", title, id, e)); - } - } - - // make sure main title is not in the set of alternative titles - alternativeTitles.remove(title); + Set alternativeTitles = getAlternativeTitles("movie/" + id, "titles", title, originalTitle, extendedInfo); return new Movie(title, alternativeTitles.toArray(new String[0]), year, -1, id, locale); }).filter(Objects::nonNull).collect(toList()); } + private Set getAlternativeTitles(String path, String key, String title, String originalTitle, boolean extendedInfo) { + Set alternativeTitles = new LinkedHashSet(); + if (originalTitle != null) { + alternativeTitles.add(originalTitle); + } + + if (extendedInfo) { + try { + Object response = request(path + "/alternative_titles", emptyMap(), Locale.ENGLISH, REQUEST_LIMIT); + streamJsonObjects(response, key).map(n -> { + return getString(n, "title"); + }).filter(Objects::nonNull).filter(n -> n.length() >= 2).forEach(alternativeTitles::add); + } catch (Exception e) { + debug.warning(format("Failed to fetch alternative titles for %s => %s", path, e)); + } + } + + // make sure main title is not in the set of alternative titles + alternativeTitles.remove(title); + + return alternativeTitles; + } + public URI getMoviePageLink(int tmdbid) { - return URI.create("http://www.themoviedb.org/movie/" + tmdbid); + return URI.create("https://www.themoviedb.org/movie/" + tmdbid); } @Override @@ -716,4 +726,107 @@ public class TMDbClient implements MovieIdentificationService { } + @Override + public boolean hasSeasonSupport() { + return true; + } + + @Override + protected SortOrder vetoRequestParameter(SortOrder order) { + return SortOrder.Airdate; + } + + @Override + public URI getEpisodeListLink(SearchResult searchResult) { + return URI.create("https://www.themoviedb.org/tv/" + searchResult.getId()); + } + + @Override + protected List fetchSearchResult(String query, Locale locale) throws Exception { + // query by name with year filter if possible + Matcher nameYear = getNameYearMatcher(query); + if (nameYear.matches()) { + return searchTV(nameYear.group(1).trim(), Integer.parseInt(nameYear.group(2)), locale, true); + } else { + return searchTV(query.trim(), -1, locale, true); + } + } + + public List searchTV(String seriesName, int startYear, Locale locale, boolean extendedInfo) throws Exception { + Map query = new LinkedHashMap(2); + query.put("query", seriesName); + if (startYear > 0) { + query.put("first_air_date_year", startYear); + } + Object response = request("search/tv", query, locale, SEARCH_LIMIT); + + return streamJsonObjects(response, "results").map(it -> { + Integer id = getInteger(it, "id"); + String name = getString(it, "name"); + String originalName = getString(it, "original_name"); + + if (name == null) { + name = originalName; + } + + if (id == null || name == null) { + return null; + } + + Set alternativeTitles = getAlternativeTitles("tv/" + id, "results", name, originalName, extendedInfo); + + return new SearchResult(id, name, alternativeTitles); + }).filter(Objects::nonNull).collect(toList()); + } + + @Override + protected SeriesData fetchSeriesData(SearchResult series, SortOrder sortOrder, Locale locale) throws Exception { + // http://api.themoviedb.org/3/tv/id + Object tv = request("tv/" + series.getId(), emptyMap(), locale, REQUEST_LIMIT); + + SeriesInfo info = new SeriesInfo(getName(), sortOrder, locale, series.getId()); + info.setName(Stream.of("original_name", "name").map(key -> getString(tv, key)).filter(Objects::nonNull).findFirst().orElse(series.getName())); + info.setAliasNames(series.getAliasNames()); + info.setStatus(getString(tv, "status")); + info.setLanguage(getString(tv, "original_language")); + info.setStartDate(getStringValue(tv, "first_air_date", SimpleDate::parse)); + info.setRating(getStringValue(tv, "vote_average", Double::new)); + info.setRatingCount(getStringValue(tv, "vote_count", Integer::new)); + info.setRuntime(stream(getArray(tv, "episode_run_time")).map(Object::toString).map(Integer::new).findFirst().orElse(null)); + info.setGenres(streamJsonObjects(tv, "genres").map(it -> getString(it, "name")).collect(toList())); + info.setNetwork(streamJsonObjects(tv, "networks").map(it -> getString(it, "name")).findFirst().orElse(null)); + + int[] seasons = streamJsonObjects(tv, "seasons").mapToInt(it -> getInteger(it, "season_number")).toArray(); + List episodes = new ArrayList(); + List specials = new ArrayList(); + + for (int s : seasons) { + // http://api.themoviedb.org/3/tv/id/season/season_number + Object season = request("tv/" + series.getId() + "/season/" + s, emptyMap(), locale, REQUEST_LIMIT); + + streamJsonObjects(season, "episodes").forEach(episode -> { + Integer episodeNumber = getInteger(episode, "episode_number"); + Integer seasonNumber = getInteger(episode, "season_number"); + String episodeTitle = getString(episode, "name"); + SimpleDate airdate = getStringValue(episode, "air_date", SimpleDate::parse); + + Integer absoluteNumber = episodes.size() + 1; + + if (s > 0) { + episodes.add(new Episode(series.getName(), seasonNumber, episodeNumber, episodeTitle, absoluteNumber, null, airdate, info)); + } else { + specials.add(new Episode(series.getName(), null, null, episodeTitle, null, episodeNumber, airdate, info)); + } + }); + } + + // episodes my not be ordered by DVD episode number + episodes.sort(episodeComparator()); + + // add specials at the end + episodes.addAll(specials); + + return new SeriesData(info, episodes); + } + } diff --git a/source/net/filebot/web/TheTVDBClient.java b/source/net/filebot/web/TheTVDBClient.java index 0242e6ea..853a3186 100644 --- a/source/net/filebot/web/TheTVDBClient.java +++ b/source/net/filebot/web/TheTVDBClient.java @@ -1,6 +1,5 @@ package net.filebot.web; -import static java.util.Collections.*; import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; import static net.filebot.util.StringUtilities.*; @@ -199,7 +198,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } // episodes my not be ordered by DVD episode number - sort(episodes, episodeComparator()); + episodes.sort(episodeComparator()); // add specials at the end episodes.addAll(specials); diff --git a/test/net/filebot/web/TMDbClientTest.java b/test/net/filebot/web/TMDbClientTest.java index 543719c1..942da53e 100644 --- a/test/net/filebot/web/TMDbClientTest.java +++ b/test/net/filebot/web/TMDbClientTest.java @@ -90,8 +90,64 @@ public class TMDbClientTest { @Test public void getArtwork() throws Exception { List artwork = tmdb.getArtwork("tt0418279"); - assertEquals("posters", artwork.get(0).getCategory()); - assertEquals("http://image.tmdb.org/t/p/original/bgSHbGEA1OM6qDs3Qba4VlSZsNG.jpg", artwork.get(0).getUrl().toString()); + assertEquals("backdrops", artwork.get(0).getCategory()); + assertEquals("https://image.tmdb.org/t/p/original/ac0HwGJIU3GxjjGujlIjLJmAGPR.jpg", artwork.get(0).getUrl().toString()); + } + + SearchResult buffy = new SearchResult(95, "Buffy the Vampire Slayer"); + SearchResult wonderfalls = new SearchResult(1982, "Wonderfalls"); + SearchResult firefly = new SearchResult(1437, "Firefly"); + + @Test + public void search() throws Exception { + // test default language and query escaping (blanks) + List results = tmdb.search("babylon 5", Locale.ENGLISH); + + assertEquals(1, results.size()); + + assertEquals("Babylon 5", results.get(0).getName()); + assertEquals(3137, results.get(0).getId()); + } + + @Test + public void getEpisodeListAll() throws Exception { + List list = tmdb.getEpisodeList(buffy, SortOrder.Airdate, Locale.ENGLISH); + + assertTrue(list.size() >= 144); + + // check ordinary episode + Episode first = list.get(0); + assertEquals("Buffy the Vampire Slayer", first.getSeriesName()); + assertEquals("1997-03-10", first.getSeriesInfo().getStartDate().toString()); + assertEquals("Welcome to the Hellmouth (1)", first.getTitle()); + assertEquals("1", first.getEpisode().toString()); + assertEquals("1", first.getSeason().toString()); + assertEquals("1", first.getAbsolute().toString()); + assertEquals("1997-03-10", first.getAirdate().toString()); + + // check special episode + Episode last = list.get(list.size() - 1); + assertEquals("Buffy the Vampire Slayer", last.getSeriesName()); + assertEquals("Unaired Buffy the Vampire Slayer pilot", last.getTitle()); + assertEquals(null, last.getSeason()); + assertEquals(null, last.getEpisode()); + assertEquals(null, last.getAbsolute()); + assertEquals("1", last.getSpecial().toString()); + assertEquals(null, last.getAirdate()); + } + + @Test + public void getEpisodeListSingleSeason() throws Exception { + List list = tmdb.getEpisodeList(wonderfalls, SortOrder.Airdate, Locale.ENGLISH); + + Episode first = list.get(0); + assertEquals("Wonderfalls", first.getSeriesName()); + assertEquals("2004-03-12", first.getSeriesInfo().getStartDate().toString()); + assertEquals("Wax Lion", first.getTitle()); + assertEquals("1", first.getEpisode().toString()); + assertEquals("1", first.getSeason().toString()); + assertEquals("1", first.getAbsolute().toString()); + assertEquals("2004-03-12", first.getAirdate().toString()); } @Ignore