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..e865c6465 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -137,12 +137,27 @@ 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(apiResult); + } + /// /// Gets top rated movies. /// 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) diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 7a0f7e385..6d0859cef 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,12 @@ 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); + 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/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/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 70e4a94c7..aa46b0c8e 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 @@ -357,34 +385,45 @@ 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> 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) + 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/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.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/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/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 95f31198b..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,6 +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..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 @@ -230,4 +230,18 @@ ::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; +} + +::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 57bfaca91..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 @@ -3,12 +3,15 @@ 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'; +import { Router } from '@angular/router'; import { SearchFilter } from './SearchFilter'; import { StorageService } from '../shared/storage/storage-service'; import { map } from 'rxjs/operators'; @@ -54,7 +57,9 @@ export class MyNavComponent implements OnInit { private settingsService: SettingsService, private store: StorageService, private filterService: FilterService, - private readonly settingState: SettingsStateService) { + private dialogService: MatDialog, + private readonly settingState: SettingsStateService, + private router: Router) { } public async ngOnInit() { @@ -121,6 +126,18 @@ export class MyNavComponent implements OnInit { this.store.save("searchFilter", JSON.stringify(this.searchFilter)); } + public openAdvancedSearch() { + 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 { 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..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,10 +4,12 @@ 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() +@Injectable({ + providedIn: 'root', + }) export class TheMovieDbService extends ServiceHelpers { constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { super(http, "/api/v1/TheMovieDb", href); @@ -26,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/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-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 new file mode 100644 index 000000000..4c8e58821 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -0,0 +1,68 @@ +
+

+ Advanced Search +

+
+ + +
+ +
+ +
+
+ Please choose what type of media you are searching for: +
+
+
+ + Movies + TV Shows + +
+
+
+ + 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.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..614d2d103 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -0,0 +1,61 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { RequestType } from "../../interfaces"; +import { SearchV2Service } from "../../services"; +import { AdvancedSearchDialogDataService } from "./advanced-search-dialog-data.service"; + +@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, + private fb: FormBuilder, + private searchService: SearchV2Service, + private advancedSearchDialogService: AdvancedSearchDialogDataService + ) {} + + public form: FormGroup; + + public async ngOnInit() { + + this.form = this.fb.group({ + 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 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, + 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 new file mode 100644 index 000000000..93ed4618e --- /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..06c92e4cf --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts @@ -0,0 +1,77 @@ +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 { + 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; + + remove(word: IMovieDbKeyword): void { + const exisiting = this.form.controls.genreIds.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.genreIds.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.genreIds.value; + if(exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.genreIds.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..0e6aa5fc3 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html @@ -0,0 +1,24 @@ + + 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..67427c6e4 --- /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.keywordIds.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.keywordIds.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.keywordIds.value; + if (exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + 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..93d23855a --- /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 1189320dd..50dfc4538 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -1,43 +1,47 @@ -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"; +import { WatchProvidersSelectComponent } from "./components/watch-providers-select/watch-providers-select.component"; @NgModule({ declarations: [ @@ -45,6 +49,10 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques EpisodeRequestComponent, DetailsGroupComponent, AdminRequestDialogComponent, + AdvancedSearchDialogComponent, + KeywordSearchComponent, + GenreSelectComponent, + WatchProvidersSelectComponent, ], imports: [ SidebarModule, @@ -59,6 +67,7 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques MatAutocompleteModule, MatInputModule, MatTabsModule, + MatRadioModule, MatButtonModule, MatNativeDateModule, MatChipsModule, @@ -89,6 +98,10 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques IssuesReportComponent, EpisodeRequestComponent, AdminRequestDialogComponent, + 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 ac4bb4eea..f5fdb996f 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,33 @@ 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); + + /// + /// 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 /// diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index b497c784e..03ad19b47 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -22,6 +22,7 @@ "RequestDenied": "Request Denied", "NotRequested": "Not Requested", "Requested": "Requested", + "Search":"Search", "Request": "Request", "Denied": "Denied", "Approve": "Approve", @@ -88,6 +89,7 @@ "MoviesTab": "Movies", "TvTab": "TV Shows", "MusicTab": "Music", + "AdvancedSearch":"You can fill in any of the below to discover new media. All of the results are sorted by popularity", "Suggestions": "Suggestions", "NoResults": "Sorry, we didn't find any results!", "DigitalDate": "Digital Release: {{date}}",