Merge pull request #4274 from Ombi-app/advanced-search

Added the basics of advanced discovery
pull/4301/head^2 v4.0.1458
Jamie 3 years ago committed by GitHub
commit 7f2c5a5f3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,5 +29,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> AdvancedSearch(DiscoverModel model, int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
}
}

@ -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<IEnumerable<SearchMovieViewModel>> 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<MovieDbSearchResult>();
//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);
}
/// <summary>
/// Gets top rated movies.
/// </summary>

@ -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)

@ -26,7 +26,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieDbSearchResult>> UpcomingTv(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> SimilarMovies(int movieId, string langCode);
Task<FindResult> Find(string externalId, ExternalSource source);
Task<TvExternals> GetTvExternals(int theMovieDbId);
Task<TvExternals> GetTvExternals(int theMovieDbId);
Task<SeasonDetails> GetSeasonEpisodes(int theMovieDbId, int seasonNumber, CancellationToken token, string langCode = "en");
Task<TvInfo> GetTVInfo(string themoviedbid, string langCode = "en");
Task<TheMovieDbContainer<ActorResult>> SearchByActor(string searchTerm, string langCode);
@ -35,10 +35,12 @@ namespace Ombi.Api.TheMovieDb
Task<TheMovieDbContainer<DiscoverMovies>> DiscoverMovies(string langCode, int keywordId);
Task<FullMovieInfo> GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode);
Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken);
Task<List<Keyword>> SearchKeyword(string searchTerm);
Task<Keyword> GetKeyword(int keywordId);
Task<List<TheMovidDbKeyValue>> SearchKeyword(string searchTerm);
Task<TheMovidDbKeyValue> GetKeyword(int keywordId);
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
Task<List<Genre>> GetGenres(string media);
Task<List<Genre>> GetGenres(string media, CancellationToken cancellationToken);
Task<List<WatchProvidersResults>> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken);
Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken);
}
}

@ -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<int> GenreIds { get; set; } = new List<int>();
public List<int> KeywordIds { get; set; } = new List<int>();
public List<int> WatchProviders { get; set; } = new List<int>();
public List<int> Companies { get; set; } = new List<int>();
}
}

@ -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; }
}
}

@ -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; }

@ -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; }
}
}

@ -68,6 +68,34 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<TheMovieDbContainer<DiscoverMovies>>(request);
}
public async Task<List<MovieDbSearchResult>> 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<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
public async Task<Collections> 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<List<MovieDbSearchResult>>(result.results);
}
public async Task<List<Keyword>> SearchKeyword(string searchTerm)
public async Task<List<TheMovidDbKeyValue>> 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<TheMovieDbContainer<Keyword>>(request);
return result.results ?? new List<Keyword>();
var result = await Api.Request<TheMovieDbContainer<TheMovidDbKeyValue>>(request);
return result.results ?? new List<TheMovidDbKeyValue>();
}
public async Task<List<WatchProvidersResults>> 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<TheMovieDbContainer<WatchProvidersResults>>(request, cancellationToken);
return result.results ?? new List<WatchProvidersResults>();
}
public async Task<Keyword> GetKeyword(int keywordId)
public async Task<TheMovidDbKeyValue> GetKeyword(int keywordId)
{
var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
AddRetry(request);
var keyword = await Api.Request<Keyword>(request);
var keyword = await Api.Request<TheMovidDbKeyValue>(request);
return keyword == null || keyword.Id == 0 ? null : keyword;
}
public async Task<List<Genre>> GetGenres(string media)
public async Task<List<Genre>> 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<GenreContainer<Genre>>(request);
var result = await Api.Request<GenreContainer<Genre>>(request, cancellationToken);
return result.genres ?? new List<Genre>();
}

@ -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,

@ -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] },
];

@ -1,4 +1,5 @@
<div class="small-middle-container" >
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner>
</div>

@ -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

@ -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),

@ -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[];
}

@ -78,6 +78,7 @@
<mat-slide-toggle id="filterMusic" class="mat-menu-item slide-menu" [checked]="searchFilter.music"
(click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.Music)">
{{ 'NavigationBar.Filter.Music' | translate}}</mat-slide-toggle>
<button class="advanced-search" mat-raised-button color="accent" (click)="openAdvancedSearch()">Advanced Search</button>
<!-- <mat-slide-toggle class="mat-menu-item slide-menu" [checked]="searchFilter.people"
(click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.People)">
{{ 'NavigationBar.Filter.People' | translate}}</mat-slide-toggle> -->

