From 721ef47bb2a2d6798824fbeba59e58955c9fde3c Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Fri, 25 Oct 2019 01:56:56 -0500 Subject: [PATCH] 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"