using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using AutoMapper; using Nito.AsyncEx; using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.TheMovieDbApi.Models; // Due to conflicting Genre models in // Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models using Genre = Ombi.TheMovieDbApi.Models.Genre; namespace Ombi.Api.TheMovieDb { public class TheMovieDbApi : IMovieDbApi { public TheMovieDbApi(IMapper mapper, IApi api, ISettingsService settingsService) { Api = api; Mapper = mapper; Settings = new AsyncLazy(() => settingsService.GetSettingsAsync()); } private const string ApiToken = "b8eabaf5608b88d0298aa189dd90bf00"; private const string BaseUri = "http://api.themoviedb.org/3/"; private IMapper Mapper { get; } private IApi Api { get; } private AsyncLazy Settings { get; } public async Task GetMovieInformation(int movieId) { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); var result = await Api.Request(request); return Mapper.Map(result); } public async Task GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode) { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langCode); request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,credits,similar,recommendations,release_dates,external_ids,keywords"); AddRetry(request); return await Api.Request(request, cancellationToken); } public async Task> DiscoverMovies(string langCode, int keywordId) { // https://developers.themoviedb.org/3/discover/movie-discover var request = new Request("discover/movie", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langCode); request.FullUri = request.FullUri.AddQueryParameter("with_keyword", keywordId.ToString()); request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); return await Api.Request>(request); } public async Task> AdvancedSearch(DiscoverModel model, int page, CancellationToken cancellationToken) { var request = new Request($"discover/{model.Type}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); if (model.ReleaseYear.HasValue && model.ReleaseYear.Value > 1900) { request.FullUri = request.FullUri.AddQueryParameter("year", model.ReleaseYear.Value.ToString()); } if (model.KeywordIds.Any()) { request.FullUri = request.FullUri.AddQueryParameter("with_keyword", string.Join(',', model.KeywordIds)); } if (model.GenreIds.Any()) { request.FullUri = request.FullUri.AddQueryParameter("with_genres", string.Join(',', model.GenreIds)); } if (model.WatchProviders.Any()) { request.FullUri = request.FullUri.AddQueryParameter("with_watch_providers", string.Join(',', model.WatchProviders)); } //request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); request.AddQueryString("page", page.ToString()); var result = await Api.Request>(request, cancellationToken); return Mapper.Map>(result.results); } public async Task GetCollection(string langCode, int collectionId, CancellationToken cancellationToken) { // https://developers.themoviedb.org/3/discover/movie-discover var request = new Request($"collection/{collectionId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langCode); return await Api.Request(request, cancellationToken); } public async Task Find(string externalId, ExternalSource source) { var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); request.AddQueryString("external_source", source.ToString()); return await Api.Request(request); } public async Task> SearchByActor(string searchTerm, string langCode) { var request = new Request($"search/person", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); request.AddQueryString("language", langCode); var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); var result = await Api.Request>(request); return result; } public async Task GetActorMovieCredits(int actorId, string langCode) { var request = new Request($"person/{actorId}/movie_credits", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); var result = await Api.Request(request); return result; } public async Task GetActorTvCredits(int actorId, string langCode) { var request = new Request($"person/{actorId}/tv_credits", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); var result = await Api.Request(request); return result; } public async Task> SearchTv(string searchTerm, string year = default) { var request = new Request($"search/tv", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); if (year.HasValue()) { request.AddQueryString("first_air_date_year", year); } AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } public async Task GetTvExternals(int theMovieDbId) { var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); return await Api.Request(request); } public async Task> SimilarMovies(int movieId, string langCode) { var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } public async Task GetMovieInformationWithExtraInfo(int movieId, string langCode = "en") { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("append_to_response", "videos,release_dates"); request.AddQueryString("language", langCode); AddRetry(request); var result = await Api.Request(request); return Mapper.Map(result); } public async Task> SearchMovie(string searchTerm, int? year, string langCode) { var request = new Request($"search/movie", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); request.AddQueryString("language", langCode); if (year.HasValue && year.Value > 0) { request.AddQueryString("year", year.Value.ToString()); } var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } /// /// Maintains filter parity with /movie/popular. /// public async Task> PopularMovies(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)) { return await Popular("movie", langCode, page, cancellationToken); } public async Task> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)) { return await Popular("tv", langCode, page, cancellationToken); } public async Task> Popular(string type, string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)) { var request = new Request($"discover/{type}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); request.AddQueryString("sort_by", "popularity.desc"); if (page != null) { request.AddQueryString("page", page.ToString()); } await AddDiscoverSettings(request); await AddGenreFilter(request, type); AddRetry(request); var result = await Api.Request>(request, cancellationToken); return Mapper.Map>(result.results); } public Task> TopRated(string langCode, int? page = null) { return TopRated("movie", langCode, page); } public Task> TopRatedTv(string langCode, int? page = null) { return TopRated("tv", langCode, page); } /// /// Maintains filter parity with /movie/top_rated. /// private async Task> TopRated(string type, string langCode, int? page = null) { var request = new Request($"discover/{type}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); request.AddQueryString("sort_by", "vote_average.desc"); if (page != null) { request.AddQueryString("page", page.ToString()); } // `vote_count` consideration isn't explicitly documented, but using only the `sort_by` filter // does not provide the same results as `/movie/top_rated`. This appears to be adequate enough // to filter out extremely high-rated movies due to very little votes request.AddQueryString("vote_count.gte", "250"); await AddDiscoverSettings(request); await AddGenreFilter(request, type); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } public Task> TrendingMovies(string langCode, int? page = null) { return Trending("movie", langCode, page); } public Task> TrendingTv(string langCode, int? page = null) { return Trending("tv", langCode, page); } private async Task> Trending(string type, string langCode, int? page = null) { // https://developers.themoviedb.org/3/trending/get-trending var timeWindow = "week"; // another option can be 'day' var request = new Request($"trending/{type}/{timeWindow}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); if (page != null) { request.AddQueryString("page", page.ToString()); } AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } /// /// Maintains filter parity with /movie/upcoming. /// public async Task> UpcomingMovies(string langCode, int? page = null) { var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); // Release types "2 or 3" explicitly stated as used in docs request.AddQueryString("with_release_type", "2|3"); // The date range being used in `/movie/upcoming` isn't documented, but we infer it is // an offset from today based on the minimum and maximum date they provide in the output var startDate = DateTime.Today.AddDays(7); request.AddQueryString("release_date.gte", startDate.ToString("yyyy-MM-dd")); request.AddQueryString("release_date.lte", startDate.AddDays(17).ToString("yyyy-MM-dd")); if (page != null) { request.AddQueryString("page", page.ToString()); } await AddDiscoverSettings(request); await AddGenreFilter(request, "movie"); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } public async Task> UpcomingTv(string langCode, int? page = null) { var request = new Request($"discover/tv", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); // Search for shows that will air in the next month var startDate = DateTime.Today.AddDays(1); request.AddQueryString($"first_air_date.gte", startDate.ToString("yyyy-MM-dd")); request.AddQueryString($"first_air_date.lte", startDate.AddDays(30).ToString("yyyy-MM-dd")); if (page != null) { request.AddQueryString("page", page.ToString()); } await AddDiscoverSettings(request); await AddGenreFilter(request, "tv"); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } /// /// Maintains filter parity with /movie/now_playing. /// public async Task> NowPlaying(string langCode, int? page = null) { var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); // Release types "2 or 3" explicitly stated as used in docs request.AddQueryString("with_release_type", "2|3"); // The date range being used in `/movie/now_playing` isn't documented, but we infer it is // an offset from today based on the minimum and maximum date they provide in the output var today = DateTime.Today; request.AddQueryString("release_date.gte", today.AddDays(-42).ToString("yyyy-MM-dd")); request.AddQueryString("release_date.lte", today.AddDays(6).ToString("yyyy-MM-dd")); if (page != null) { request.AddQueryString("page", page.ToString()); } await AddDiscoverSettings(request); await AddGenreFilter(request, "movie"); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } public async Task GetTVInfo(string themoviedbid, string langCode = "en") { var request = new Request($"/tv/{themoviedbid}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); request.AddQueryString("append_to_response", "videos,credits,similar,recommendations,external_ids,keywords,images"); AddRetry(request); return await Api.Request(request); } public async Task GetSeasonEpisodes(int theMovieDbId, int seasonNumber, CancellationToken token, string langCode = "en") { var request = new Request($"/tv/{theMovieDbId}/season/{seasonNumber}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); AddRetry(request); return await Api.Request(request, token); } public async Task> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null) { var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); request.AddQueryString("sort_by", "vote_average.desc"); request.AddQueryString("with_keywords", keywordId); // `vote_count` consideration isn't explicitly documented, but using only the `sort_by` filter // does not provide the same results as `/movie/top_rated`. This appears to be adequate enough // to filter out extremely high-rated movies due to very little votes request.AddQueryString("vote_count.gte", "250"); if (page != null) { request.AddQueryString("page", page.ToString()); } await AddDiscoverSettings(request); await AddGenreFilter(request, "movie"); AddRetry(request); var result = await Api.Request>(request, cancellationToken); return Mapper.Map>(result.results); } public async Task> SearchKeyword(string searchTerm) { var request = new Request("search/keyword", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); AddRetry(request); var result = await Api.Request>(request); return result.results ?? new List(); } public async Task> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken) { var request = new Request($"/watch/providers/{media}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); AddRetry(request); var result = await Api.Request>(request, cancellationToken); return result.results ?? new List(); } public async Task GetKeyword(int keywordId) { var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); var keyword = await Api.Request(request); return keyword == null || keyword.Id == 0 ? null : keyword; } public async Task> GetGenres(string media, CancellationToken cancellationToken, string languageCode) { var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", languageCode); AddRetry(request); var result = await Api.Request>(request, cancellationToken); return result.genres ?? new List(); } public async Task> GetLanguages(CancellationToken cancellationToken) { var request = new Request($"/configuration/languages", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); var result = await Api.Request>(request, cancellationToken); return result ?? new List(); } public Task> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken) { var request = new Request("search/multi", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", languageCode); request.AddQueryString("query", searchTerm); var result = Api.Request>(request, cancellationToken); return result; } public Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token) { var request = new Request($"movie/{theMoviedbId}/watch/providers", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); return Api.Request(request, token); } public Task GetTvWatchProviders(int theMoviedbId, CancellationToken token) { var request = new Request($"tv/{theMoviedbId}/watch/providers", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); return Api.Request(request, token); } private async Task AddDiscoverSettings(Request request) { var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); if (settings.ExcludedKeywordIds?.Any() == true) { request.AddQueryString("without_keywords", string.Join(",", settings.ExcludedKeywordIds)); } if (settings.OriginalLanguages?.Any() == true) { request.AddQueryString("with_original_language", string.Join("|", settings.OriginalLanguages)); } } private async Task AddGenreFilter(Request request, string media_type) { var settings = await Settings; List excludedGenres; switch (media_type) { case "tv": excludedGenres = settings.ExcludedTvGenreIds; break; case "movie": excludedGenres = settings.ExcludedMovieGenreIds; break; default: return; } if (excludedGenres?.Any() == true) { request.AddQueryString("without_genres", string.Join(",", excludedGenres)); } } private static void AddRetry(Request request) { request.Retry = true; request.StatusCodeToRetry.Add((HttpStatusCode)429); } } }