@ -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%;
}
}

@ -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}`;

@ -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<IMovieDbKeyword[]> {
return this.http.get<IMovieDbKeyword[]>(`${this.url}/Genres/${media}`, { headers: this.headers })
}
public getWatchProviders(media: string): Observable<IWatchProvidersResults[]> {
return this.http.get<IWatchProvidersResults[]>(`${this.url}/WatchProviders/${media}`, {headers: this.headers});
}
}

@ -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<ISearchMovieResult[]>(`${this.url}/Movie/Popular/${currentlyLoaded}/${toLoad}`).toPromise();
}
public advancedSearch(model: IDiscoverModel, currentlyLoaded: number, toLoad: number): Promise<ISearchMovieResult[]> {
return this.http.post<ISearchMovieResult[]>(`${this.url}/advancedSearch/Movie/${currentlyLoaded}/${toLoad}`, model).toPromise();
}
public upcomingMovies(): Observable<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/upcoming`);
}

@ -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<HTMLInputElement>;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private tmdbService: TheMovieDbService,

@ -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<any>();
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;
}
}

@ -0,0 +1,68 @@
<form [formGroup]="form" (ngSubmit)="onSubmit()" *ngIf="form">
<h1 id="advancedOptionsTitle">
<i class="fas fa-sliders-h"></i> Advanced Search
</h1>
<hr />
<div class="alert alert-info" role="alert">
<i class="fas fa-x7 fa-search glyphicon"></i>
<span>{{ "Search.AdvancedSearch" | translate }}</span>
</div>
<div style="max-width: 0; max-height: 0; overflow: hidden">
<input autofocus="true" />
</div>
<div class="row">
<div style="margin: 2%;">
<span>Please choose what type of media you are searching for:</span>
</div>
<div class="col-md-12">
<div class="md-form-field">
<mat-radio-group formControlName="type" aria-label="Select an option">
<mat-radio-button value="movie">Movies </mat-radio-button>
<mat-radio-button style="padding-left: 5px;" value="tv">TV Shows </mat-radio-button>
</mat-radio-group>
</div>
</div>
<div class="col-md-12" style="margin-top:1%">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Year of Release</mat-label>
<input matInput id="releaseYear" name="releaseYear" formControlName="releaseYear">
</mat-form-field>
</div>
<div class="col-md-12">
<genre-select [form]="form" [mediaType]="form.controls.type.value"></genre-select>
</div>
<div class="col-md-12">
<watch-providers-select [form]="form" [mediaType]="form.controls.type.value"></watch-providers-select>
</div>
<div class="col-md-12">
<span style="margin: 1%;">Please note that Keyword Searching is very hit and miss due to the inconsistent data in TheMovieDb</span>
<keyword-search [form]="form"></keyword-search>
</div>
</div>
<div mat-dialog-actions class="right-buttons">
<button
mat-raised-button
id="cancelButton"
[mat-dialog-close]=""
color="warn"
>
<i class="fas fa-times"></i> {{ "Common.Cancel" | translate }}
</button>
<button
mat-raised-button
id="requestButton"
color="accent"
type="submit"
>
<i class="fas fa-plus"></i> {{ "Common.Search" | translate }}
</button>
</div>
</form>

@ -0,0 +1,8 @@
@import "~styles/variables.scss";
.alert-info {
background: $accent;
border-color: $ombi-background-primary;
color:white;
}

@ -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<AdvancedSearchDialogComponent, boolean>,
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 = <number[]>formData.watchProviders.map(x => x.provider_id);
const genres = <number[]>formData.genreIds.map(x => x.id);
const keywords = <number[]>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);
}
}

