mirror of https://github.com/Ombi-app/Ombi
Merge pull request #4274 from Ombi-app/advanced-search
Added the basics of advanced discoverypull/4301/head^2 v4.0.1458
commit
7f2c5a5f3e
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in new issue