From 3008cd124d8454024d1de6340497fd4f91d3e815 Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Sat, 19 Oct 2019 00:53:27 -0500 Subject: [PATCH 1/4] Add adult movie filtering --- src/Ombi.Api/Request.cs | 19 +-- .../Models/External/TheMovieDbSettings.cs | 9 ++ src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 122 ++++++++++++------ .../ClientApp/app/interfaces/ISettings.ts | 5 + .../app/services/settings.service.ts | 9 ++ .../ClientApp/app/settings/settings.module.ts | 3 + .../app/settings/settingsmenu.component.html | 1 + .../themoviedb/themoviedb.component.html | 29 +++++ .../themoviedb/themoviedb.component.ts | 32 +++++ src/Ombi/Controllers/SettingsController.cs | 19 +++ 10 files changed, 187 insertions(+), 61 deletions(-) create mode 100644 src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs create mode 100644 src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html create mode 100644 src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts diff --git a/src/Ombi.Api/Request.cs b/src/Ombi.Api/Request.cs index fd888d0d2..918e189fe 100644 --- a/src/Ombi.Api/Request.cs +++ b/src/Ombi.Api/Request.cs @@ -93,24 +93,7 @@ namespace Ombi.Api public void AddQueryString(string key, string value) { if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) return; - - var builder = new UriBuilder(FullUri); - var startingTag = string.Empty; - var hasQuery = false; - if (string.IsNullOrEmpty(builder.Query)) - { - startingTag = "?"; - } - else - { - hasQuery = true; - startingTag = builder.Query.Contains("?") ? "&" : "?"; - } - builder.Query = hasQuery - ? $"{builder.Query}{startingTag}{key}={value}" - : $"{startingTag}{key}={value}"; - - _modified = builder.Uri; + _modified = FullUri.AddQueryParameter(key, value); } public void AddJsonBody(object obj) diff --git a/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs b/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs new file mode 100644 index 000000000..562d9fc88 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs @@ -0,0 +1,9 @@ +namespace Ombi.Core.Settings.Models.External +{ + public sealed class TheMovieDbSettings : Ombi.Settings.Settings.Models.Settings + { + public bool ShowAdultMovies { get; set; } + + public string ExcludedKeywordIds { get; set; } + } +} diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 79ccc5bb7..7c24d4c60 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -1,31 +1,36 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using AutoMapper; +using Nito.AsyncEx; using Ombi.Api.TheMovieDb.Models; -using Ombi.Helpers; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; using Ombi.TheMovieDbApi.Models; namespace Ombi.Api.TheMovieDb { public class TheMovieDbApi : IMovieDbApi { - public TheMovieDbApi(IMapper mapper, IApi api) + 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 readonly string ApiToken = "b8eabaf5608b88d0298aa189dd90bf00"; - private static readonly string BaseUri ="http://api.themoviedb.org/3/"; private IApi Api { get; } + private AsyncLazy Settings { get; } public async Task GetMovieInformation(int movieId) { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + request.AddQueryString("api_key", ApiToken); AddRetry(request); var result = await Api.Request(request); @@ -35,7 +40,7 @@ namespace Ombi.Api.TheMovieDb public async Task Find(string externalId, ExternalSource source) { var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + request.AddQueryString("api_key", ApiToken); AddRetry(request); request.AddQueryString("external_source", source.ToString()); @@ -46,9 +51,11 @@ namespace Ombi.Api.TheMovieDb public async Task> SearchByActor(string searchTerm, string langCode) { var request = new Request($"search/person", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); - request.FullUri = request.FullUri.AddQueryParameter("language", langCode); + 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; @@ -57,8 +64,8 @@ namespace Ombi.Api.TheMovieDb public async Task GetActorMovieCredits(int actorId, string langCode) { var request = new Request($"person/{actorId}/movie_credits", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("language", langCode); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); var result = await Api.Request(request); return result; @@ -67,8 +74,8 @@ namespace Ombi.Api.TheMovieDb public async Task> SearchTv(string searchTerm) { var request = new Request($"search/tv", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("query", searchTerm); AddRetry(request); var result = await Api.Request>(request); @@ -78,7 +85,7 @@ namespace Ombi.Api.TheMovieDb public async Task GetTvExternals(int theMovieDbId) { var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + request.AddQueryString("api_key", ApiToken); AddRetry(request); return await Api.Request(request); @@ -87,9 +94,8 @@ namespace Ombi.Api.TheMovieDb public async Task> SimilarMovies(int movieId, string langCode) { var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - - request.FullUri = request.FullUri.AddQueryParameter("language", langCode); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); AddRetry(request); var result = await Api.Request>(request); @@ -99,65 +105,95 @@ namespace Ombi.Api.TheMovieDb public async Task GetMovieInformationWithExtraInfo(int movieId, string langCode = "en") { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates"); - request.FullUri = request.FullUri.AddQueryParameter("language", langCode); + 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 langageCode) + public async Task> SearchMovie(string searchTerm, int? year, string langCode) { var request = new Request($"search/movie", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); - request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("query", searchTerm); + request.AddQueryString("language", langCode); if (year.HasValue && year.Value > 0) { - request.FullUri = request.FullUri.AddQueryParameter("year", year.Value.ToString()); + 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); } - public async Task> PopularMovies(string langageCode) + public async Task> PopularMovies(string langCode) { - var request = new Request($"movie/popular", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); + var settings = await Settings; + request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); + request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - public async Task> TopRated(string langageCode) + public async Task> TopRated(string langCode) { - var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); + request.AddQueryString("vote_count.gte", "250"); + request.AddQueryString("sort_by", "vote_average.desc"); + var settings = await Settings; + request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); + request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - public async Task> Upcoming(string langageCode) + public async Task> Upcoming(string langCode) { - var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); + request.AddQueryString("with_release_type", "2|3"); + 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")); + var settings = await Settings; + request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); + request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - public async Task> NowPlaying(string langageCode) + public async Task> NowPlaying(string langCode) { - var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); - request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); + request.AddQueryString("with_release_type", "2|3"); + 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")); + var settings = await Settings; + request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); + request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -166,7 +202,7 @@ namespace Ombi.Api.TheMovieDb public async Task GetTVInfo(string themoviedbid) { var request = new Request($"/tv/{themoviedbid}", BaseUri, HttpMethod.Get); - request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + request.AddQueryString("api_key", ApiToken); AddRetry(request); return await Api.Request(request); diff --git a/src/Ombi/ClientApp/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/app/interfaces/ISettings.ts index 75aaf7605..26892ca6e 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISettings.ts @@ -245,3 +245,8 @@ export interface IVoteSettings extends ISettings { musicVoteMax: number; tvShowVoteMax: number; } + +export interface ITheMovieDbSettings extends ISettings { + showAdultMovies: boolean; + excludedKeywordIds: string; +} diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index a80cfd772..8c7787b6d 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -32,6 +32,7 @@ import { ISlackNotificationSettings, ISonarrSettings, ITelegramNotifcationSettings, + ITheMovieDbSettings, IUpdateSettings, IUserManagementSettings, IVoteSettings, @@ -301,6 +302,14 @@ export class SettingsService extends ServiceHelpers { return this.http.post(`${this.url}/vote`, JSON.stringify(settings), {headers: this.headers}); } + public getTheMovieDbSettings(): Observable { + return this.http.get(`${this.url}/themoviedb`, {headers: this.headers}); + } + + public saveTheMovieDbSettings(settings: ITheMovieDbSettings) { + return this.http.post(`${this.url}/themoviedb`, JSON.stringify(settings), {headers: this.headers}); + } + public getNewsletterSettings(): Observable { return this.http.get(`${this.url}/notifications/newsletter`, {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/app/settings/settings.module.ts b/src/Ombi/ClientApp/app/settings/settings.module.ts index 6fb69dc27..4d672d622 100644 --- a/src/Ombi/ClientApp/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/app/settings/settings.module.ts @@ -36,6 +36,7 @@ import { PushbulletComponent } from "./notifications/pushbullet.component"; import { PushoverComponent } from "./notifications/pushover.component"; import { SlackComponent } from "./notifications/slack.component"; import { TelegramComponent } from "./notifications/telegram.component"; +import { TheMovieDbComponent } from "./themoviedb/themoviedb.component"; import { OmbiComponent } from "./ombi/ombi.component"; import { PlexComponent } from "./plex/plex.component"; import { RadarrComponent } from "./radarr/radarr.component"; @@ -80,6 +81,7 @@ const routes: Routes = [ { path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] }, { path: "Lidarr", component: LidarrComponent, canActivate: [AuthGuard] }, { path: "Vote", component: VoteComponent, canActivate: [AuthGuard] }, + { path: "TheMovieDb", component: TheMovieDbComponent, canActivate: [AuthGuard] }, { path: "FailedRequests", component: FailedRequestsComponent, canActivate: [AuthGuard] }, ]; @@ -135,6 +137,7 @@ const routes: Routes = [ NewsletterComponent, LidarrComponent, VoteComponent, + TheMovieDbComponent, FailedRequestsComponent, ], exports: [ diff --git a/src/Ombi/ClientApp/app/settings/settingsmenu.component.html b/src/Ombi/ClientApp/app/settings/settingsmenu.component.html index d58c96e2e..686f4c020 100644 --- a/src/Ombi/ClientApp/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/app/settings/settingsmenu.component.html @@ -13,6 +13,7 @@
  • User Importer
  • Authentication
  • Vote
  • +
  • The Movie Database
  • diff --git a/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html new file mode 100644 index 000000000..afce968c2 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html @@ -0,0 +1,29 @@ + + +
    + The Movie Database +
    +
    +
    + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts new file mode 100644 index 000000000..e167d3009 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from "@angular/core"; + +import { ITheMovieDbSettings } from "../../interfaces"; +import { NotificationService } from "../../services"; +import { SettingsService } from "../../services"; + +@Component({ + templateUrl: "./themoviedb.component.html", +}) +export class TheMovieDbComponent implements OnInit { + + public settings: ITheMovieDbSettings; + public advanced: boolean; + + constructor(private settingsService: SettingsService, private notificationService: NotificationService) { } + + public ngOnInit() { + this.settingsService.getTheMovieDbSettings().subscribe(x => { + this.settings = x; + }); + } + + public save() { + this.settingsService.saveTheMovieDbSettings(this.settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved The Movie Database settings"); + } else { + this.notificationService.success("There was an error when saving The Movie Database settings"); + } + }); + } +} diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index ebc2fbe66..6033ae2ca 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -659,6 +659,25 @@ namespace Ombi.Controllers return vote.Enabled; } + /// + /// Save The Movie DB settings. + /// + /// The settings. + [HttpPost("themoviedb")] + public async Task TheMovieDbSettings([FromBody]TheMovieDbSettings settings) + { + return await Save(settings); + } + + /// + /// Get The Movie DB settings. + /// + [HttpGet("themoviedb")] + public async Task TheMovieDbSettings() + { + return await Get(); + } + /// /// Saves the email notification settings. /// From 9154dbbe9b16eddc8422db4c81a231ecc00003c0 Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Sat, 19 Oct 2019 01:28:13 -0500 Subject: [PATCH 2/4] Fix TS import order --- src/Ombi/ClientApp/app/settings/settings.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi/ClientApp/app/settings/settings.module.ts b/src/Ombi/ClientApp/app/settings/settings.module.ts index 4d672d622..00328c778 100644 --- a/src/Ombi/ClientApp/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/app/settings/settings.module.ts @@ -36,12 +36,12 @@ import { PushbulletComponent } from "./notifications/pushbullet.component"; import { PushoverComponent } from "./notifications/pushover.component"; import { SlackComponent } from "./notifications/slack.component"; import { TelegramComponent } from "./notifications/telegram.component"; -import { TheMovieDbComponent } from "./themoviedb/themoviedb.component"; import { OmbiComponent } from "./ombi/ombi.component"; import { PlexComponent } from "./plex/plex.component"; import { RadarrComponent } from "./radarr/radarr.component"; import { SickRageComponent } from "./sickrage/sickrage.component"; import { SonarrComponent } from "./sonarr/sonarr.component"; +import { TheMovieDbComponent } from "./themoviedb/themoviedb.component"; import { UpdateComponent } from "./update/update.component"; import { UserManagementComponent } from "./usermanagement/usermanagement.component"; import { VoteComponent } from "./vote/vote.component"; From 22d47512b692a4cac9d54f752ec445ee7bacd3cd Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Tue, 22 Oct 2019 12:12:20 -0500 Subject: [PATCH 3/4] Add comments to clarify filter decisions --- src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 32 ++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 7c24d4c60..b5f0a1596 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -133,11 +133,15 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } + /// + /// Maintains filter parity with /movie/popular. + /// public async Task> PopularMovies(string langCode) { var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); + request.AddQueryString("sort_by", "popularity.desc"); var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); @@ -147,13 +151,21 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } + /// + /// Maintains filter parity with /movie/top_rated. + /// public async Task> TopRated(string langCode) { var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); - request.AddQueryString("vote_count.gte", "250"); request.AddQueryString("sort_by", "vote_average.desc"); + + // `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"); + var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); @@ -163,15 +175,24 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } + /// + /// Maintains filter parity with /movie/upcoming. + /// public async Task> Upcoming(string langCode) { 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")); + var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); @@ -181,15 +202,24 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } + /// + /// Maintains filter parity with /movie/now_playing. + /// public async Task> NowPlaying(string langCode) { 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")); + var settings = await Settings; request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); From 721ef47bb2a2d6798824fbeba59e58955c9fde3c Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Fri, 25 Oct 2019 01:56:56 -0500 Subject: [PATCH 4/4] Use tags and autocomplete for excluded keywords --- .../Models/External/TheMovieDbSettings.cs | 6 ++- src/Ombi.TheMovieDbApi/IMovieDbApi.cs | 2 + src/Ombi.TheMovieDbApi/Models/Keyword.cs | 13 +++++ src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 53 +++++++++++++------ src/Ombi/ClientApp/app/interfaces/IMovieDb.ts | 4 ++ .../ClientApp/app/interfaces/ISettings.ts | 2 +- src/Ombi/ClientApp/app/interfaces/index.ts | 1 + .../app/services/applications/index.ts | 1 + .../applications/themoviedb.service.ts | 25 +++++++++ .../ClientApp/app/settings/settings.module.ts | 5 +- .../themoviedb/themoviedb.component.html | 31 +++++++++-- .../themoviedb/themoviedb.component.ts | 47 ++++++++++++++-- src/Ombi/ClientApp/styles/base.scss | 8 +++ .../External/TheMovieDbController.cs | 38 +++++++++++++ src/Ombi/package.json | 1 + src/Ombi/yarn.lock | 15 ++++++ 16 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 src/Ombi.TheMovieDbApi/Models/Keyword.cs create mode 100644 src/Ombi/ClientApp/app/interfaces/IMovieDb.ts create mode 100644 src/Ombi/ClientApp/app/services/applications/themoviedb.service.ts create mode 100644 src/Ombi/Controllers/External/TheMovieDbController.cs diff --git a/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs b/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs index 562d9fc88..fe0c238d8 100644 --- a/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/TheMovieDbSettings.cs @@ -1,9 +1,11 @@ -namespace Ombi.Core.Settings.Models.External +using System.Collections.Generic; + +namespace Ombi.Core.Settings.Models.External { public sealed class TheMovieDbSettings : Ombi.Settings.Settings.Models.Settings { public bool ShowAdultMovies { get; set; } - public string ExcludedKeywordIds { get; set; } + public List ExcludedKeywordIds { get; set; } } } diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 43d8b02c1..16ff4ce92 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -21,5 +21,7 @@ namespace Ombi.Api.TheMovieDb Task GetTVInfo(string themoviedbid); Task> SearchByActor(string searchTerm, string langCode); Task GetActorMovieCredits(int actorId, string langCode); + Task> SearchKeyword(string searchTerm); + Task GetKeyword(int keywordId); } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/Keyword.cs b/src/Ombi.TheMovieDbApi/Models/Keyword.cs new file mode 100644 index 000000000..770eebc94 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/Keyword.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Ombi.Api.TheMovieDb.Models +{ + public sealed class Keyword + { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + } +} diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index b5f0a1596..a55bdadeb 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -142,10 +143,7 @@ namespace Ombi.Api.TheMovieDb request.AddQueryString("api_key", ApiToken); request.AddQueryString("language", langCode); request.AddQueryString("sort_by", "popularity.desc"); - var settings = await Settings; - request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); - request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); - + await AddDiscoverMovieSettings(request); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -165,11 +163,8 @@ namespace Ombi.Api.TheMovieDb // 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"); - - var settings = await Settings; - request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); - request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); + await AddDiscoverMovieSettings(request); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -193,10 +188,7 @@ namespace Ombi.Api.TheMovieDb request.AddQueryString("release_date.gte", startDate.ToString("yyyy-MM-dd")); request.AddQueryString("release_date.lte", startDate.AddDays(17).ToString("yyyy-MM-dd")); - var settings = await Settings; - request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); - request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); - + await AddDiscoverMovieSettings(request); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -220,10 +212,7 @@ namespace Ombi.Api.TheMovieDb request.AddQueryString("release_date.gte", today.AddDays(-42).ToString("yyyy-MM-dd")); request.AddQueryString("release_date.lte", today.AddDays(6).ToString("yyyy-MM-dd")); - var settings = await Settings; - request.AddQueryString("include_adult", settings.ShowAdultMovies.ToString().ToLower()); - request.AddQueryString("without_keywords", settings.ExcludedKeywordIds); - + await AddDiscoverMovieSettings(request); AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -237,6 +226,38 @@ namespace Ombi.Api.TheMovieDb return await Api.Request(request); } + + 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 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; + } + + private async Task AddDiscoverMovieSettings(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)); + } + } + private static void AddRetry(Request request) { request.Retry = true; diff --git a/src/Ombi/ClientApp/app/interfaces/IMovieDb.ts b/src/Ombi/ClientApp/app/interfaces/IMovieDb.ts new file mode 100644 index 000000000..63443ae4c --- /dev/null +++ b/src/Ombi/ClientApp/app/interfaces/IMovieDb.ts @@ -0,0 +1,4 @@ +export interface IMovieDbKeyword { + id: number; + name: string; +} diff --git a/src/Ombi/ClientApp/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/app/interfaces/ISettings.ts index 26892ca6e..d3dece200 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISettings.ts @@ -248,5 +248,5 @@ export interface IVoteSettings extends ISettings { export interface ITheMovieDbSettings extends ISettings { showAdultMovies: boolean; - excludedKeywordIds: string; + excludedKeywordIds: number[]; } diff --git a/src/Ombi/ClientApp/app/interfaces/index.ts b/src/Ombi/ClientApp/app/interfaces/index.ts index e1cf823e8..24eac0093 100644 --- a/src/Ombi/ClientApp/app/interfaces/index.ts +++ b/src/Ombi/ClientApp/app/interfaces/index.ts @@ -3,6 +3,7 @@ export * from "./ICouchPotato"; export * from "./IImages"; export * from "./IMediaServerStatus"; export * from "./INotificationSettings"; +export * from "./IMovieDb"; export * from "./IPlex"; export * from "./IRadarr"; export * from "./IRequestEngineResult"; diff --git a/src/Ombi/ClientApp/app/services/applications/index.ts b/src/Ombi/ClientApp/app/services/applications/index.ts index 295a53415..28edca151 100644 --- a/src/Ombi/ClientApp/app/services/applications/index.ts +++ b/src/Ombi/ClientApp/app/services/applications/index.ts @@ -7,3 +7,4 @@ export * from "./tester.service"; export * from "./plexoauth.service"; export * from "./plextv.service"; export * from "./lidarr.service"; +export * from "./themoviedb.service"; diff --git a/src/Ombi/ClientApp/app/services/applications/themoviedb.service.ts b/src/Ombi/ClientApp/app/services/applications/themoviedb.service.ts new file mode 100644 index 000000000..0b5d83278 --- /dev/null +++ b/src/Ombi/ClientApp/app/services/applications/themoviedb.service.ts @@ -0,0 +1,25 @@ +import { PlatformLocation } from "@angular/common"; +import { HttpClient, HttpErrorResponse, HttpParams } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { empty, Observable, throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; + +import { IMovieDbKeyword } from "../../interfaces"; +import { ServiceHelpers } from "../service.helpers"; + +@Injectable() +export class TheMovieDbService extends ServiceHelpers { + constructor(http: HttpClient, public platformLocation: PlatformLocation) { + super(http, "/api/v1/TheMovieDb", platformLocation); + } + + public getKeywords(searchTerm: string): Observable { + const params = new HttpParams().set("searchTerm", searchTerm); + return this.http.get(`${this.url}/Keywords`, {headers: this.headers, params}); + } + + public getKeyword(keywordId: number): Observable { + return this.http.get(`${this.url}/Keywords/${keywordId}`, { headers: this.headers }) + .pipe(catchError((error: HttpErrorResponse) => error.status === 404 ? empty() : throwError(error))); + } +} diff --git a/src/Ombi/ClientApp/app/settings/settings.module.ts b/src/Ombi/ClientApp/app/settings/settings.module.ts index 00328c778..377756e56 100644 --- a/src/Ombi/ClientApp/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/app/settings/settings.module.ts @@ -3,13 +3,14 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule, Routes } from "@angular/router"; import { NgbAccordionModule, NgbModule } from "@ng-bootstrap/ng-bootstrap"; +import { TagInputModule } from "ngx-chips"; import { ClipboardModule } from "ngx-clipboard"; import { AuthGuard } from "../auth/auth.guard"; import { AuthService } from "../auth/auth.service"; import { CouchPotatoService, EmbyService, IssuesService, JobService, LidarrService, MobileService, NotificationMessageService, PlexService, RadarrService, - RequestRetryService, SonarrService, TesterService, ValidationService, + RequestRetryService, SonarrService, TesterService, TheMovieDbService, ValidationService, } from "../services"; import { PipeModule } from "../pipes/pipe.module"; @@ -99,6 +100,7 @@ const routes: Routes = [ NgbAccordionModule, AutoCompleteModule, CalendarModule, + TagInputModule, ClipboardModule, PipeModule, RadioButtonModule, @@ -159,6 +161,7 @@ const routes: Routes = [ NotificationMessageService, LidarrService, RequestRetryService, + TheMovieDbService, ], }) diff --git a/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html index afce968c2..ccadc68ac 100644 --- a/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html +++ b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.html @@ -11,13 +11,34 @@
    -
    diff --git a/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts index e167d3009..4fc9138a0 100644 --- a/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts +++ b/src/Ombi/ClientApp/app/settings/themoviedb/themoviedb.component.ts @@ -1,8 +1,16 @@ import { Component, OnInit } from "@angular/core"; +import { empty, of } from "rxjs"; import { ITheMovieDbSettings } from "../../interfaces"; import { NotificationService } from "../../services"; import { SettingsService } from "../../services"; +import { TheMovieDbService } from "../../services"; + +interface IKeywordTag { + id: number; + name: string; + initial: boolean; +} @Component({ templateUrl: "./themoviedb.component.html", @@ -10,17 +18,48 @@ import { SettingsService } from "../../services"; export class TheMovieDbComponent implements OnInit { public settings: ITheMovieDbSettings; - public advanced: boolean; + public excludedKeywords: IKeywordTag[]; - constructor(private settingsService: SettingsService, private notificationService: NotificationService) { } + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private tmdbService: TheMovieDbService) { } public ngOnInit() { - this.settingsService.getTheMovieDbSettings().subscribe(x => { - this.settings = x; + this.settingsService.getTheMovieDbSettings().subscribe(settings => { + this.settings = settings; + this.excludedKeywords = settings.excludedKeywordIds + ? settings.excludedKeywordIds.map(id => ({ + id, + name: "", + initial: true, + })) + : []; }); } + public autocompleteKeyword = (text: string) => this.tmdbService.getKeywords(text); + + public onAddingKeyword = (tag: string | IKeywordTag) => { + if (typeof tag === "string") { + const id = Number(tag); + return isNaN(id) ? empty() : this.tmdbService.getKeyword(id); + } else { + return of(tag); + } + } + + public onKeywordSelect = (keyword: IKeywordTag) => { + if (keyword.initial) { + this.tmdbService.getKeyword(keyword.id) + .subscribe(k => { + keyword.name = k.name; + keyword.initial = false; + }); + } + } + public save() { + this.settings.excludedKeywordIds = this.excludedKeywords.map(k => k.id); this.settingsService.saveTheMovieDbSettings(this.settings).subscribe(x => { if (x) { this.notificationService.success("Successfully saved The Movie Database settings"); diff --git a/src/Ombi/ClientApp/styles/base.scss b/src/Ombi/ClientApp/styles/base.scss index 9c7756d8f..ce56d0033 100644 --- a/src/Ombi/ClientApp/styles/base.scss +++ b/src/Ombi/ClientApp/styles/base.scss @@ -1010,6 +1010,14 @@ a > h4:hover { width:300px; } +.ng2-tag-input.dark input { + background: transparent; + color: white; +} + +.ng2-tag-input .fa-cloud-download { + margin-right: 5px; +} ::ng-deep ngb-accordion > div.card > div.card-header { padding:0px; diff --git a/src/Ombi/Controllers/External/TheMovieDbController.cs b/src/Ombi/Controllers/External/TheMovieDbController.cs new file mode 100644 index 000000000..714831633 --- /dev/null +++ b/src/Ombi/Controllers/External/TheMovieDbController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Attributes; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Ombi.Controllers.External +{ + [Admin] + [ApiV1] + [Produces("application/json")] + public sealed class TheMovieDbController : Controller + { + public TheMovieDbController(IMovieDbApi tmdbApi) => TmdbApi = tmdbApi; + + private IMovieDbApi TmdbApi { get; } + + /// + /// Searches for keywords matching the specified term. + /// + /// The search term. + [HttpGet("Keywords")] + public async Task> GetKeywords([FromQuery]string searchTerm) => + await TmdbApi.SearchKeyword(searchTerm); + + /// + /// Gets the keyword matching the specified ID. + /// + /// The keyword ID. + [HttpGet("Keywords/{keywordId}")] + public async Task GetKeywords(int keywordId) + { + var keyword = await TmdbApi.GetKeyword(keywordId); + return keyword == null ? NotFound() : (IActionResult)Ok(keyword); + } + } +} diff --git a/src/Ombi/package.json b/src/Ombi/package.json index acd432418..47ddb1fd2 100644 --- a/src/Ombi/package.json +++ b/src/Ombi/package.json @@ -63,6 +63,7 @@ "natives": "1.1.6", "ng2-cookies": "^1.0.12", "ngx-bootstrap": "^3.1.4", + "ngx-chips": "^2.1.0", "ngx-clipboard": "^11.1.1", "ngx-editor": "^4.1.0", "ngx-infinite-scroll": "^6.0.1", diff --git a/src/Ombi/yarn.lock b/src/Ombi/yarn.lock index 3f35f5f50..d68dca944 100644 --- a/src/Ombi/yarn.lock +++ b/src/Ombi/yarn.lock @@ -4183,10 +4183,25 @@ ng2-cookies@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/ng2-cookies/-/ng2-cookies-1.0.12.tgz#3f3e613e0137b0649b705c678074b4bd08149ccc" +ng2-material-dropdown@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/ng2-material-dropdown/-/ng2-material-dropdown-0.11.0.tgz#27a402ef3cbdcaf6791ef4cfd4b257e31db7546f" + integrity sha512-wptBo09qKecY0QPTProAThrc4A3ajJTcHE9LTpCG5XZZUhXLBzhnGK8OW33TN8A+K/jqcs7OB74ppYJiqs3nhQ== + dependencies: + tslib "^1.9.0" + ngx-bootstrap@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-3.1.4.tgz#5105c0227da3b51a1972d04efa1504a79474fd57" +ngx-chips@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ngx-chips/-/ngx-chips-2.1.0.tgz#aa299bcf40dc3e1f6288bf1d29e2fdfe9a132ed3" + integrity sha512-OQV4dTfD3nXm5d2mGKUSgwOtJOaMnZ4F+lwXOtd7DWRSUne0JQWwoZNHdOpuS6saBGhqCPDAwq6KxdR5XSgZUQ== + dependencies: + ng2-material-dropdown "0.11.0" + tslib "^1.9.0" + ngx-clipboard@^11.1.1: version "11.1.9" resolved "https://registry.yarnpkg.com/ngx-clipboard/-/ngx-clipboard-11.1.9.tgz#a391853dc49e436de407260863a2c814d73a9332"