@ -0,0 +1,25 @@
<mat-form-field appearance="outline" floatLabel=auto class="example-chip-list">
<mat-label>Genres</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.genreIds.value"
[removable]="true"
(removed)="remove(word)">
{{word.name}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Search Keyword"
#keywordInput
[formControl]="control"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"/>
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let word of filteredKeywords | async" [value]="word">
{{word.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

@ -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<IMovieDbKeyword[]>;
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
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;
}
}

@ -0,0 +1,24 @@
<mat-form-field class="example-chip-list" appearance="outline" floatLabel=auto>
<mat-label>Keywords</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.keywordIds.value"
[removable]="true"
(removed)="remove(word)">
{{word.name}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Search Keyword"
#keywordInput
[formControl]="control"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"/>
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let word of filteredKeywords | async" [value]="word">
{{word.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

@ -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<IMovieDbKeyword[]>;
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
ngOnInit(): void {
this.filteredKeywords = this.control.valueChanges.pipe(
startWith(''),
debounceTime(400),
distinctUntilChanged(),
switchMap(val => {
return this.filter(val || '')
})
);
}
filter(val: string): Observable<any[]> {
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);
}
}

@ -0,0 +1,24 @@
<mat-form-field class="example-chip-list" appearance="outline" floatLabel=auto>
<mat-label>Watch Providers</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.watchProviders.value"
[removable]="true"
(removed)="remove(word)">
{{word.provider_name}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Search Keyword"
#keywordInput
[formControl]="control"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"/>
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let word of filteredList | async" [value]="word">
{{word.provider_name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

@ -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<IWatchProvidersResults[]>;
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
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;
}
}

@ -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,

@ -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
/// </summary>
/// <param name="searchTerm">The search term.</param>
[HttpGet("Keywords")]
public async Task<IEnumerable<Keyword>> GetKeywords([FromQuery]string searchTerm) =>
public async Task<IEnumerable<TheMovidDbKeyValue>> GetKeywords([FromQuery]string searchTerm) =>
await TmdbApi.SearchKeyword(searchTerm);
/// <summary>
@ -36,15 +36,33 @@ namespace Ombi.Controllers.External
public async Task<IActionResult> GetKeywords(int keywordId)
{
var keyword = await TmdbApi.GetKeyword(keywordId);
return keyword == null ? NotFound() : (IActionResult)Ok(keyword);
return keyword == null ? NotFound() : Ok(keyword);
}
/// <summary>
/// Gets the genres for either Tv or Movies depending on media type
/// </summary>
/// <param name="media">Either `tv` or `movie`.</param>
/// <param name="media">Either `tv` or `movie`.</param>
[HttpGet("Genres/{media}")]
public async Task<IEnumerable<Genre>> GetGenres(string media) =>
await TmdbApi.GetGenres(media);
await TmdbApi.GetGenres(media, HttpContext.RequestAborted);
/// <summary>
/// Searches for the watch providers matching the specified term.
/// </summary>
/// <param name="searchTerm">The search term.</param>
[HttpGet("WatchProviders/movie")]
public async Task<IEnumerable<WatchProvidersResults>> GetWatchProvidersMovies([FromQuery] string searchTerm) =>
await TmdbApi.SearchWatchProviders("movie", searchTerm, HttpContext.RequestAborted);
/// <summary>
/// Searches for the watch providers matching the specified term.
/// </summary>
/// <param name="searchTerm">The search term.</param>
[HttpGet("WatchProviders/tv")]
public async Task<IEnumerable<WatchProvidersResults>> GetWatchProvidersTv([FromQuery] string searchTerm) =>
await TmdbApi.SearchWatchProviders("tv", searchTerm, HttpContext.RequestAborted);
}
}

@ -183,6 +183,19 @@ namespace Ombi.Controllers.V2
DateTimeOffset.Now.AddHours(12));
}
/// <summary>
/// Returns Advanced Searched Media using paging
/// </summary>
/// <remarks>We use TheMovieDb as the Movie Provider</remarks>
/// <returns></returns>
[HttpPost("advancedSearch/movie/{currentPosition}/{amountToLoad}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public Task<IEnumerable<SearchMovieViewModel>> AdvancedSearchMovie([FromBody]DiscoverModel model, int currentPosition, int amountToLoad)
{
return _movieEngineV2.AdvancedSearch(model, currentPosition, amountToLoad, Request.HttpContext.RequestAborted);
}
/// <summary>
/// Returns Seasonal Movies
/// </summary>

@ -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}}",

Loading…
Cancel
Save