pull/4274/head
tidusjar 3 years ago
parent 4118f0de41
commit 6ee9606f7c

@ -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,7 +137,22 @@ namespace Ombi.Core.Engine.V2
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode,
() => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12));
() => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
}
public async Task<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(results);

@ -40,5 +40,7 @@ namespace Ombi.Api.TheMovieDb
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
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; }
}
}

@ -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
@ -368,6 +396,17 @@ namespace Ombi.Api.TheMovieDb
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<TheMovidDbKeyValue> GetKeyword(int keywordId)
{
var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get);

@ -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,7 +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 (click)="openAdvancedSearch()">Advanced Search</button>
<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,8 @@
::ng-deep .mat-sidenav-fixed .mat-list-base .mat-list-item .mat-list-item-content, .mat-list-base .mat-list-option .mat-list-item-content{
padding:0;
margin: 0 4em 0 0.5em;
}
.advanced-search {
margin-left: 10px;
}

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { empty, Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
import { IMovieDbKeyword } from "../../interfaces";
import { IMovieDbKeyword, IWatchProvidersResults } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers";
@Injectable({
@ -28,4 +28,8 @@ export class TheMovieDbService extends ServiceHelpers {
public getGenres(media: string): Observable<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,4 +1,4 @@
<form [formGroup]="form" *ngIf="form">
<form [formGroup]="form" (ngSubmit)="onSubmit()" *ngIf="form">
<h1 id="advancedOptionsTitle">
<i class="fas fa-sliders-h"></i> Advanced Search
</h1>
@ -34,6 +34,10 @@
<div class="col-md-6">
<genre-select [form]="form" [mediaType]="form.controls.type.value"></genre-select>
</div>
<div class="col-md-6">
<watch-providers-select [form]="form" [mediaType]="form.controls.type.value"></watch-providers-select>
</div>
</div>
@ -49,10 +53,10 @@
<button
mat-raised-button
id="requestButton"
(click)="submitRequest()"
color="accent"
type="submit"
>
<i class="fas fa-plus"></i> {{ "Common.Request" | translate }}
<i class="fas fa-plus"></i> {{ "Common.Search" | translate }}
</button>
</div>
</form>

@ -1,6 +1,8 @@
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { IDiscoverModel } from "../../interfaces";
import { SearchV2Service } from "../../services";
@Component({
@ -12,7 +14,8 @@ export class AdvancedSearchDialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<AdvancedSearchDialogComponent, string>,
@Inject(MAT_DIALOG_DATA) public data: any,
private fb: FormBuilder
private fb: FormBuilder,
private searchService: SearchV2Service,
) {}
public form: FormGroup;
@ -21,16 +24,28 @@ export class AdvancedSearchDialogComponent implements OnInit {
public async ngOnInit() {
this.form = this.fb.group({
keywords: [[]],
genres: [[]],
keywordIds: [[]],
genreIds: [[]],
releaseYear: [],
type: ['movie'],
watchProviders: [[]],
})
this.form.controls.type.valueChanges.subscribe(val => {
this.form.controls.genres.setValue([]);
this.form.controls.watchProviders.setValue([]);
});
}
public async onSubmit() {
const watchProviderIds = <number[]>this.form.controls.watchProviders.value.map(x => x.provider_id);
const genres = <number[]>this.form.controls.genreIds.value.map(x => x.id);
await this.searchService.advancedSearch({
watchProviders: watchProviderIds,
genreIds: genres,
type: this.form.controls.type.value,
}, 0, 30);
}
}

@ -3,7 +3,7 @@
<mat-label>Genres</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.genres.value"
*ngFor="let word of form.controls.genreIds.value"
[removable]="true"
(removed)="remove(word)">
{{word.name}}

@ -11,7 +11,7 @@ import { TheMovieDbService } from "../../../services";
selector: "genre-select",
templateUrl: "genre-select.component.html"
})
export class GenreSelectComponent implements OnInit {
export class GenreSelectComponent {
constructor(
private tmdbService: TheMovieDbService
) {}
@ -39,32 +39,24 @@ export class GenreSelectComponent implements OnInit {
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
async ngOnInit() {
// this.genres = await this.tmdbService.getGenres(this.mediaType).toPromise();
}
remove(word: IMovieDbKeyword): void {
const exisiting = this.form.controls.genres.value;
const exisiting = this.form.controls.genreIds.value;
const index = exisiting.indexOf(word);
if (index >= 0) {
exisiting.splice(index, 1);
this.form.controls.genres.setValue(exisiting);
this.form.controls.genreIds.setValue(exisiting);
}
}
selected(event: MatAutocompleteSelectedEvent): void {
const val = event.option.value;
const exisiting = this.form.controls.genres.value;
const exisiting = this.form.controls.genreIds.value;
if(exisiting.indexOf(val) < 0) {
exisiting.push(val);
}
this.form.controls.genres.setValue(exisiting);
this.form.controls.genreIds.setValue(exisiting);
this.input.nativeElement.value = '';
this.control.setValue(null);
}

@ -15,7 +15,7 @@
<mat-label>Keywords</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.keywords.value"
*ngFor="let word of form.controls.keywordIds.value"
[removable]="true"
(removed)="remove(word)">
{{word.name}}

@ -40,23 +40,23 @@ export class KeywordSearchComponent implements OnInit {
};
remove(word: IMovieDbKeyword): void {
const exisiting = this.form.controls.keywords.value;
const exisiting = this.form.controls.keywordIds.value;
const index = exisiting.indexOf(word);
if (index >= 0) {
exisiting.splice(index, 1);
this.form.controls.keywords.setValue(exisiting);
this.form.controls.keywordIds.setValue(exisiting);
}
}
selected(event: MatAutocompleteSelectedEvent): void {
const val = event.option.value;
const exisiting = this.form.controls.keywords.value;
const exisiting = this.form.controls.keywordIds.value;
if (exisiting.indexOf(val) < 0) {
exisiting.push(val);
}
this.form.controls.keywords.setValue(exisiting);
this.form.controls.keywordIds.setValue(exisiting);
this.input.nativeElement.value = '';
this.control.setValue(null);
}

@ -0,0 +1,24 @@
<mat-form-field class="example-chip-list" appearance="fill">
<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;
}
}

@ -41,6 +41,7 @@ import { SidebarModule } from "primeng/sidebar";
import { TheMovieDbService } from "../services";
import { TranslateModule } from "@ngx-translate/core";
import { TruncateModule } from "@yellowspot/ng-truncate";
import { WatchProvidersSelectComponent } from "./components/watch-providers-select/watch-providers-select.component";
@NgModule({
declarations: [
@ -51,6 +52,7 @@ import { TruncateModule } from "@yellowspot/ng-truncate";
AdvancedSearchDialogComponent,
KeywordSearchComponent,
GenreSelectComponent,
WatchProvidersSelectComponent,
],
imports: [
SidebarModule,
@ -99,6 +101,7 @@ import { TruncateModule } from "@yellowspot/ng-truncate";
AdvancedSearchDialogComponent,
GenreSelectComponent,
KeywordSearchComponent,
WatchProvidersSelectComponent,
DetailsGroupComponent,
TruncateModule,
InputSwitchModule,

@ -46,5 +46,23 @@ namespace Ombi.Controllers.External
[HttpGet("Genres/{media}")]
public async Task<IEnumerable<Genre>> GetGenres(string 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>

Loading…
Cancel
Save