From 8c7087452559fa49963584f81b05b9668930fd4d Mon Sep 17 00:00:00 2001 From: stefangross Date: Sun, 4 Jul 2021 10:29:17 +0200 Subject: [PATCH 1/8] fixed issue with sorting movie requests --- src/Ombi.Core/Engine/MovieRequestEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 269961c2f..7507389b7 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -315,7 +315,7 @@ namespace Ombi.Core.Engine // TODO fix this so we execute this on the server var requests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) - ? allRequests.ToList().OrderBy(x => x.RequestedDate).ToList() + ? allRequests.ToList().OrderBy(x => prop.GetValue(x)).ToList() : allRequests.ToList().OrderByDescending(x => prop.GetValue(x)).ToList(); var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); From d01da2d28811b6a6df60552e6c9b45f8e9a5d671 Mon Sep 17 00:00:00 2001 From: stefangross Date: Sun, 4 Jul 2021 10:29:17 +0200 Subject: [PATCH 2/8] fixed issue with sorting movie requests --- src/Ombi.Core/Engine/MovieRequestEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 269961c2f..7507389b7 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -315,7 +315,7 @@ namespace Ombi.Core.Engine // TODO fix this so we execute this on the server var requests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) - ? allRequests.ToList().OrderBy(x => x.RequestedDate).ToList() + ? allRequests.ToList().OrderBy(x => prop.GetValue(x)).ToList() : allRequests.ToList().OrderByDescending(x => prop.GetValue(x)).ToList(); var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); From 83ee2dae1a5e5d7e38f9db533b45e085efaaa369 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 26 Jul 2021 14:39:06 +0100 Subject: [PATCH 3/8] Made a start on the advanced options --- src/Ombi.TheMovieDbApi/IMovieDbApi.cs | 8 +- .../{Keyword.cs => TheMovidDbKeyValue.cs} | 2 +- src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 14 +- src/Ombi/ClientApp/src/app/app.module.ts | 120 +++++++++++------- .../search-results.component.html | 1 + .../src/app/my-nav/my-nav.component.html | 1 + .../src/app/my-nav/my-nav.component.ts | 7 + .../applications/themoviedb.service.ts | 4 +- .../themoviedb/themoviedb.component.ts | 12 +- .../advanced-search-dialog.component.html | 58 +++++++++ .../advanced-search-dialog.component.scss | 8 ++ .../advanced-search-dialog.component.ts | 36 ++++++ .../genre-select/genre-select.component.html | 25 ++++ .../genre-select/genre-select.component.ts | 85 +++++++++++++ .../keyword-search.component.html | 37 ++++++ .../keyword-search.component.ts | 64 ++++++++++ .../ClientApp/src/app/shared/shared.module.ts | 58 +++++---- .../V1/External/TheMovieDbController.cs | 16 +-- 18 files changed, 457 insertions(+), 99 deletions(-) rename src/Ombi.TheMovieDbApi/Models/{Keyword.cs => TheMovidDbKeyValue.cs} (84%) create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts create mode 100644 src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts create mode 100644 src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 7a0f7e385..202392c5f 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -26,7 +26,7 @@ namespace Ombi.Api.TheMovieDb Task> UpcomingTv(string languageCode, int? page = null); Task> SimilarMovies(int movieId, string langCode); Task Find(string externalId, ExternalSource source); - Task GetTvExternals(int theMovieDbId); + Task GetTvExternals(int theMovieDbId); Task GetSeasonEpisodes(int theMovieDbId, int seasonNumber, CancellationToken token, string langCode = "en"); Task GetTVInfo(string themoviedbid, string langCode = "en"); Task> SearchByActor(string searchTerm, string langCode); @@ -35,10 +35,10 @@ namespace Ombi.Api.TheMovieDb Task> DiscoverMovies(string langCode, int keywordId); Task GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode); Task GetCollection(string langCode, int collectionId, CancellationToken cancellationToken); - Task> SearchKeyword(string searchTerm); - Task GetKeyword(int keywordId); + Task> SearchKeyword(string searchTerm); + Task GetKeyword(int keywordId); Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token); Task GetTvWatchProviders(int theMoviedbId, CancellationToken token); - Task> GetGenres(string media); + Task> GetGenres(string media, CancellationToken cancellationToken); } } diff --git a/src/Ombi.TheMovieDbApi/Models/Keyword.cs b/src/Ombi.TheMovieDbApi/Models/TheMovidDbKeyValue.cs similarity index 84% rename from src/Ombi.TheMovieDbApi/Models/Keyword.cs rename to src/Ombi.TheMovieDbApi/Models/TheMovidDbKeyValue.cs index 770eebc94..a5f3fc0db 100644 --- a/src/Ombi.TheMovieDbApi/Models/Keyword.cs +++ b/src/Ombi.TheMovieDbApi/Models/TheMovidDbKeyValue.cs @@ -2,7 +2,7 @@ namespace Ombi.Api.TheMovieDb.Models { - public sealed class Keyword + public sealed class TheMovidDbKeyValue { [DataMember(Name = "id")] public int Id { get; set; } diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 70e4a94c7..a8fdc3269 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -357,34 +357,34 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } - public async Task> SearchKeyword(string searchTerm) + 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(); + var result = await Api.Request>(request); + return result.results ?? new List(); } - public async Task GetKeyword(int keywordId) + 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); + var keyword = await Api.Request(request); return keyword == null || keyword.Id == 0 ? null : keyword; } - public async Task> GetGenres(string media) + public async Task> GetGenres(string media, CancellationToken cancellationToken) { var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); - var result = await Api.Request>(request); + var result = await Api.Request>(request, cancellationToken); return result.genres ?? new List(); } diff --git a/src/Ombi/ClientApp/src/app/app.module.ts b/src/Ombi/ClientApp/src/app/app.module.ts index 8e44d59bd..1716b5c5e 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -1,73 +1,98 @@ -import { CommonModule, PlatformLocation, APP_BASE_HREF } from "@angular/common"; -import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; -import { NgModule } from "@angular/core"; +import { APP_BASE_HREF, CommonModule, PlatformLocation } from "@angular/common"; +import { CardsFreeModule, MDBBootstrapModule, NavbarModule } from "angular-bootstrap-md"; +import { CustomPageService, ImageService, RequestService, SettingsService } from "./services"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http"; +import { IdentityService, IssuesService, JobService, MessageService, PlexTvService, SearchService, StatusService } from "./services"; import { RouterModule, Routes } from "@angular/router"; - -import { JwtModule } from "@auth0/angular-jwt"; import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; -import { TranslateHttpLoader } from "@ngx-translate/http-loader"; -import { CookieService } from "ng2-cookies"; +import { AppComponent } from "./app.component"; +import { AuthGuard } from "./auth/auth.guard"; +import { AuthService } from "./auth/auth.service"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { BrowserModule } from "@angular/platform-browser"; import { ButtonModule } from "primeng/button"; import { ConfirmDialogModule } from "primeng/confirmdialog"; +import { CookieComponent } from "./auth/cookie.component"; +import { CookieService } from "ng2-cookies"; +import { CustomPageComponent } from "./custompage/custompage.component"; import { DataViewModule } from "primeng/dataview"; import { DialogModule } from "primeng/dialog"; -import { OverlayPanelModule } from "primeng/overlaypanel"; -import { TooltipModule } from "primeng/tooltip"; -import { SidebarModule } from "primeng/sidebar"; - +import { FilterService } from "./discover/services/filter-service"; +import { JwtModule } from "@auth0/angular-jwt"; +import { LandingPageComponent } from "./landingpage/landingpage.component"; +import { LandingPageService } from "./services"; +import { LayoutModule } from '@angular/cdk/layout'; +import { LoginComponent } from "./login/login.component"; +import { LoginOAuthComponent } from "./login/loginoauth.component"; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from "@angular/material/card"; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatNativeDateModule } from '@angular/material/core'; +import { MatChipsModule } from "@angular/material/chips"; +import { MatDialogModule } from "@angular/material/dialog"; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from "@angular/material/input"; import { MatListModule } from '@angular/material/list'; +import { MatMenuModule } from "@angular/material/menu"; +import { MatNativeDateModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatCardModule } from "@angular/material/card"; -import { MatInputModule } from "@angular/material/input"; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTabsModule } from "@angular/material/tabs"; +import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from "@angular/material/tooltip"; +import { MyNavComponent } from './my-nav/my-nav.component'; +import { NavSearchComponent } from "./my-nav/nav-search.component"; +import { NgModule } from "@angular/core"; +import { NotificationService } from "./services"; +import { OverlayModule } from "@angular/cdk/overlay"; +import { OverlayPanelModule } from "primeng/overlaypanel"; +import { PageNotFoundComponent } from "./errors/not-found.component"; +import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component"; +import { ResetPasswordComponent } from "./login/resetpassword.component"; +import { SearchV2Service } from "./services/searchV2.service"; +import { SidebarModule } from "primeng/sidebar"; +import { SignalRNotificationService } from "./services/signlarnotification.service"; +import { StorageService } from "./shared/storage/storage-service"; +import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component"; +import { TooltipModule } from "primeng/tooltip"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor"; + +// Components + + + + + + -import { MDBBootstrapModule, CardsFreeModule, NavbarModule } from "angular-bootstrap-md"; -// Components -import { AppComponent } from "./app.component"; -import { CookieComponent } from "./auth/cookie.component"; -import { CustomPageComponent } from "./custompage/custompage.component"; -import { PageNotFoundComponent } from "./errors/not-found.component"; -import { LandingPageComponent } from "./landingpage/landingpage.component"; -import { LoginComponent } from "./login/login.component"; -import { LoginOAuthComponent } from "./login/loginoauth.component"; -import { ResetPasswordComponent } from "./login/resetpassword.component"; -import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component"; // Services -import { AuthGuard } from "./auth/auth.guard"; -import { AuthService } from "./auth/auth.service"; -import { ImageService, SettingsService, CustomPageService, RequestService } from "./services"; -import { LandingPageService } from "./services"; -import { NotificationService } from "./services"; -import { IssuesService, JobService, PlexTvService, StatusService, SearchService, IdentityService, MessageService } from "./services"; -import { MyNavComponent } from './my-nav/my-nav.component'; -import { LayoutModule } from '@angular/cdk/layout'; -import { SearchV2Service } from "./services/searchV2.service"; -import { NavSearchComponent } from "./my-nav/nav-search.component"; -import { OverlayModule } from "@angular/cdk/overlay"; -import { StorageService } from "./shared/storage/storage-service"; -import { SignalRNotificationService } from "./services/signlarnotification.service"; -import { MatMenuModule } from "@angular/material/menu"; -import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component"; -import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor"; -import { FilterService } from "./discover/services/filter-service"; + + + + + + + + + + + + + + + + + + const routes: Routes = [ { path: "*", component: PageNotFoundComponent }, @@ -135,6 +160,8 @@ export function JwtTokenGetter() { MatMenuModule, MatInputModule, MatTabsModule, + MatChipsModule, + MatDialogModule, ReactiveFormsModule, MatAutocompleteModule, TooltipModule, @@ -146,7 +173,6 @@ export function JwtTokenGetter() { MatCheckboxModule, MatProgressSpinnerModule, MDBBootstrapModule.forRoot(), - // NbThemeModule.forRoot({ name: 'dark'}), JwtModule.forRoot({ config: { tokenGetter: JwtTokenGetter, diff --git a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html index 29a583890..2bd13441d 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html @@ -1,4 +1,5 @@
+
diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html index 95f31198b..84ae81fe8 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html @@ -78,6 +78,7 @@ {{ 'NavigationBar.Filter.Music' | translate}} + diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts index 57bfaca91..393d12fd4 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts @@ -3,9 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { IUser, RequestType, UserType } from '../interfaces'; import { SettingsService, SettingsStateService } from '../services'; +import { AdvancedSearchDialogComponent } from '../shared/advanced-search-dialog/advanced-search-dialog.component'; import { FilterService } from '../discover/services/filter-service'; import { ILocalUser } from '../auth/IUserLogin'; import { INavBar } from '../interfaces/ICommon'; +import { MatDialog } from '@angular/material/dialog'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { Md5 } from 'ts-md5/dist/md5'; import { Observable } from 'rxjs'; @@ -54,6 +56,7 @@ export class MyNavComponent implements OnInit { private settingsService: SettingsService, private store: StorageService, private filterService: FilterService, + private dialogService: MatDialog, private readonly settingState: SettingsStateService) { } @@ -121,6 +124,10 @@ export class MyNavComponent implements OnInit { this.store.save("searchFilter", JSON.stringify(this.searchFilter)); } + public openAdvancedSearch() { + this.dialogService.open(AdvancedSearchDialogComponent, null); + } + public getUserImage(): string { var fallback = this.applicationLogo ? this.applicationLogo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png'; return `https://www.gravatar.com/avatar/${this.emailHash}?d=${fallback}`; diff --git a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts index ff76d591b..0af37c508 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts @@ -7,7 +7,9 @@ import { catchError } from "rxjs/operators"; import { IMovieDbKeyword } from "../../interfaces"; import { ServiceHelpers } from "../service.helpers"; -@Injectable() +@Injectable({ + providedIn: 'root', + }) export class TheMovieDbService extends ServiceHelpers { constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { super(http, "/api/v1/TheMovieDb", href); diff --git a/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts b/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts index 49ec3e51f..008e2dfab 100644 --- a/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts @@ -1,13 +1,13 @@ import {COMMA, ENTER} from "@angular/cdk/keycodes"; -import { Component, OnInit, ElementRef, ViewChild } from "@angular/core"; -import { MatAutocomplete } from "@angular/material/autocomplete"; +import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { IMovieDbKeyword, ITheMovieDbSettings } from "../../interfaces"; +import { debounceTime, switchMap } from "rxjs/operators"; -import { ITheMovieDbSettings, IMovieDbKeyword } from "../../interfaces"; +import { MatAutocomplete } from "@angular/material/autocomplete"; import { NotificationService } from "../../services"; import { SettingsService } from "../../services"; import { TheMovieDbService } from "../../services"; -import { FormBuilder, FormGroup } from "@angular/forms"; -import { debounceTime, switchMap } from "rxjs/operators"; interface IKeywordTag { id: number; @@ -30,8 +30,6 @@ export class TheMovieDbComponent implements OnInit { public filteredMovieGenres: IMovieDbKeyword[]; public filteredTvGenres: IMovieDbKeyword[]; - @ViewChild('fruitInput') public fruitInput: ElementRef; - constructor(private settingsService: SettingsService, private notificationService: NotificationService, private tmdbService: TheMovieDbService, diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html new file mode 100644 index 000000000..02f9ac35b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -0,0 +1,58 @@ +
+

+ Advanced Search +

+
+ + +
+ +
+ +
+
+
+ + Movies + TV Shows + +
+
+
+ + Year + + +
+ +
+ +
+
+ +
+ +
+ +
+ + +
+
diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss new file mode 100644 index 000000000..8d701c21f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss @@ -0,0 +1,8 @@ + +@import "~styles/variables.scss"; + +.alert-info { + background: $accent; + border-color: $ombi-background-primary; + color:white; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts new file mode 100644 index 000000000..e59f7fab7 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -0,0 +1,36 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; + + +@Component({ + selector: "advanced-search-dialog", + templateUrl: "advanced-search-dialog.component.html", + styleUrls: [ "advanced-search-dialog.component.scss" ] +}) +export class AdvancedSearchDialogComponent implements OnInit { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private fb: FormBuilder + ) {} + + public form: FormGroup; + + + public async ngOnInit() { + + this.form = this.fb.group({ + keywords: [[]], + genres: [[]], + releaseYear: [], + type: ['movie'], + }) + + this.form.controls.type.valueChanges.subscribe(val => { + this.form.controls.genres.setValue([]); + }); + + + } +} diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html new file mode 100644 index 000000000..523974a90 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html @@ -0,0 +1,25 @@ + + + Genres + + + {{word.name}} + cancel + + + + + + {{word.name}} + + + + diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts new file mode 100644 index 000000000..c80eb4ae8 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts @@ -0,0 +1,85 @@ +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from "rxjs/operators"; + +import { IMovieDbKeyword } from "../../../interfaces"; +import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { Observable } from "rxjs"; +import { TheMovieDbService } from "../../../services"; + +@Component({ + selector: "genre-select", + templateUrl: "genre-select.component.html" +}) +export class GenreSelectComponent implements OnInit { + constructor( + private tmdbService: TheMovieDbService + ) {} + + @Input() public form: FormGroup; + + private _mediaType: string; + @Input() set mediaType(type: string) { + this._mediaType = type; + this.tmdbService.getGenres(this._mediaType).subscribe((res) => { + this.genres = res; + this.filteredKeywords = this.control.valueChanges.pipe( + startWith(''), + map((genre: string | null) => genre ? this._filter(genre) : this.genres.slice())); + }); + + } + get mediaType(): string { + return this._mediaType; + } + public genres: IMovieDbKeyword[] = []; + public control = new FormControl(); + public filteredTags: IMovieDbKeyword[]; + public filteredKeywords: Observable; + + @ViewChild('keywordInput') input: ElementRef; + + async ngOnInit() { + + // this.genres = await this.tmdbService.getGenres(this.mediaType).toPromise(); + + + + } + + remove(word: IMovieDbKeyword): void { + const exisiting = this.form.controls.genres.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.genres.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.genres.value; + if(exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.genres.setValue(exisiting); + this.input.nativeElement.value = ''; + this.control.setValue(null); + } + + private _filter(value: string|IMovieDbKeyword): IMovieDbKeyword[] { + if (typeof value === 'object') { + const filterValue = value.name.toLowerCase(); + return this.genres.filter(g => g.name.toLowerCase().includes(filterValue)); + } else if (typeof value === 'string') { + const filterValue = value.toLowerCase(); + return this.genres.filter(g => g.name.toLowerCase().includes(filterValue)); + } + + return this.genres; + } + + +} diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html new file mode 100644 index 000000000..1ff1874e9 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html @@ -0,0 +1,37 @@ + + + + + Keywords + + + {{word.name}} + cancel + + + + + + {{word.name}} + + + + diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts new file mode 100644 index 000000000..1ea8bcef1 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts @@ -0,0 +1,64 @@ +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { debounceTime, distinctUntilChanged, startWith, switchMap } from "rxjs/operators"; + +import { IMovieDbKeyword } from "../../../interfaces"; +import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { Observable } from "rxjs"; +import { TheMovieDbService } from "../../../services"; + +@Component({ + selector: "keyword-search", + templateUrl: "keyword-search.component.html" +}) +export class KeywordSearchComponent implements OnInit { + constructor( + private tmdbService: TheMovieDbService + ) {} + + @Input() public form: FormGroup; + public control = new FormControl(); + public filteredTags: IMovieDbKeyword[]; + public filteredKeywords: Observable; + + @ViewChild('keywordInput') input: ElementRef; + + ngOnInit(): void { + + this.filteredKeywords = this.control.valueChanges.pipe( + startWith(''), + debounceTime(400), + distinctUntilChanged(), + switchMap(val => { + return this.filter(val || '') + }) + ); + } + + filter(val: string): Observable { + return this.tmdbService.getKeywords(val); + }; + + remove(word: IMovieDbKeyword): void { + const exisiting = this.form.controls.keywords.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.keywords.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.keywords.value; + if (exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.keywords.setValue(exisiting); + this.input.nativeElement.value = ''; + this.control.setValue(null); + } + +} diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index 1189320dd..a76914470 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -1,43 +1,46 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { TranslateModule } from "@ngx-translate/core"; -import { TruncateModule } from "@yellowspot/ng-truncate"; -import { MomentModule } from "ngx-moment"; -import { IssuesReportComponent } from "./issues-report.component"; - -import { SidebarModule } from "primeng/sidebar"; +import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component"; +import { AdvancedSearchDialogComponent } from "./advanced-search-dialog/advanced-search-dialog.component"; +import { CommonModule } from "@angular/common"; +import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; +import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; +import { GenreSelectComponent } from "./components/genre-select/genre-select.component"; import { InputSwitchModule } from "primeng/inputswitch"; - +import { IssuesReportComponent } from "./issues-report.component"; +import { KeywordSearchComponent } from "./components/keyword-search/keyword-search.component"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatButtonModule } from '@angular/material/button'; -import { MatNativeDateModule } from '@angular/material/core'; +import { MatCardModule } from "@angular/material/card"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatExpansionModule } from "@angular/material/expansion"; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from "@angular/material/input"; import { MatListModule } from '@angular/material/list'; +import {MatMenuModule} from '@angular/material/menu'; +import { MatNativeDateModule } from '@angular/material/core'; import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; +import {MatRadioModule} from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSortModule } from '@angular/material/sort'; import { MatStepperModule } from '@angular/material/stepper'; import { MatTableModule } from '@angular/material/table'; -import {MatMenuModule} from '@angular/material/menu'; +import { MatTabsModule } from "@angular/material/tabs"; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTreeModule } from '@angular/material/tree'; - import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatCardModule } from "@angular/material/card"; -import { MatCheckboxModule } from "@angular/material/checkbox"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatExpansionModule } from "@angular/material/expansion"; -import { MatInputModule } from "@angular/material/input"; -import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; -import { MatSlideToggleModule } from "@angular/material/slide-toggle"; -import { MatTabsModule } from "@angular/material/tabs"; -import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; -import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; -import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component"; +import { MomentModule } from "ngx-moment"; +import { NgModule } from "@angular/core"; +import { SidebarModule } from "primeng/sidebar"; +import { TheMovieDbService } from "../services"; +import { TranslateModule } from "@ngx-translate/core"; +import { TruncateModule } from "@yellowspot/ng-truncate"; @NgModule({ declarations: [ @@ -45,6 +48,9 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques EpisodeRequestComponent, DetailsGroupComponent, AdminRequestDialogComponent, + AdvancedSearchDialogComponent, + KeywordSearchComponent, + GenreSelectComponent, ], imports: [ SidebarModule, @@ -59,6 +65,7 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques MatAutocompleteModule, MatInputModule, MatTabsModule, + MatRadioModule, MatButtonModule, MatNativeDateModule, MatChipsModule, @@ -89,6 +96,9 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques IssuesReportComponent, EpisodeRequestComponent, AdminRequestDialogComponent, + AdvancedSearchDialogComponent, + GenreSelectComponent, + KeywordSearchComponent, DetailsGroupComponent, TruncateModule, InputSwitchModule, diff --git a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs index ac4bb4eea..3dacdb406 100644 --- a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs +++ b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; -using Ombi.Attributes; using System.Collections.Generic; using System.Threading.Tasks; @@ -11,10 +11,10 @@ using Genre = Ombi.TheMovieDbApi.Models.Genre; namespace Ombi.Controllers.External { - [Admin] [ApiV1] + [Authorize] [Produces("application/json")] - public sealed class TheMovieDbController : Controller + public sealed class TheMovieDbController : ControllerBase { public TheMovieDbController(IMovieDbApi tmdbApi) => TmdbApi = tmdbApi; @@ -25,7 +25,7 @@ namespace Ombi.Controllers.External /// /// The search term. [HttpGet("Keywords")] - public async Task> GetKeywords([FromQuery]string searchTerm) => + public async Task> GetKeywords([FromQuery]string searchTerm) => await TmdbApi.SearchKeyword(searchTerm); /// @@ -36,15 +36,15 @@ namespace Ombi.Controllers.External public async Task GetKeywords(int keywordId) { var keyword = await TmdbApi.GetKeyword(keywordId); - return keyword == null ? NotFound() : (IActionResult)Ok(keyword); + return keyword == null ? NotFound() : Ok(keyword); } /// /// Gets the genres for either Tv or Movies depending on media type /// - /// Either `tv` or `movie`. + /// Either `tv` or `movie`. [HttpGet("Genres/{media}")] public async Task> GetGenres(string media) => - await TmdbApi.GetGenres(media); + await TmdbApi.GetGenres(media, HttpContext.RequestAborted); } } From 4118f0de413bc770141da2ec5a238fa689c0c317 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 27 Jul 2021 09:38:20 +0100 Subject: [PATCH 4/8] Fixed the discord notification test issue --- src/Ombi.Helpers/CacheService.cs | 17 +++++++++++++---- src/Ombi.Helpers/MediaCacheService.cs | 15 ++++++++++----- src/Ombi/Startup.cs | 1 + 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Ombi.Helpers/CacheService.cs b/src/Ombi.Helpers/CacheService.cs index 3cf392880..c0acf0dea 100644 --- a/src/Ombi.Helpers/CacheService.cs +++ b/src/Ombi.Helpers/CacheService.cs @@ -2,13 +2,14 @@ using System.Threading; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { public class CacheService : ICacheService { - protected readonly IAppCache _memoryCache; - public CacheService(IAppCache memoryCache) + protected readonly IMemoryCache _memoryCache; + public CacheService(IMemoryCache memoryCache) { _memoryCache = memoryCache; } @@ -20,7 +21,11 @@ namespace Ombi.Helpers absoluteExpiration = DateTimeOffset.Now.AddHours(1); } - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } public void Remove(string key) @@ -31,7 +36,11 @@ namespace Ombi.Helpers public T GetOrAdd(string cacheKey, Func factory, DateTimeOffset absoluteExpiration) { // locks get and set internally - return _memoryCache.GetOrAdd(cacheKey, () => factory(), absoluteExpiration); + return _memoryCache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private static class TypeLock diff --git a/src/Ombi.Helpers/MediaCacheService.cs b/src/Ombi.Helpers/MediaCacheService.cs index 1cb969b60..c514c8e25 100644 --- a/src/Ombi.Helpers/MediaCacheService.cs +++ b/src/Ombi.Helpers/MediaCacheService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { @@ -14,7 +15,7 @@ namespace Ombi.Helpers { private const string CacheKey = "MediaCacheServiceKeys"; - public MediaCacheService(IAppCache memoryCache) : base(memoryCache) + public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache) { } @@ -33,24 +34,28 @@ namespace Ombi.Helpers // Not in the cache, so add this Key into our MediaServiceCache await UpdateLocalCache(cacheKey); - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private async Task UpdateLocalCache(string cacheKey) { - var mediaServiceCache = await _memoryCache.GetAsync>(CacheKey); + var mediaServiceCache = _memoryCache.Get>(CacheKey); if (mediaServiceCache == null) { mediaServiceCache = new List(); } mediaServiceCache.Add(cacheKey); _memoryCache.Remove(CacheKey); - _memoryCache.Add(CacheKey, mediaServiceCache); + _memoryCache.Set(CacheKey, mediaServiceCache); } public async Task Purge() { - var keys = await _memoryCache.GetAsync>(CacheKey); + var keys = _memoryCache.Get>(CacheKey); if (keys == null) { return; diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 4b82c3124..95fd6447f 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -84,6 +84,7 @@ namespace Ombi //{ // setup.AddHealthCheckEndpoint("Ombi", "/health"); //}); + services.AddMemoryCache(); services.AddLazyCache(); services.AddHttpClient(); From 6327782c74e572bd15d93b9ae933a41c807267e5 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 27 Jul 2021 09:38:20 +0100 Subject: [PATCH 5/8] Fixed the discord notification test issue --- src/Ombi.Helpers/CacheService.cs | 17 +++++++++++++---- src/Ombi.Helpers/MediaCacheService.cs | 15 ++++++++++----- src/Ombi/Startup.cs | 1 + 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Ombi.Helpers/CacheService.cs b/src/Ombi.Helpers/CacheService.cs index 3cf392880..c0acf0dea 100644 --- a/src/Ombi.Helpers/CacheService.cs +++ b/src/Ombi.Helpers/CacheService.cs @@ -2,13 +2,14 @@ using System.Threading; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { public class CacheService : ICacheService { - protected readonly IAppCache _memoryCache; - public CacheService(IAppCache memoryCache) + protected readonly IMemoryCache _memoryCache; + public CacheService(IMemoryCache memoryCache) { _memoryCache = memoryCache; } @@ -20,7 +21,11 @@ namespace Ombi.Helpers absoluteExpiration = DateTimeOffset.Now.AddHours(1); } - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } public void Remove(string key) @@ -31,7 +36,11 @@ namespace Ombi.Helpers public T GetOrAdd(string cacheKey, Func factory, DateTimeOffset absoluteExpiration) { // locks get and set internally - return _memoryCache.GetOrAdd(cacheKey, () => factory(), absoluteExpiration); + return _memoryCache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private static class TypeLock diff --git a/src/Ombi.Helpers/MediaCacheService.cs b/src/Ombi.Helpers/MediaCacheService.cs index 1cb969b60..c514c8e25 100644 --- a/src/Ombi.Helpers/MediaCacheService.cs +++ b/src/Ombi.Helpers/MediaCacheService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { @@ -14,7 +15,7 @@ namespace Ombi.Helpers { private const string CacheKey = "MediaCacheServiceKeys"; - public MediaCacheService(IAppCache memoryCache) : base(memoryCache) + public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache) { } @@ -33,24 +34,28 @@ namespace Ombi.Helpers // Not in the cache, so add this Key into our MediaServiceCache await UpdateLocalCache(cacheKey); - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private async Task UpdateLocalCache(string cacheKey) { - var mediaServiceCache = await _memoryCache.GetAsync>(CacheKey); + var mediaServiceCache = _memoryCache.Get>(CacheKey); if (mediaServiceCache == null) { mediaServiceCache = new List(); } mediaServiceCache.Add(cacheKey); _memoryCache.Remove(CacheKey); - _memoryCache.Add(CacheKey, mediaServiceCache); + _memoryCache.Set(CacheKey, mediaServiceCache); } public async Task Purge() { - var keys = await _memoryCache.GetAsync>(CacheKey); + var keys = _memoryCache.Get>(CacheKey); if (keys == null) { return; diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 4b82c3124..95fd6447f 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -84,6 +84,7 @@ namespace Ombi //{ // setup.AddHealthCheckEndpoint("Ombi", "/health"); //}); + services.AddMemoryCache(); services.AddLazyCache(); services.AddHttpClient(); From 6ee9606f7cfaccb832e75543db121c557be93f54 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 28 Jul 2021 16:48:45 +0100 Subject: [PATCH 6/8] MORE! --- .../Engine/Interfaces/IMovieEngineV2.cs | 1 + .../Engine/V2/MovieSearchEngineV2.cs | 17 +++- src/Ombi.TheMovieDbApi/IMovieDbApi.cs | 2 + .../Models/DiscoverModel.cs | 18 +++++ src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs | 21 +++++ .../Models/WatchProvidersResults.cs | 16 ++++ src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 39 ++++++++++ .../ClientApp/src/app/interfaces/IMovieDb.ts | 15 ++++ .../src/app/my-nav/my-nav.component.html | 2 +- .../src/app/my-nav/my-nav.component.scss | 4 + .../applications/themoviedb.service.ts | 6 +- .../src/app/services/searchV2.service.ts | 6 +- .../advanced-search-dialog.component.html | 10 ++- .../advanced-search-dialog.component.ts | 21 ++++- .../genre-select/genre-select.component.html | 2 +- .../genre-select/genre-select.component.ts | 18 ++--- .../keyword-search.component.html | 2 +- .../keyword-search.component.ts | 8 +- .../watch-providers-select.component.html | 24 ++++++ .../watch-providers-select.component.ts | 77 +++++++++++++++++++ .../ClientApp/src/app/shared/shared.module.ts | 3 + .../V1/External/TheMovieDbController.cs | 18 +++++ src/Ombi/Controllers/V2/SearchController.cs | 13 ++++ 23 files changed, 314 insertions(+), 29 deletions(-) create mode 100644 src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs create mode 100644 src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs create mode 100644 src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs create mode 100644 src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 5a3624c5e..2d86443d1 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -29,5 +29,6 @@ namespace Ombi.Core.Engine.Interfaces Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); Task> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); Task> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken); + Task> AdvancedSearch(DiscoverModel model, int currentlyLoaded, int toLoad, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index 25c0fc2a2..acc390aec 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -137,7 +137,22 @@ namespace Ombi.Core.Engine.V2 foreach (var pagesToLoad in pages) { var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode, - () => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12)); + () => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12)); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + public async Task> AdvancedSearch(DiscoverModel model, int currentlyLoaded, int toLoad, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + + var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } return await TransformMovieResultsToResponse(results); diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 202392c5f..6d0859cef 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -40,5 +40,7 @@ namespace Ombi.Api.TheMovieDb Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token); Task GetTvWatchProviders(int theMoviedbId, CancellationToken token); Task> GetGenres(string media, CancellationToken cancellationToken); + Task> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken); + Task> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken); } } diff --git a/src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs b/src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs new file mode 100644 index 000000000..53f3bfb21 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.TheMovieDb.Models +{ + public class DiscoverModel + { + public string Type { get; set; } + public int? ReleaseYear { get; set; } + public List GenreIds { get; set; } = new List(); + public List KeywordIds { get; set; } = new List(); + public List WatchProviders { get; set; } = new List(); + public List Companies { get; set; } = new List(); + } +} diff --git a/src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs b/src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs new file mode 100644 index 000000000..3edb7320b --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs @@ -0,0 +1,21 @@ +namespace Ombi.Api.TheMovieDb.Models { + + public class DiscoverTv + { + public int vote_count { get; set; } + public int id { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public string title { get; set; } + public float popularity { get; set; } + public string poster_path { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public int[] genre_ids { get; set; } + public string backdrop_path { get; set; } + public bool adult { get; set; } + public string overview { get; set; } + public string release_date { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs b/src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs new file mode 100644 index 000000000..44462018b --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.TheMovieDb.Models +{ + public class WatchProvidersResults + { + public int provider_id { get; set; } + public string logo_path { get; set; } + public string provider_name { get; set; } + public string origin_country { get; set; } + } +} diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index a8fdc3269..7e89b48fd 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -68,6 +68,34 @@ namespace Ombi.Api.TheMovieDb return await Api.Request>(request); } + + + public async Task> AdvancedSearch(DiscoverModel model, 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"); + + 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 @@ -368,6 +396,17 @@ namespace Ombi.Api.TheMovieDb 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); diff --git a/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts b/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts index 63443ae4c..f82225434 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts @@ -2,3 +2,18 @@ id: number; name: string; } + +export interface IWatchProvidersResults { + provider_id: number; + logo_path: string; + provider_name: string; +} + +export interface IDiscoverModel { + type: string; + releaseYear?: number|undefined; + genreIds?: number[]; + keywordIds?: number[]; + watchProviders?: number[]; + companies?: number[]; +} diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html index 84ae81fe8..940002d9f 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html @@ -78,7 +78,7 @@ {{ 'NavigationBar.Filter.Music' | translate}} - + diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss index 2b018ee8e..4f17a636d 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -230,4 +230,8 @@ ::ng-deep .mat-sidenav-fixed .mat-list-base .mat-list-item .mat-list-item-content, .mat-list-base .mat-list-option .mat-list-item-content{ padding:0; margin: 0 4em 0 0.5em; +} + +.advanced-search { + margin-left: 10px; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts index 0af37c508..a49fb4146 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts @@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core"; import { empty, Observable, throwError } from "rxjs"; import { catchError } from "rxjs/operators"; -import { IMovieDbKeyword } from "../../interfaces"; +import { IMovieDbKeyword, IWatchProvidersResults } from "../../interfaces"; import { ServiceHelpers } from "../service.helpers"; @Injectable({ @@ -28,4 +28,8 @@ export class TheMovieDbService extends ServiceHelpers { public getGenres(media: string): Observable { return this.http.get(`${this.url}/Genres/${media}`, { headers: this.headers }) } + + public getWatchProviders(media: string): Observable { + return this.http.get(`${this.url}/WatchProviders/${media}`, {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts index 99e5afb39..348db936b 100644 --- a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts @@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; -import { IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces"; +import { IDiscoverModel, IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces"; import { ServiceHelpers } from "./service.helpers"; import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2"; @@ -51,6 +51,10 @@ export class SearchV2Service extends ServiceHelpers { return this.http.get(`${this.url}/Movie/Popular/${currentlyLoaded}/${toLoad}`).toPromise(); } + public advancedSearch(model: IDiscoverModel, currentlyLoaded: number, toLoad: number): Promise { + return this.http.post(`${this.url}/advancedSearch/Movie/${currentlyLoaded}/${toLoad}`, model).toPromise(); + } + public upcomingMovies(): Observable { return this.http.get(`${this.url}/Movie/upcoming`); } diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html index 02f9ac35b..67bd1c2d5 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -1,4 +1,4 @@ -
+

Advanced Search

@@ -34,6 +34,10 @@
+ +
+ +
@@ -49,10 +53,10 @@ diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts index e59f7fab7..a06d43b43 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -1,6 +1,8 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, FormGroup } from "@angular/forms"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { IDiscoverModel } from "../../interfaces"; +import { SearchV2Service } from "../../services"; @Component({ @@ -12,7 +14,8 @@ export class AdvancedSearchDialogComponent implements OnInit { constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any, - private fb: FormBuilder + private fb: FormBuilder, + private searchService: SearchV2Service, ) {} public form: FormGroup; @@ -21,16 +24,28 @@ export class AdvancedSearchDialogComponent implements OnInit { public async ngOnInit() { this.form = this.fb.group({ - keywords: [[]], - genres: [[]], + keywordIds: [[]], + genreIds: [[]], releaseYear: [], type: ['movie'], + watchProviders: [[]], }) this.form.controls.type.valueChanges.subscribe(val => { this.form.controls.genres.setValue([]); + this.form.controls.watchProviders.setValue([]); }); } + + public async onSubmit() { + const watchProviderIds = this.form.controls.watchProviders.value.map(x => x.provider_id); + const genres = this.form.controls.genreIds.value.map(x => x.id); + await this.searchService.advancedSearch({ + watchProviders: watchProviderIds, + genreIds: genres, + type: this.form.controls.type.value, + }, 0, 30); + } } diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html index 523974a90..9907b002c 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html @@ -3,7 +3,7 @@ Genres {{word.name}} diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts index c80eb4ae8..06c92e4cf 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts @@ -11,7 +11,7 @@ import { TheMovieDbService } from "../../../services"; selector: "genre-select", templateUrl: "genre-select.component.html" }) -export class GenreSelectComponent implements OnInit { +export class GenreSelectComponent { constructor( private tmdbService: TheMovieDbService ) {} @@ -39,32 +39,24 @@ export class GenreSelectComponent implements OnInit { @ViewChild('keywordInput') input: ElementRef; - async ngOnInit() { - - // this.genres = await this.tmdbService.getGenres(this.mediaType).toPromise(); - - - - } - remove(word: IMovieDbKeyword): void { - const exisiting = this.form.controls.genres.value; + const exisiting = this.form.controls.genreIds.value; const index = exisiting.indexOf(word); if (index >= 0) { exisiting.splice(index, 1); - this.form.controls.genres.setValue(exisiting); + this.form.controls.genreIds.setValue(exisiting); } } selected(event: MatAutocompleteSelectedEvent): void { const val = event.option.value; - const exisiting = this.form.controls.genres.value; + const exisiting = this.form.controls.genreIds.value; if(exisiting.indexOf(val) < 0) { exisiting.push(val); } - this.form.controls.genres.setValue(exisiting); + this.form.controls.genreIds.setValue(exisiting); this.input.nativeElement.value = ''; this.control.setValue(null); } diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html index 1ff1874e9..631c42596 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html @@ -15,7 +15,7 @@ Keywords {{word.name}} diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts index 1ea8bcef1..67427c6e4 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts @@ -40,23 +40,23 @@ export class KeywordSearchComponent implements OnInit { }; remove(word: IMovieDbKeyword): void { - const exisiting = this.form.controls.keywords.value; + const exisiting = this.form.controls.keywordIds.value; const index = exisiting.indexOf(word); if (index >= 0) { exisiting.splice(index, 1); - this.form.controls.keywords.setValue(exisiting); + this.form.controls.keywordIds.setValue(exisiting); } } selected(event: MatAutocompleteSelectedEvent): void { const val = event.option.value; - const exisiting = this.form.controls.keywords.value; + const exisiting = this.form.controls.keywordIds.value; if (exisiting.indexOf(val) < 0) { exisiting.push(val); } - this.form.controls.keywords.setValue(exisiting); + this.form.controls.keywordIds.setValue(exisiting); this.input.nativeElement.value = ''; this.control.setValue(null); } diff --git a/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html new file mode 100644 index 000000000..3c22c7d33 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html @@ -0,0 +1,24 @@ + + Watch Providers + + + {{word.provider_name}} + cancel + + + + + + {{word.provider_name}} + + + + diff --git a/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts new file mode 100644 index 000000000..2c89756e4 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts @@ -0,0 +1,77 @@ +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { IMovieDbKeyword, IWatchProvidersResults } from "../../../interfaces"; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from "rxjs/operators"; + +import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { Observable } from "rxjs"; +import { TheMovieDbService } from "../../../services"; + +@Component({ + selector: "watch-providers-select", + templateUrl: "watch-providers-select.component.html" +}) +export class WatchProvidersSelectComponent { + constructor( + private tmdbService: TheMovieDbService + ) {} + + private _mediaType: string; + @Input() set mediaType(type: string) { + this._mediaType = type; + this.tmdbService.getWatchProviders(this._mediaType).subscribe((res) => { + this.watchProviders = res; + this.filteredList = this.control.valueChanges.pipe( + startWith(''), + map((genre: string | null) => genre ? this._filter(genre) : this.watchProviders.slice())); + }); + + } + get mediaType(): string { + return this._mediaType; + } + @Input() public form: FormGroup; + + public watchProviders: IWatchProvidersResults[] = []; + public control = new FormControl(); + public filteredTags: IWatchProvidersResults[]; + public filteredList: Observable; + + @ViewChild('keywordInput') input: ElementRef; + + + remove(word: IWatchProvidersResults): void { + const exisiting = this.form.controls.watchProviders.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.watchProviders.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.watchProviders.value; + if (exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.watchProviders.setValue(exisiting); + this.input.nativeElement.value = ''; + this.control.setValue(null); + } + + private _filter(value: string|IWatchProvidersResults): IWatchProvidersResults[] { + if (typeof value === 'object') { + const filterValue = value.provider_name.toLowerCase(); + return this.watchProviders.filter(g => g.provider_name.toLowerCase().includes(filterValue)); + } else if (typeof value === 'string') { + const filterValue = value.toLowerCase(); + return this.watchProviders.filter(g => g.provider_name.toLowerCase().includes(filterValue)); + } + + return this.watchProviders; + } + +} diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index a76914470..50dfc4538 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -41,6 +41,7 @@ import { SidebarModule } from "primeng/sidebar"; import { TheMovieDbService } from "../services"; import { TranslateModule } from "@ngx-translate/core"; import { TruncateModule } from "@yellowspot/ng-truncate"; +import { WatchProvidersSelectComponent } from "./components/watch-providers-select/watch-providers-select.component"; @NgModule({ declarations: [ @@ -51,6 +52,7 @@ import { TruncateModule } from "@yellowspot/ng-truncate"; AdvancedSearchDialogComponent, KeywordSearchComponent, GenreSelectComponent, + WatchProvidersSelectComponent, ], imports: [ SidebarModule, @@ -99,6 +101,7 @@ import { TruncateModule } from "@yellowspot/ng-truncate"; AdvancedSearchDialogComponent, GenreSelectComponent, KeywordSearchComponent, + WatchProvidersSelectComponent, DetailsGroupComponent, TruncateModule, InputSwitchModule, diff --git a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs index 3dacdb406..f5fdb996f 100644 --- a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs +++ b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs @@ -46,5 +46,23 @@ namespace Ombi.Controllers.External [HttpGet("Genres/{media}")] public async Task> GetGenres(string media) => await TmdbApi.GetGenres(media, HttpContext.RequestAborted); + + /// + /// Searches for the watch providers matching the specified term. + /// + /// The search term. + [HttpGet("WatchProviders/movie")] + public async Task> GetWatchProvidersMovies([FromQuery] string searchTerm) => + await TmdbApi.SearchWatchProviders("movie", searchTerm, HttpContext.RequestAborted); + + + /// + /// Searches for the watch providers matching the specified term. + /// + /// The search term. + [HttpGet("WatchProviders/tv")] + public async Task> GetWatchProvidersTv([FromQuery] string searchTerm) => + await TmdbApi.SearchWatchProviders("tv", searchTerm, HttpContext.RequestAborted); + } } diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index 4bf8f15da..0b58244b3 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -183,6 +183,19 @@ namespace Ombi.Controllers.V2 DateTimeOffset.Now.AddHours(12)); } + /// + /// Returns Advanced Searched Media using paging + /// + /// We use TheMovieDb as the Movie Provider + /// + [HttpPost("advancedSearch/movie/{currentPosition}/{amountToLoad}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public Task> AdvancedSearchMovie([FromBody]DiscoverModel model, int currentPosition, int amountToLoad) + { + return _movieEngineV2.AdvancedSearch(model, currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + } + /// /// Returns Seasonal Movies /// From 924a562c57f4126345b1a51dfd625136af67a2f6 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 28 Jul 2021 21:40:49 +0100 Subject: [PATCH 7/8] Added the basic advanced search --- .../Engine/V2/MovieSearchEngineV2.cs | 12 +-- src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 2 +- .../src/app/discover/components/index.ts | 17 +++-- .../search-results.component.ts | 76 +++++++++++++++++-- .../src/app/discover/discover.module.ts | 14 ++-- .../src/app/my-nav/my-nav.component.scss | 10 +++ .../src/app/my-nav/my-nav.component.ts | 14 +++- .../advanced-search-dialog-data.service.ts | 27 +++++++ .../advanced-search-dialog.component.html | 30 +++++--- .../advanced-search-dialog.component.ts | 32 +++++--- .../genre-select/genre-select.component.html | 2 +- .../keyword-search.component.html | 15 +--- .../watch-providers-select.component.html | 4 +- src/Ombi/wwwroot/translations/en.json | 2 + 14 files changed, 185 insertions(+), 72 deletions(-) create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index acc390aec..e865c6465 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -147,15 +147,15 @@ namespace Ombi.Core.Engine.V2 { var langCode = await DefaultLanguageCode(null); - var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); + //var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); var results = new List(); - foreach (var pagesToLoad in pages) - { + //foreach (var pagesToLoad in pages) + //{ var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken); - results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); - } - return await TransformMovieResultsToResponse(results); + //results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + //} + return await TransformMovieResultsToResponse(apiResult); } /// diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 7e89b48fd..aa46b0c8e 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -90,7 +90,7 @@ namespace Ombi.Api.TheMovieDb { request.FullUri = request.FullUri.AddQueryParameter("with_watch_providers", string.Join(',', model.WatchProviders)); } - request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); + //request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); var result = await Api.Request>(request, cancellationToken); return Mapper.Map>(result.results); diff --git a/src/Ombi/ClientApp/src/app/discover/components/index.ts b/src/Ombi/ClientApp/src/app/discover/components/index.ts index 2d399cd76..835ef702c 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/index.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/index.ts @@ -1,15 +1,15 @@ -import { DiscoverComponent } from "./discover/discover.component"; -import { DiscoverCollectionsComponent } from "./collections/discover-collections.component"; +import { RadarrService, RequestService, SearchService, SonarrService } from "../../services"; + +import { AuthGuard } from "../../auth/auth.guard"; +import { CarouselListComponent } from "./carousel-list/carousel-list.component"; import { DiscoverActorComponent } from "./actor/discover-actor.component"; import { DiscoverCardComponent } from "./card/discover-card.component"; -import { Routes } from "@angular/router"; -import { AuthGuard } from "../../auth/auth.guard"; -import { SearchService, RequestService, SonarrService, RadarrService } from "../../services"; -import { MatDialog } from "@angular/material/dialog"; +import { DiscoverCollectionsComponent } from "./collections/discover-collections.component"; +import { DiscoverComponent } from "./discover/discover.component"; import { DiscoverSearchResultsComponent } from "./search-results/search-results.component"; -import { CarouselListComponent } from "./carousel-list/carousel-list.component"; +import { MatDialog } from "@angular/material/dialog"; import { RequestServiceV2 } from "../../services/requestV2.service"; - +import { Routes } from "@angular/router"; export const components: any[] = [ DiscoverComponent, @@ -34,4 +34,5 @@ export const routes: Routes = [ { path: "collection/:collectionId", component: DiscoverCollectionsComponent, canActivate: [AuthGuard] }, { path: "actor/:actorId", component: DiscoverActorComponent, canActivate: [AuthGuard] }, { path: ":searchTerm", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] }, + { path: "advanced/search", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] }, ]; \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts index d9e046da9..02415a970 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts @@ -1,14 +1,15 @@ +import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { SearchV2Service } from "../../../services"; -import { IDiscoverCardResult } from "../../interfaces"; -import { IMultiSearchResult, RequestType } from "../../../interfaces"; +import { IMultiSearchResult, ISearchMovieResult, RequestType } from "../../../interfaces"; + +import { AdvancedSearchDialogDataService } from "../../../shared/advanced-search-dialog/advanced-search-dialog-data.service"; +import { AuthService } from "../../../auth/auth.service"; import { FilterService } from "../../services/filter-service"; +import { IDiscoverCardResult } from "../../interfaces"; import { SearchFilter } from "../../../my-nav/SearchFilter"; +import { SearchV2Service } from "../../../services"; import { StorageService } from "../../../shared/storage/storage-service"; - import { isEqual } from "lodash"; -import { AuthService } from "../../../auth/auth.service"; @Component({ templateUrl: "./search-results.component.html", @@ -25,22 +26,41 @@ export class DiscoverSearchResultsComponent implements OnInit { public filter: SearchFilter; + private isAdvancedSearch: boolean; + constructor(private searchService: SearchV2Service, private route: ActivatedRoute, private filterService: FilterService, + private router: Router, + private advancedDataService: AdvancedSearchDialogDataService, private store: StorageService, private authService: AuthService) { this.route.params.subscribe((params: any) => { + this.isAdvancedSearch = this.router.url === '/discover/advanced/search'; + if (this.isAdvancedSearch) { + this.loadAdvancedData(); + return; + } this.searchTerm = params.searchTerm; this.clear(); this.init(); }); + + this.advancedDataService.onDataChange.subscribe(() => { + this.clear(); + this.loadAdvancedData(); + }); + } public async ngOnInit() { - this.loadingFlag = true; this.isAdmin = this.authService.isAdmin(); + if (this.advancedDataService) { + return; + } + this.loadingFlag = true; + this.filterService.onFilterChange.subscribe(async x => { if (!isEqual(this.filter, x)) { this.filter = { ...x }; @@ -115,6 +135,48 @@ export class DiscoverSearchResultsComponent implements OnInit { this.discoverResults = []; } + private loadAdvancedData() { + const advancedData = this.advancedDataService.getData(); + this.mapAdvancedData(advancedData); + return; + } + + public mapAdvancedData(advancedData: ISearchMovieResult[]) { + this.finishLoading(); + const type = this.advancedDataService.getType(); + advancedData.forEach(m => { + + let mediaType = type; + + let poster = `https://image.tmdb.org/t/p/w300/${m.posterPath}`; + if (!m.posterPath) { + if (mediaType === RequestType.movie) { + poster = "images/default_movie_poster.png" + } + if (mediaType === RequestType.tvShow) { + poster = "images/default_tv_poster.png" + } + } + + this.discoverResults.push({ + posterPath: poster, + requested: false, + title: m.title, + type: mediaType, + id: m.id, + url: "", + rating: 0, + overview: m.overview, + approved: false, + imdbid: "", + denied: false, + background: "", + available: false, + tvMovieDb: false + }); + }); + } + private async search() { this.clear(); this.results = await this.searchService diff --git a/src/Ombi/ClientApp/src/app/discover/discover.module.ts b/src/Ombi/ClientApp/src/app/discover/discover.module.ts index 2514f0636..414c881e6 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.module.ts +++ b/src/Ombi/ClientApp/src/app/discover/discover.module.ts @@ -1,16 +1,14 @@ -import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; +import * as fromComponents from './components'; + +import { CarouselModule } from 'primeng/carousel'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; - -import { SharedModule } from "../shared/shared.module"; +import { NgModule } from "@angular/core"; import { PipeModule } from "../pipes/pipe.module"; -import { CarouselModule } from 'primeng/carousel'; +import { RouterModule } from "@angular/router"; +import { SharedModule } from "../shared/shared.module"; import { SkeletonModule } from 'primeng/skeleton'; -import * as fromComponents from './components'; - - @NgModule({ imports: [ RouterModule.forChild(fromComponents.routes), diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss index 4f17a636d..40406fe05 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -234,4 +234,14 @@ .advanced-search { margin-left: 10px; +} + +::ng-deep .dialog-responsive { + width: 40%; +} + +@media only screen and (max-width: 760px) { + ::ng-deep .dialog-responsive { + width: 100%; + } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts index 393d12fd4..10c2b5998 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts @@ -11,6 +11,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { Md5 } from 'ts-md5/dist/md5'; import { Observable } from 'rxjs'; +import { Router } from '@angular/router'; import { SearchFilter } from './SearchFilter'; import { StorageService } from '../shared/storage/storage-service'; import { map } from 'rxjs/operators'; @@ -57,7 +58,8 @@ export class MyNavComponent implements OnInit { private store: StorageService, private filterService: FilterService, private dialogService: MatDialog, - private readonly settingState: SettingsStateService) { + private readonly settingState: SettingsStateService, + private router: Router) { } public async ngOnInit() { @@ -125,7 +127,15 @@ export class MyNavComponent implements OnInit { } public openAdvancedSearch() { - this.dialogService.open(AdvancedSearchDialogComponent, null); + const dialogRef = this.dialogService.open(AdvancedSearchDialogComponent, { panelClass: 'dialog-responsive' }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.router.navigate([`discover/advanced/search`]); + } + + return; + }); } public getUserImage(): string { diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts new file mode 100644 index 000000000..c6312915a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts @@ -0,0 +1,27 @@ +import { EventEmitter, Injectable, Output } from "@angular/core"; + +import { RequestType } from "../../interfaces"; + +@Injectable({ + providedIn: "root" +}) +export class AdvancedSearchDialogDataService { + + @Output() public onDataChange = new EventEmitter(); + private _data: any; + private _type: RequestType; + + setData(data: any, type: RequestType) { + this._data = data; + this._type = type; + this.onDataChange.emit(this._data); + } + + getData(): any { + return this._data; + } + + getType(): RequestType { + return this._type; + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html index 67bd1c2d5..4c8e58821 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -4,8 +4,8 @@
@@ -13,7 +13,10 @@
-
+
+ Please choose what type of media you are searching for: +
+
Movies @@ -21,23 +24,26 @@
-
- - Year +
+ + Year of Release
-
- -
-
+ +
- -
+ +
+
+ + Please note that Keyword Searching is very hit and miss due to the inconsistent data in TheMovieDb + +
diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts index a06d43b43..614d2d103 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -1,9 +1,9 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, FormGroup } from "@angular/forms"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; -import { IDiscoverModel } from "../../interfaces"; +import { RequestType } from "../../interfaces"; import { SearchV2Service } from "../../services"; - +import { AdvancedSearchDialogDataService } from "./advanced-search-dialog-data.service"; @Component({ selector: "advanced-search-dialog", @@ -12,15 +12,14 @@ import { SearchV2Service } from "../../services"; }) export class AdvancedSearchDialogComponent implements OnInit { constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, private fb: FormBuilder, private searchService: SearchV2Service, + private advancedSearchDialogService: AdvancedSearchDialogDataService ) {} public form: FormGroup; - public async ngOnInit() { this.form = this.fb.group({ @@ -35,17 +34,28 @@ export class AdvancedSearchDialogComponent implements OnInit { this.form.controls.genres.setValue([]); this.form.controls.watchProviders.setValue([]); }); - - } public async onSubmit() { - const watchProviderIds = this.form.controls.watchProviders.value.map(x => x.provider_id); - const genres = this.form.controls.genreIds.value.map(x => x.id); - await this.searchService.advancedSearch({ + const formData = this.form.value; + const watchProviderIds = formData.watchProviders.map(x => x.provider_id); + const genres = formData.genreIds.map(x => x.id); + const keywords = formData.keywordIds.map(x => x.id); + const data = await this.searchService.advancedSearch({ watchProviders: watchProviderIds, genreIds: genres, - type: this.form.controls.type.value, + keywordIds: keywords, + releaseYear: formData.releaseYear, + type: formData.type, }, 0, 30); + + this.advancedSearchDialogService.setData(data, formData.type === 'movie' ? RequestType.movie : RequestType.tvShow); + + this.dialogRef.close(true); + } + + public onClose() { + this.dialogRef.close(false); } + } diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html index 9907b002c..93ed4618e 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html @@ -1,5 +1,5 @@ - + Genres - - - - {{option.name}} - - - --> - - - + Keywords - Watch Providers + + Watch Providers Date: Thu, 29 Jul 2021 11:18:14 +0100 Subject: [PATCH 8/8] Small fix around the detection of available Jellyfin movies --- .../Rule/Rules/Search/JellyfinAvailabilityRule.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs index 0447458d9..c51645112 100644 --- a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs @@ -28,6 +28,7 @@ namespace Ombi.Core.Rule.Rules.Search var useImdb = false; var useTheMovieDb = false; var useTvDb = false; + var useId = false; if (obj.ImdbId.HasValue()) { @@ -39,6 +40,14 @@ namespace Ombi.Core.Rule.Rules.Search } if (item == null) { + if (obj.Id > 0) + { + item = await JellyfinContentRepository.GetByTheMovieDbId(obj.Id.ToString()); + if (item != null) + { + useId = true; + } + } if (obj.TheMovieDbId.HasValue()) { item = await JellyfinContentRepository.GetByTheMovieDbId(obj.TheMovieDbId); @@ -63,6 +72,11 @@ namespace Ombi.Core.Rule.Rules.Search if (item != null) { + if (useId) + { + obj.TheMovieDbId = obj.Id.ToString(); + useTheMovieDb = true; + } obj.Available = true; var s = await JellyfinSettings.GetSettingsAsync(); if (s.Enable)