chore: Storybook (#4700)

[skip ci]
pull/4706/head
Jamie 2 years ago committed by GitHub
parent dd48149ad9
commit d5e85ffdac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,47 @@
name: 'Chromatic'
# Event for the workflow
on:
push:
workflow_dispatch:
# List of jobs
jobs:
storybook-build:
# Operating System
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: NodeModules Cache
uses: actions/cache@v2
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
working-directory: ./src/Ombi/ClientApp
run: yarn
- name: Publish to Chromatic
if: github.ref != 'refs/heads/master'
uses: chromaui/action@v1
with:
projectToken: 7c47e1a1a4bd
exitZeroOnChanges: true
workingDir: ./src/Ombi/ClientApp
buildScriptName: storybookbuild
exitOnceUploaded: true
- name: Publish to Chromatic and auto accept changes
if: github.ref == 'refs/heads/master'
uses: chromaui/action@v1
with:
projectToken: 7c47e1a1a4bd
autoAcceptChanges: true # 👈 Option to accept all changes
workingDir: ./src/Ombi/ClientApp
buildScriptName: storybookbuild
exitOnceUploaded: true

@ -34,15 +34,17 @@ jobs:
- name: Install Frontend Deps
run: yarn --cwd ./src/Ombi/ClientApp install
- name: Start Frontend
run: |
nohup yarn --cwd ./src/Ombi/ClientApp start &
- name: Install Automation Deps
run: yarn --cwd ./tests install
- name: Start Backend
run: |
nohup dotnet run --project ./src/Ombi -- --host http://*:3577 &
- name: Start Frontend
run: |
nohup yarn --cwd ./src/Ombi/ClientApp start &
- name: Cypress Tests
uses: cypress-io/github-action@v2.8.2
with:
@ -51,8 +53,8 @@ jobs:
headless: true
working-directory: tests
wait-on: http://localhost:3577/
# 7 minutes
wait-on-timeout: 420
# 10 minutes
wait-on-timeout: 600
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -0,0 +1,16 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
"framework": "@storybook/angular",
"core": {
"builder": "@storybook/builder-webpack5"
},
"staticDirs": ['../../wwwroot/images']
}

@ -0,0 +1,5 @@
<style>
.test-class {
background-color: purple;
}
</style>

@ -0,0 +1,14 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
setCompodocJson(docJson);
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: { inlineStories: true },
}

@ -0,0 +1,24 @@
{
"extends": "../src/tsconfig.json",
"compilerOptions": {
"types": [
"node"
],
"typeRoots": [
"../node_modules/@typings"
],
"allowSyntheticDefaultImports": true
},
"exclude": [
"../src/test.ts",
"../src/**/*.spec.ts",
"../projects/**/*.spec.ts"
],
"include": [
"../src/**/*",
"../projects/**/*"
],
"files": [
"./typings.d.ts"
]
}

@ -0,0 +1,4 @@
declare module '*.md' {
const content: string;
export default content;
}

@ -0,0 +1,18 @@
{
"pipes": [],
"interfaces": [],
"injectables": [],
"guards": [],
"interceptors": [],
"classes": [],
"directives": [],
"components": [],
"modules": [],
"miscellaneous": [],
"routes": [],
"coverage": {
"count": 0,
"status": "low",
"files": []
}
}

@ -5,7 +5,11 @@
"ng": "ng",
"start": "ng serve --port 3578 --configuration hmr",
"build": "node --max_old_space_size=6144 node_modules/@angular/cli/bin/ng build -c production",
"lint": "ng lint"
"lint": "ng lint",
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
"storybook": "start-storybook -p 6006",
"chromatic": "chromatic --exit-zero-on-changes",
"storybookbuild": "yarn build-storybook"
},
"private": true,
"dependencies": {
@ -22,7 +26,7 @@
"@angular/platform-server": "^14.0.0",
"@angular/router": "^14.0.0",
"@angularclass/hmr": "^3.0.0",
"@aspnet/signalr": "^1.1.0",
"@microsoft/signalr": "^6.0.7",
"@auth0/angular-jwt": "^5.0.2",
"@fortawesome/fontawesome-free": "^6.0.0",
"@fullcalendar/core": "^4.2.0",
@ -59,16 +63,31 @@
"ts-md5": "^1.2.7",
"tslib": "^1.10.0",
"tslint-angular": "^1.1.2",
"zone.js": "~0.11.4"
"zone.js": "~0.11.4",
"protractor": "~5.4.0",
"ts-node": "~5.0.1",
"tslint": "^5.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^14.0.0",
"@angular/cli": "^14.0.0",
"@angular/compiler-cli": "^14.0.0",
"@angular/language-service": "^14.0.0",
"@babel/core": "^7.18.9",
"@compodoc/compodoc": "^1.1.19",
"@types/node": "^16.11.45",
"@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.9",
"@storybook/addon-interactions": "^6.5.9",
"@storybook/addon-links": "^6.5.9",
"@storybook/angular": "^6.5.9",
"@storybook/builder-webpack5": "^6.5.9",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"@types/jasmine": "~3.6.7",
"@types/jasminewd2": "~2.0.8",
"@types/node": "^16.10.9",
"babel-loader": "^8.2.5",
"chromatic": "^6.7.1",
"codelyzer": "^6.0.1",
"typescript": "~4.7.3"
},
@ -76,5 +95,7 @@
"protractor": "~5.4.0",
"ts-node": "~5.0.1",
"tslint": "^5.12.0"
}
}
},
"readme": "ERROR: No README data found!",
"_id": "ombi@3.0.0"
}

@ -0,0 +1,73 @@
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { APP_BASE_HREF } from '@angular/common';
import { Story, Meta, moduleMetadata } from '@storybook/angular';
import { RequestType } from '../../interfaces';
import { ImageComponent } from './image.component';
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
title: 'Image Component',
component: ImageComponent,
decorators: [
moduleMetadata({
providers: [
{
provide: APP_BASE_HREF,
useValue: ""
},
]
})
]
} as Meta;
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<ImageComponent> = (args: ImageComponent) => ({
props: args,
});
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
Primary.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie
};
export const ClassApplied = Template.bind({});
ClassApplied.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie,
class: 'test-class'
};
export const StyleApplied = Template.bind({});
StyleApplied.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie,
style: 'background-color: red;'
};
export const IdApplied = Template.bind({});
IdApplied.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie,
id: 'testId123'
};
// export const InvalidMovieImage = Template.bind({});
// InvalidMovieImage.args = {
// src: 'https://httpstat.us/429',
// type: RequestType.movie,
// id: 'testId123'
// };
// export const InvalidTvImage = Template.bind({});
// InvalidTvImage.args = {
// src: 'https://httpstat.us/429',
// type: RequestType.tvShow,
// };
// export const InvalidMusicImage = Template.bind({});
// InvalidMusicImage.args = {
// src: 'https://httpstat.us/429',
// type: RequestType.album,
// };

@ -1,198 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<ng-template #FilterRef>
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ 'Search.Suggestions' | translate }}
<i class="fa-list fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li><a (click)="popularMovies()" [translate]="'Search.Movies.PopularMovies'"></a></li>
<li><a (click)="upcomingMovies()" [translate]="'Search.Movies.UpcomingMovies'"></a></li>
<li><a (click)="topRatedMovies()" [translate]="'Search.Movies.TopRatedMovies'"></a></li>
<li><a (click)="nowPlayingMovies()" [translate]="'Search.Movies.NowPlayingMovies'"></a></li>
</ul>
<button class="btn btn-sm btn-primary-outline" (click)="refineOpen()">
{{ 'Search.Refine' | translate }}
<i class="fas" [ngClass]="{'fa-chevron-down': !refineSearchEnabled, 'fa-chevron-up': refineSearchEnabled}"></i>
</button>
</div>
</ng-template>
<div class="input-group search-bar-background">
<input id="search" type="text" placeholder="{{ 'Search.SearchBarPlaceholder' | translate}}"
class="form-control form-control-custom form-control-search form-control-withbuttons"
(keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="search-button-container-inline">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<i class="fas fa-search"></i>
</div>
</div>
<div class="row search-button-container">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<!-- Refine search options -->
<div class="row top-spacing form-group vcenter" *ngIf="refineSearchEnabled">
<div class="col-md-1">
<div class="form-group">
<label class="control-label">Year</label>
<input [(ngModel)]="searchYear" class="form-control form-control-custom refine-option">
</div>
</div>
<!-- <label for="name" class="col-xs-2 col-md-1">Language:</label> -->
<div class="col-md-2">
<div class="form-group">
<label for="select" class="control-label">Language</label>
<div id="profiles">
<select [(ngModel)]="selectedLanguage" class="form-control form-control-custom refine-option" id="select">
<option *ngFor="let lang of langauges" value="{{lang.code}}">{{lang.nativeName}}</option>
</select>
</div>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="actorSearch" name="actorSearch" [(ngModel)]="actorSearch">
<label for="actorSearch" tooltipPosition="top" pTooltip="Search for movies by actor">Actor Search</label>
</div>
</div>
</div>
<div class="col-md-7">
<button class="btn pull-right btn-success-outline" (click)="applyRefinedSearch()">Apply</button>
</div>
</div>
<remaining-requests [movie]="true" [quotaRefreshEvents]="movieRequested.asObservable()" #remainingFilms></remaining-requests>
<!-- Movie content -->
<div id="movieList">
<div *ngIf="searchApplied && movieResults?.length <= 0" class='no-search-results'>
<i class='fas fa-film no-search-results-icon'></i>
<div class='no-search-results-text' [translate]="'Search.NoResults'"></div>
</div>
<div *ngFor="let result of movieResults">
<div class="row">
<div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding">
<img *ngIf="result.posterPath" class="img-responsive poster movie-poster" src="{{result.posterPath}}"
alt="poster">
</div>
<div class="col-sm-8 small-padding">
<div>
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
<h4>{{result.title}} ({{result.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
</a>
<span class="tags">
<span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{
'Search.TheatricalRelease' | translate: {date: result.releaseDate | amLocal |
amDateFormat: 'LL'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel"
target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate |
amLocal | amUserLocale | amDateFormat: 'LL'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span
class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank"><span
class="label label-info" [translate]="'Search.Movies.Trailer'"></span></a>
<span *ngIf="result.quality" id="qualityLabel" class="label label-success">{{result.quality}}p</span>
<ng-template [ngIf]="result.available"><span class="label label-success" id="availableLabel"
[translate]="'Common.Available'"></span></ng-template>
<ng-template [ngIf]="result.approved && !result.available"><span class="label label-info"
id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span></ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.available"><span class="label label-warning"
id="pendingApprovalLabel" [translate]="'Common.PendingApproval'"></span></ng-template>
<ng-template [ngIf]="!result.requested && !result.available && !result.approved"><span
class="label label-danger" id="notRequestedLabel" [translate]="'Common.NotRequested'"></span></ng-template>
</span>
<br />
</div>
<p style="font-size: 0.9rem !important">{{result.overview}}</p>
</div>
<div class="col-sm-2 small-padding">
<div *ngIf="result.available">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fas fa-check"></i>
{{ 'Common.Available' | translate }}</button>
</div>
<div *ngIf="!result.available">
<div *ngIf="result.requested || result.approved; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]><i
class="fas fa-check"></i> {{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button id="{{result.id}}" style="text-align: right" class="btn btn-primary-outline"
(click)="request(result)">
<i *ngIf="result.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i> <i
*ngIf="!result.requestProcessing && !result.processed" class="fas fa-plus"></i>
<i *ngIf="result.processed && !result.requestProcessing" class="fas fa-check"></i> {{
'Common.Request' | translate }}</button>
</ng-template>
</div>
<div *ngIf="result.requested">
<a *ngIf="result.showSubscribe && !result.subscribed" style="text-align: right" class="btn btn btn-success-outline"
(click)="subscribe(result)" pTooltip="Subscribe for notifications when this movie becomes available">
<i class="fas fa-rss"></i> Subscribe</a>
<a *ngIf="result.showSubscribe && result.subscribed" style="text-align: right;" class="btn btn btn-warning-outline"
(click)="unSubscribe(result)" pTooltip="Unsubscribe notifications when this movie becomes available">
<i class="fas fa-rss"></i> Unsubscribe</a>
</div>
<button style="text-align: right" class="btn btn-sm btn-info-outline" (click)="similarMovies(result.id)">
<i class="far fa-eye"></i> {{ 'Search.Similar' | translate }}</button>
<br />
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}"
target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnPlex' | translate}}</a>
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline"
href="{{result.embyUrl}}" target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="result.jellyfinUrl" style="text-align: right" id="jellyfinbtn" class="btn btn-sm btn-success-outline"
href="{{result.jellyfinUrl}}" target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fas fa-plus"></i> {{'Requests.ReportIssue' | translate}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, result)">{{cat.value}}</a></li>
</ul>
</div>
</div>
</div>
<br />
<br />
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,275 +0,0 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, Input, OnInit, Inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IIssueCategory, ILanguageRefine, IRequestEngineResult, ISearchMovieResult } from "../interfaces";
import { NotificationService, RequestService, SearchService, SettingsService } from "../services";
import * as languageData from "../../other/iso-lang.json";
@Component({
selector: "movie-search",
templateUrl: "./moviesearch.component.html",
styleUrls: ["./search.component.scss"],
})
export class MovieSearchComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public movieRequested: Subject<void> = new Subject<void>();
public movieResults: ISearchMovieResult[];
public result: IRequestEngineResult;
public searchApplied = false;
public refineSearchEnabled = false;
public searchYear?: number;
public actorSearch: boolean;
public selectedLanguage: string;
public langauges: ILanguageRefine[];
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
public defaultPoster: string;
private href: string;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService, private sanitizer: DomSanitizer,
@Inject(APP_BASE_HREF) href:string, private settingsService: SettingsService) {
this.href= href;
this.langauges = <ILanguageRefine[]><any>languageData;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
this.runSearch();
});
this.defaultPoster = "../../../images/default_movie_poster.png";
const base = this.href;
if (base) {
this.defaultPoster = "../../.." + base + "/images/default_movie_poster.png";
}
}
public ngOnInit() {
this.searchText = "";
this.movieResults = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
this.settingsService.getDefaultLanguage().subscribe(x => this.selectedLanguage = x);
this.popularMovies();
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public request(searchResult: ISearchMovieResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
searchResult.showSubscribe = false;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
try {
const language = this.selectedLanguage && this.selectedLanguage.length > 0 ? this.selectedLanguage : "en";
this.requestService.requestMovie({ theMovieDbId: searchResult.id, languageCode: language })
.subscribe(x => {
this.result = x;
if (this.result.result) {
this.movieRequested.next();
this.translate.get("Search.RequestAdded", { title: searchResult.title }).subscribe(x => {
this.notificationService.success(x);
searchResult.processed = true;
});
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
public popularMovies() {
this.clearResults();
this.searchService.popularMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public nowPlayingMovies() {
this.clearResults();
this.searchService.nowPlayingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public topRatedMovies() {
this.clearResults();
this.searchService.topRatedMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public upcomingMovies() {
this.clearResults();
this.searchService.upcomingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public reportIssue(catId: IIssueCategory, req: ISearchMovieResult) {
this.issueRequestId = req.id;
const releaseDate = new Date(req.releaseDate);
this.issueRequestTitle = req.title + ` (${releaseDate.getFullYear()})`;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
public similarMovies(theMovieDbId: number) {
this.clearResults();
const lang = this.selectedLanguage && this.selectedLanguage.length > 0 ? this.selectedLanguage : "";
this.searchService.similarMovies(theMovieDbId, lang)
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public subscribe(r: ISearchMovieResult) {
r.subscribed = true;
this.requestService.subscribeToMovie(r.requestId)
.subscribe(x => {
this.notificationService.success(`Subscribed To Movie ${r.title}!`);
});
}
public unSubscribe(r: ISearchMovieResult) {
r.subscribed = false;
this.requestService.unSubscribeToMovie(r.requestId)
.subscribe(x => {
this.notificationService.success("Unsubscribed Movie!");
});
}
public refineOpen() {
this.refineSearchEnabled = !this.refineSearchEnabled;
if (!this.refineSearchEnabled) {
this.searchYear = undefined;
}
}
public applyRefinedSearch() {
this.runSearch();
}
private getExtraInfo() {
this.movieResults.forEach((val, index) => {
if (val.posterPath === null) {
val.posterPath = this.defaultPoster;
} else {
val.posterPath = "https://image.tmdb.org/t/p/w300/" + val.posterPath;
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")");
if (this.applyRefinedSearch) {
this.searchService.getMovieInformationWithRefined(val.id, this.selectedLanguage)
.subscribe(m => {
this.updateItem(val, m);
});
} else {
this.searchService.getMovieInformation(val.id)
.subscribe(m => {
this.updateItem(val, m);
});
}
});
}
private updateItem(key: ISearchMovieResult, updated: ISearchMovieResult) {
const index = this.movieResults.indexOf(key, 0);
if (index > -1) {
const copy = { ...this.movieResults[index] };
this.movieResults[index] = updated;
this.movieResults[index].background = copy.background;
this.movieResults[index].posterPath = copy.posterPath;
}
}
private clearResults() {
this.movieResults = [];
this.searchApplied = false;
}
private runSearch() {
if (this.searchText === "") {
this.clearResults();
return;
}
if (this.refineOpen) {
if (!this.actorSearch) {
this.searchService.searchMovieWithRefined(this.searchText, this.searchYear, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
} else {
this.searchService.searchMovieByActor(this.searchText, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
}
} else {
this.searchService.searchMovie(this.searchText)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
}
}
}

@ -1,149 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<div class="input-group">
<input id="search" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons" (keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Suggestions
<i class="fas fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li>
<a (click)="popularMovies()">Popular Movies</a>
</li>
<li>
<a (click)="upcomingMovies()">Upcoming Movies</a>
</li>
<li>
<a (click)="topRatedMovies()">Top Rated Movies</a>
</li>
<li>
<a (click)="nowPlayingMovies()">Now Playing Movies</a>
</li>
</ul>
</div>
<i id="movieSearchButton" class="fas fa-search"></i>
</div>
</div>
<br />
<br />
<!-- Movie content -->
<div id="movieList">
<div *ngIf="searchApplied && movieResults?.length <= 0" class='no-search-results'>
<i class='fas fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>Sorry, we didn't find any results!</div>
</div>
<!--NEW-->
<div *ngFor="let grid of movieResultGrid; let i = index">
<div class="row">
<div *ngFor="let r of grid.movies">
<div class="col-md-3">
<img *ngIf="r.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w150/{{r.posterPath}}"
alt="poster">
</div>
</div>
</div>
</div>
<!--END NEW-->
<br/>
<br/>
<br/>
<hr/>
<div *ngFor="let result of movieResults">
<div class="row">
<div id="imgDiv" class="col-sm-2">
<img *ngIf="result.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w150/{{result.posterPath}}"
alt="poster">
</div>
<div class="col-sm-8">
<div>
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
<h4>{{result.title}} ({{result.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
</a>
<span *ngIf="result.releaseDate" class="label label-info" target="_blank">Release Date: {{result.releaseDate | amLocal | amUserLocale | amDateFormat: 'L'}}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homepageLabel" target="_blank">
<span class="label label-info">HomePage</span>
</a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank">
<span class="label label-info">Trailer</span>
</a>
<span *ngIf="result.quality" class="label label-success" id="qualityLabel">{{result.quality}}p</span>
<ng-template [ngIf]="result.available">
<span class="label label-success" id="availableLabel">Available</span>
</ng-template>
<ng-template [ngIf]="result.approved && !result.available">
<span class="label label-info" id="processingRequestLabel">Processing Request</span>
</ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.available">
<span class="label label-warning" id="pendingApprovalLabel">Pending Approval</span>
</ng-template>
<ng-template [ngIf]="!result.requested && !result.available && !result.approved">
<span class="label label-danger" id="notRequetsedLabel">Not Requested</span>
</ng-template>
<br/>
<br/>
</div>
<p style="font-size: 0.9rem !important">{{result.overview}}</p>
</div>
<div class="col-sm-2">
<div *ngIf="result.available">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fas fa-check"></i> Available</button>
<div *ngIf="result.plexUrl">
<br/>
<br/>
<a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{result.plexUrl}}" target="_blank">
<i class="far fa-eye"></i> View In Plex</a>
</div>
</div>
<div *ngIf="!result.available">
<div *ngIf="result.requested || result.approved; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]>
<i class="fas fa-check"></i> Requested</button>
</ng-template>
<ng-template #notRequestedBtn>
<button id="{{result.id}}" style="text-align: right" class="btn btn-primary-outline" (click)="request(result)">
<i *ngIf="result.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!result.requestProcessing && !result.processed" class="fas fa-plus"></i>
<i *ngIf="result.processed && !result.requestProcessing" class="fas fa-check"></i>Request</button>
</ng-template>
</div>
<br/>
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank">
<i class="far fa-eye"></i> View On Plex</a>
</div>
</div>
</div>
<hr/>
</div>
</div>
</div>

@ -1,164 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IRequestEngineResult, ISearchMovieResult, ISearchMovieResultContainer } from "../interfaces";
import { NotificationService, RequestService, SearchService } from "../services";
@Component({
selector: "movie-search-grid",
templateUrl: "./moviesearchgrid.component.html",
})
export class MovieSearchGridComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public movieResults: ISearchMovieResult[];
public movieResultGrid: ISearchMovieResultContainer[] = [];
public result: IRequestEngineResult;
public searchApplied = false;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService) {
this.searchChanged.pipe(
debounceTime(600), // Wait Xms afterthe last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.clearResults();
return;
}
this.searchService.searchMovie(this.searchText)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some exta info including IMDBId
// This way the search is fast at displaying results.
this.getExtaInfo();
});
});
}
public ngOnInit() {
this.searchText = "";
this.movieResults = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public request(searchResult: ISearchMovieResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
try {
this.requestService.requestMovie({ theMovieDbId: searchResult.id, languageCode: "en" })
.subscribe(x => {
this.result = x;
if (this.result.result) {
this.notificationService.success(
`Request for ${searchResult.title} has been added successfully`);
searchResult.processed = true;
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
public popularMovies() {
this.clearResults();
this.searchService.popularMovies()
.subscribe(x => {
this.movieResults = x;
this.processGrid(x);
this.getExtaInfo();
});
}
public nowPlayingMovies() {
this.clearResults();
this.searchService.nowPlayingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
public topRatedMovies() {
this.clearResults();
this.searchService.topRatedMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
public upcomingMovies() {
this.clearResults();
this.searchService.upcomingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
private getExtaInfo() {
this.movieResults.forEach((val) => {
this.searchService.getMovieInformation(val.id)
.subscribe(m => this.updateItem(val, m));
});
}
private updateItem(key: ISearchMovieResult, updated: ISearchMovieResult) {
const index = this.movieResults.indexOf(key, 0);
if (index > -1) {
this.movieResults[index] = updated;
}
}
private clearResults() {
this.movieResults = [];
this.searchApplied = false;
}
private processGrid(movies: ISearchMovieResult[]) {
let container = <ISearchMovieResultContainer> { movies: [] };
movies.forEach((movie, i) => {
i++;
if ((i % 4) === 0) {
container.movies.push(movie);
this.movieResultGrid.push(container);
container = <ISearchMovieResultContainer> { movies: [] };
} else {
container.movies.push(movie);
}
});
this.movieResultGrid.push(container);
}
}

@ -1,116 +0,0 @@
<div class="row">
<!--Backdrop-->
<div class="album-bg backdrop" [style.background-image]="result.background"></div>
<div class="album-tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<!--Album Art-->
<div class="col-sm-12 small-padding">
<img *ngIf="result.disk" class="img-responsive poster album-cover" src="{{result.disk}}" alt="poster">
</div>
<!--Artist Title-->
<div class="col-sm-12 small-padding">
<div>
<h4>
<a href="" target="_blank">
{{result.title | truncate: 36}}
</a>
</h4>
<h5>
<a href="" (click)="selectArtist($event, result.foreignArtistId)">
{{result.artistName}}
</a>
</h5>
</div>
<!--Tags-->
<div>
<span class="tags">
<!-- <span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | date: 'mediumDate'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | date: 'mediumDate'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.Trailer'"></span></a> -->
<ng-template [ngIf]="!result.requested && !result.fullyAvailable && !result.approved">
<span class="label label-danger" id="notRequestedLabel" [translate]="'Common.NotRequested'"></span>
</ng-template>
<ng-template [ngIf]="result.fullyAvailable">
<span class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
</ng-template>
<ng-template [ngIf]="result.partiallyAvailable">
<span class="label label-info" id="availableLabel" [translate]="'Common.PartiallyAvailable'"></span>
</ng-template>
<ng-template [ngIf]="result.monitored && !result.fullyAvailable">
<span class="label label-info" id="processingRequestLabel" [translate]="'Common.Monitored'"></span>
</ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.partiallyAvailable">
<span class="label label-warning" id="pendingApprovalLabel" [translate]="'Common.PendingApproval'"></span>
</ng-template>
<ng-template [ngIf]="result.approved && !result.fullyAvailable"><span class="label label-info" id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span></ng-template>
<ng-template [ngIf]="result.releaseDate">
<span class="label label-info" id="availableLabel">Release Date: {{result.releaseDate | amLocal | amUserLocale | amDateFormat: 'L'}}</span>
</ng-template>
<ng-template [ngIf]="result.rating">
<span class="label label-info" id="availableLabel">{{result.rating}}/10</span>
</ng-template>
</span>
</div>
</div>
<!--Buttons-->
<div class="col-sm-12 small-padding">
<!-- <div class="row" *ngIf="result.requested">
<div class="col-md-2 col-md-push-10">
<a *ngIf="result.showSubscribe && !result.subscribed" style="color:white" (click)="subscribe(result)" pTooltip="Subscribe for notifications"> <i class="fas fa-rss"></i></a>
<a *ngIf="result.showSubscribe && result.subscribed" style="color:red" (click)="unSubscribe(result)" pTooltip="Unsubscribe notification"> <i class="fas fa-rss"></i></a>
</div>
</div> -->
<div *ngIf="result.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fas fa-check"></i> {{ 'Common.Available' | translate }}</button>
</div>
<div *ngIf="!result.fullyAvailable">
<div *ngIf="result.requested || result.approved || result.monitored; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]>
<i class="fas fa-check"></i> {{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button style="text-align: right" class="btn btn-primary-outline" (click)="request(result)">
<i *ngIf="result.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!result.requestProcessing && !result.processed" class="fas fa-plus"></i>
<i *ngIf="result.processed && !result.requestProcessing" class="fas fa-check"></i> {{ 'Common.Request'
| translate }}</button>
</ng-template>
</div>
<div class="dropdown" *ngIf="(result.partiallyAvailable || result.fullyAvailable) && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fas fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, result)">{{cat.value}}</a></li>
</ul>
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,91 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { Subject } from "rxjs";
import { AuthService } from "../../auth/auth.service";
import { IIssueCategory, IRequestEngineResult } from "../../interfaces";
import { ISearchAlbumResult } from "../../interfaces/ISearchMusicResult";
import { NotificationService, RequestService } from "../../services";
@Component({
selector: "album-search",
templateUrl: "./albumsearch.component.html",
})
export class AlbumSearchComponent {
@Input() public result: ISearchAlbumResult;
public engineResult: IRequestEngineResult;
@Input() public defaultPoster: string;
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
@Input() public musicRequested: Subject<void>;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
@Output() public setSearch = new EventEmitter<string>();
constructor(
private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService) {
}
public selectArtist(event: Event, artistId: string) {
event.preventDefault();
this.setSearch.emit(artistId);
}
public reportIssue(catId: IIssueCategory, req: ISearchAlbumResult) {
this.issueRequestId = req.id;
this.issueRequestTitle = req.title + `(${req.releaseDate.getFullYear})`;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
public request(searchResult: ISearchAlbumResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
searchResult.showSubscribe = false;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMusic")) {
searchResult.approved = true;
}
try {
this.requestService.requestAlbum({ foreignAlbumId: searchResult.foreignAlbumId })
.subscribe(x => {
this.engineResult = x;
if (this.engineResult.result) {
this.musicRequested.next();
this.translate.get("Search.RequestAdded", { title: searchResult.title }).subscribe(x => {
this.notificationService.success(x);
searchResult.processed = true;
});
} else {
if (this.engineResult.errorMessage && this.engineResult.message) {
this.notificationService.warning("Request Added", `${this.engineResult.message} - ${this.engineResult.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.engineResult.message ? this.engineResult.message : this.engineResult.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
}

@ -1,65 +0,0 @@
<div class="row">
<div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-3 small-padding">
<img *ngIf="result.poster" class="img-responsive poster artist-cover" src="{{result.poster}}" alt="poster">
</div>
<div class="col-sm-7 small-padding">
<div>
<a href="" target="_blank">
<h4>{{result.artistName}}</h4>
</a>
<span class="tags">
<!-- <span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | date: 'mediumDate'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | date: 'mediumDate'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.Trailer'"></span></a> -->
<ng-template [ngIf]="result.artistType">
<span class="label label-info" id="artistType">{{result.artistType}}</span>
</ng-template>
<ng-template [ngIf]="result.disambiguation">
<span class="label label-info" id="disambiguation">{{result.disambiguation}}</span>
</ng-template>
<ng-template [ngIf]="result.monitored">
<span class="label label-info" id="disambiguation">Monitored</span>
</ng-template>
</span>
<br/>
</div>
<p style="font-size: 0.9rem !important">{{result.overview | truncate: 350 }}</p>
<div class="row">
<a id="infoTags" *ngFor="let link of result.links" href="{{link.url}}" target="_blank" class="label label-primary">
{{link.name}}
</a>
</div>
</div>
<div class="col-sm-2 small-padding">
<div class="row" *ngIf="result.requested">
<div class="col-md-2 col-md-push-10">
<!-- <a *ngIf="result.showSubscribe && !result.subscribed" style="color:white" (click)="subscribe(result)" pTooltip="Subscribe for notifications"> <i class="fas fa-rss"></i></a>
<a *ngIf="result.showSubscribe && result.subscribed" style="color:red" (click)="unSubscribe(result)" pTooltip="Unsubscribe notification"> <i class="fas fa-rss"></i></a> -->
</div>
</div>
<button style="text-align: right" class="btn btn-info-outline" [disabled]="searchingAlbums" (click)="viewAllAlbums()">
<i class="far fa-eye"></i> View Albums</button>
</div>
<!-- <button style="text-align: right" class="btn btn-sm btn-info-outline" (click)="similarMovies(result.id)"> <i class="far fa-eye"></i> {{ 'Search.Similar' | translate }}</button> -->
<br/>
</div>

@ -1,27 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ISearchAlbumResult, ISearchArtistResult } from "../../interfaces/ISearchMusicResult";
import { SearchService } from "../../services";
@Component({
selector: "artist-search",
templateUrl: "./artistsearch.component.html",
})
export class ArtistSearchComponent {
@Input() public result: ISearchArtistResult;
@Input() public defaultPoster: string;
public searchingAlbums: boolean;
@Output() public viewAlbumsResult = new EventEmitter<ISearchAlbumResult[]>();
constructor(private searchService: SearchService) {
}
public viewAllAlbums() {
this.searchingAlbums = true;
this.searchService.getAlbumsForArtist(this.result.forignArtistId).subscribe(x => {
this.viewAlbumsResult.emit(x);
});
}
}

@ -1,52 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<div class="input-group">
<input id="search" type="text" placeholder="{{ 'Search.SearchBarPlaceholder' | translate }}" class="form-control form-control-custom form-control-search form-control-withbuttons" (keyup)="search($event)">
<div class="input-group-addon right-radius">
<i class="fas fa-search"></i>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="Album" name="Mode" [checked]="searchAlbum" (click)="searchMode(true)">
<label for="Album">Album Search</label>
<input type="radio" id="Artist" name="Mode" [checked]="!searchAlbum" (click)="searchMode(false)">
<label for="Artist">Artist Search</label>
</div>
</div>
<br />
<br />
<div id="movieList">
<div *ngIf="searchApplied && artistResult?.length <= 0 && !searchAlbum" class='no-search-results'>
<i class='fas fa-music no-search-results-icon'></i>
<div class='no-search-results-text' [translate]="'Search.NoResults'"></div>
</div>
<div *ngIf="searchApplied && albumResult?.length <= 0 && searchAlbum" class='no-search-results'>
<i class='fas fa-music no-search-results-icon'></i>
<div class='no-search-results-text' [translate]="'Search.NoResults'"></div>
</div>
<remaining-requests [music]="true" [quotaRefreshEvents]="musicRequested.asObservable()" #remainingAlbums></remaining-requests>
<div *ngFor="let result of artistResult">
<artist-search [result]="result" [defaultPoster]="defaultPoster" (viewAlbumsResult)="viewAlbumsForArtist($event)"></artist-search>
<br/>
<br/>
</div>
<div class="col-md-12">
<div *ngFor="let result of albumResult" class="col-md-4">
<album-search [musicRequested]="musicRequested" [result]="result" [defaultPoster]="defaultPoster" (setSearch)="setArtistSearch($event)"></album-search>
<br/>
<br/>
</div>
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,149 +0,0 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, Input, OnInit, Inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { IIssueCategory, IRequestEngineResult } from "../../interfaces";
import { ISearchAlbumResult, ISearchArtistResult } from "../../interfaces/ISearchMusicResult";
import { SearchService } from "../../services";
@Component({
selector: "music-search",
templateUrl: "./musicsearch.component.html",
})
export class MusicSearchComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public artistResult: ISearchArtistResult[];
public albumResult: ISearchAlbumResult[];
public result: IRequestEngineResult;
public searchApplied = false;
public searchAlbum: boolean = true;
public musicRequested: Subject<void> = new Subject<void>();
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
public defaultPoster: string;
private href: string;
constructor(
private searchService: SearchService, private sanitizer: DomSanitizer,
@Inject(APP_BASE_HREF) href:string) {
this.href = href;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
if(this.searchAlbum) {
this.clearAlbumResults();
} else {
this.clearArtistResults();
}
return;
}
if(this.searchAlbum) {
if(!this.searchText) {
this.searchText = "iowa"; // REMOVE
}
this.searchService.searchAlbum(this.searchText)
.subscribe(x => {
this.albumResult = x;
this.searchApplied = true;
this.setAlbumBackground();
});
} else {
this.searchService.searchArtist(this.searchText)
.subscribe(x => {
this.artistResult = x;
this.searchApplied = true;
this.setArtistBackground();
});
}
});
this.defaultPoster = "../../../images/default-music-placeholder.png";
const base = this.href;
if (base) {
this.defaultPoster = "../../.." + base + "/images/default-music-placeholder.png";
}
}
public ngOnInit() {
this.searchText = "";
this.artistResult = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public searchMode(val: boolean) {
this.searchAlbum = val;
if(val) {
// Album
this.clearArtistResults();
} else {
this.clearAlbumResults();
}
}
public setArtistSearch(artistId: string) {
this.searchAlbum = false;
this.clearAlbumResults();
this.searchChanged.next(`lidarr:${artistId}`);
}
public viewAlbumsForArtist(albums: ISearchAlbumResult[]) {
this.clearArtistResults();
this.searchAlbum = true;
this.albumResult = albums;
this.setAlbumBackground();
}
private clearArtistResults() {
this.artistResult = [];
this.searchApplied = false;
}
private clearAlbumResults() {
this.albumResult = [];
this.searchApplied = false;
}
private setArtistBackground() {
this.artistResult.forEach((val, index) => {
if (val.poster === null) {
val.poster = this.defaultPoster;
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + val.banner + ")");
});
}
private setAlbumBackground() {
this.albumResult.forEach((val, index) => {
if (val.disk === null) {
if(val.cover === null) {
val.disk = this.defaultPoster;
} else {
val.disk = val.cover;
}
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + val.cover + ")");
});
}
}

@ -1,35 +0,0 @@
<h1 [translate]="'Search.Title'"></h1>
<h4 [translate]="'Search.Paragraph'"></h4>
<br />
<!-- Nav tabs -->
<ul id="nav-tabs" class="nav nav-tabs nav-justified" role="tablist">
<li role="presentation" class="active">
<a id="movieTabButton" href="#MoviesTab" aria-controls="home" role="tab" data-toggle="tab" (click)="selectMovieTab()"><i class="fas fa-film"></i> {{ 'Search.MoviesTab' | translate }}</a>
</li>
<li role="presentation">
<a id="tvTabButton" href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectTvTab()"><i class="fas fa-tv"></i> {{ 'Search.TvTab' | translate }}</a>
</li>
<li role="presentation" *ngIf="musicEnabled">
<a id="tvTabButton" href="#MusicTab" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectMusicTab()"><i class="fas fa-music"></i> {{ 'Search.MusicTab' | translate }}</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div [hidden]="!showMovie">
<movie-search [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></movie-search>
</div>
<div [hidden]="!showTv">
<tv-search [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></tv-search>
</div>
<div [hidden]="!showMusic">
<music-search></music-search>
</div>
</div>

@ -1,34 +0,0 @@
@media (max-width: 978px) {
.top-spacing {
padding-top: 5%
}
.form-control-search {
padding-right: 165px;
}
}
@media (min-width: 979px) {
.top-spacing {
padding-top: 2%
}
.form-control-search {
width: 90%;
}
}
.tab-content {
margin-top: 1.5em;
}
.search-bar-background {
background-color: #333333;
}
.vcenter {
display: flex;
align-items: center;
}
.refine-option {
box-shadow: inset 0 1px 5px rgba(0,0,0,1.0);
}

@ -1,47 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { IIssueCategory } from "../interfaces";
import { IssuesService, SettingsService } from "../services";
@Component({
templateUrl: "./search.component.html",
})
export class SearchComponent implements OnInit {
public showTv: boolean;
public showMovie: boolean;
public showMusic: boolean;
public issueCategories: IIssueCategory[];
public issuesEnabled = false;
public musicEnabled: boolean;
constructor(private issuesService: IssuesService,
private settingsService: SettingsService) {
}
public ngOnInit() {
this.settingsService.lidarrEnabled().subscribe(x => this.musicEnabled = x);
this.showMovie = true;
this.showTv = false;
this.showMusic = false;
this.issuesService.getCategories().subscribe(x => this.issueCategories = x);
this.settingsService.getIssueSettings().subscribe(x => this.issuesEnabled = x.enabled);
}
public selectMovieTab() {
this.showMovie = true;
this.showTv = false;
this.showMusic = false;
}
public selectTvTab() {
this.showMovie = false;
this.showTv = true;
this.showMusic = false;
}
public selectMusicTab() {
this.showMovie = false;
this.showTv = false;
this.showMusic = true;
}
}

@ -1,53 +0,0 @@
import { RouterModule, Routes } from "@angular/router";
import { AlbumSearchComponent } from "./music/albumsearch.component";
import { ArtistSearchComponent } from "./music/artistsearch.component";
import { AuthGuard } from "../auth/auth.guard";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { MovieSearchComponent } from "./moviesearch.component";
import { MovieSearchGridComponent } from "./moviesearchgrid.component";
import { MusicSearchComponent } from "./music/musicsearch.component";
import { NgModule } from "@angular/core";
import { RemainingRequestsComponent } from "../requests/remainingrequests.component";
import { RequestService } from "../services";
import { SearchComponent } from "./search.component";
import { SearchService } from "../services";
import { SeriesInformationComponent } from "./seriesinformation.component";
import { SharedModule } from "../shared/shared.module";
import { TvSearchComponent } from "./tvsearch.component";
const routes: Routes = [
{ path: "", component: SearchComponent, canActivate: [AuthGuard] },
{ path: "show/:id", component: SeriesInformationComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
CommonModule,
FormsModule,
RouterModule.forChild(routes),
TreeTableModule,
SharedModule,
SidebarModule,
TooltipModule,
],
declarations: [
SearchComponent,
MovieSearchComponent,
TvSearchComponent,
SeriesInformationComponent,
MovieSearchGridComponent,
RemainingRequestsComponent,
MusicSearchComponent,
ArtistSearchComponent,
AlbumSearchComponent,
],
exports: [
RouterModule,
],
providers: [
SearchService,
RequestService,
],
})
export class SearchModule { }

@ -1,71 +0,0 @@
<div *ngIf="series" class="content-space">
<button class="btn btn-sm btn-success pull-right" (click)="submitRequests()" title="Go to top">{{ 'Search.TvShows.SubmitRequest' | translate }}</button>
<ngb-tabset>
<div *ngFor="let season of series.seasonRequests">
<ngb-tab [id]="season.seasonNumber" [title]="season.seasonNumber">
<ng-template ngbTabContent>
<h2 [translate]="'Requests.SeasonNumberHeading'" [translateParams]="{seasonNumber: season.seasonNumber}">Season: {{season.seasonNumber}}</h2>
<button (click)="addAllEpisodes(season)" class="btn btn-sm btn-primary-outline" [translate]="'Search.TvShows.SelectAllInSeason'" [translateParams]="{seasonNumber: season.seasonNumber}">Select All in Season {{season.seasonNumber}}</button>
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th>
<a>
#
</a>
</th>
<th>
<a>
Title
</a>
</th>
<th>
<a>
Air Date
</a>
</th>
<th>
<a>
Status
</a>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ep of season.episodes">
<td>
{{ep.episodeNumber}}
</td>
<td>
{{ep.title}}
</td>
<td *ngIf="ep.airDateDisplay != 'Unknown'">
{{ep.airDate | amLocal | amUserLocale | amDateFormat: 'L' }}
</td>
<td *ngIf="ep.airDateDisplay == 'Unknown'">
{{ep.airDateDisplay }}
</td>
<td>
<ng-template [ngIf]="ep.available"><span class="label label-success" id="availableLabel">Available</span></ng-template>
<ng-template [ngIf]="ep.approved && !ep.available "><span class="label label-info" id="processingRequestLabel">Processing Request</span></ng-template>
<ng-template [ngIf]="ep.selected"><span class="label label-info" id="selectedLabel">Selected</span></ng-template>
<ng-template [ngIf]="ep.requested && !ep.approved && !ep.available && !ep.selected"><span class="label label-warning" id="pendingApprovalLabel">Pending Approval</span></ng-template>
<ng-template [ngIf]="!ep.requested && !ep.available && !ep.approved"><span class="label label-danger" id="notRequetsedLabel">Not Requested</span></ng-template>
</td>
<td>
<button *ngIf="!ep.selected" (click)="addRequest(ep)" [disabled]="ep.available || ep.requested || ep.approved" class="btn btn-sm btn-primary-outline">Select</button>
<button *ngIf="ep.selected" (click)="removeRequest(ep)" class="btn btn-sm btn-primary-outline">Unselect</button>
</td>
</tr>
</tbody>
</table>
</ng-template>
</ngb-tab>
</div>
</ngb-tabset>
</div>

@ -1,25 +0,0 @@
#requestFloatingBtn {
position: fixed; /* Fixed/sticky position */
bottom: 20px; /* Place the button at the bottom of the page */
right: 30px; /* Place the button 30px from the right */
z-index: 99; /* Make sure it does not overlap */
cursor: pointer; /* Add a mouse pointer on hover */
padding: 15px; /* Some padding */
border-radius: 10px; /* Rounded corners */
}
#requestFloatingBtn:hover {
background-color: #555; /* Add a dark-grey background on hover */
}
#bannerimage {
width: 758px;
height: 140px;
background-color: black;
background-position: center;
padding-bottom:30px;
}
.content-space {
padding-top: 10px;
}

@ -1,98 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { NotificationService } from "../services";
import { RequestService } from "../services";
import { SearchService } from "../services";
import { INewSeasonRequests, IRequestEngineResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
import { IEpisodesRequests } from "../interfaces";
import { ISearchTvResult } from "../interfaces";
import { Subject } from "rxjs";
@Component({
selector: "seriesinformation",
templateUrl: "./seriesinformation.component.html",
styleUrls: ["./seriesinformation.component.scss"],
})
export class SeriesInformationComponent implements OnInit {
public result: IRequestEngineResult;
public series: ISearchTvResult;
public requestedEpisodes: IEpisodesRequests[] = [];
@Input() public tvRequested: Subject<void>;
@Input() private seriesId: number;
constructor(private searchService: SearchService, private requestService: RequestService, private notificationService: NotificationService) { }
public ngOnInit() {
this.searchService.getShowInformation(this.seriesId)
.subscribe(x => {
this.series = x;
});
}
public submitRequests() {
// Make sure something has been selected
const selected = this.series.seasonRequests.some((season) => {
return season.episodes.some((ep) => {
return ep.selected;
});
});
if (!selected) {
this.notificationService.error("You need to select some episodes!");
return;
}
this.series.requested = true;
const viewModel = <ITvRequestViewModel> { firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id};
viewModel.seasons = [];
this.series.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel> {seasonNumber: season.seasonNumber, episodes: []};
season.episodes.forEach(ep => {
if (!this.series.latestSeason || !this.series.requestAll || !this.series.firstSeason) {
if (ep.selected) {
seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber});
}
}
});
viewModel.seasons.push(seasonsViewModel);
});
this.requestService.requestTv(viewModel)
.subscribe(x => {
this.tvRequested.next();
this.result = x as IRequestEngineResult;
if (this.result.result) {
this.notificationService.success(
`Request for ${this.series.title} has been added successfully`);
this.series.seasonRequests.forEach((season) => {
season.episodes.forEach((ep) => {
ep.selected = false;
});
});
} else {
this.notificationService.warning("Request Added", this.result.errorMessage ? this.result.errorMessage : this.result.message);
}
});
}
public addRequest(episode: IEpisodesRequests) {
episode.requested = true;
episode.selected = true;
}
public removeRequest(episode: IEpisodesRequests) {
episode.requested = false;
episode.selected = false;
}
public addAllEpisodes(season: INewSeasonRequests) {
season.episodes.forEach((ep) => this.addRequest(ep));
}
}

@ -1,185 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab">
<ng-template #FilterRef>
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ 'Search.Suggestions' | translate }}
<i class="fas fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li>
<a (click)="popularShows()">{{ 'Search.TvShows.Popular' | translate }} </a>
</li>
<li>
<a (click)="trendingShows()">{{ 'Search.TvShows.Trending' | translate }}</a>
</li>
<li>
<a (click)="mostWatchedShows()">{{ 'Search.TvShows.MostWatched' | translate }}</a>
</li>
<li>
<a (click)="anticipatedShows()">{{ 'Search.TvShows.MostAnticipated' | translate }}</a>
</li>
</ul>
</div>
</ng-template>
<div class="input-group">
<input id="search" type="text" placeholder="{{ 'Search.SearchBarPlaceholder' | translate }}"
class="form-control form-control-custom form-control-search form-control-withbuttons"
(keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="search-button-container-inline">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<i id="tvSearchButton" class="fas fa-search"></i>
</div>
</div>
<div class="row search-button-container">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<remaining-requests [tv]="true" [quotaRefreshEvents]="tvRequested.asObservable()" #remainingTvShows></remaining-requests>
<!-- Movie content -->
<div id="actorMovieList">
</div>
<!-- TV content -->
<div id="tvList">
<div *ngIf="searchApplied && tvResults?.length <= 0" class='no-search-results'>
<i class='fas fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>{{ 'Search.NoResults' | translate }}</div>
</div>
<div *ngIf="tvResults" >
<div *ngFor="let node of tvResults">
<!--This is the section that holds the parent level search results set-->
<div *ngIf="node">
<div class="row">
<div class="myBg backdrop" [style.background-image]="node?.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding">
<img *ngIf="node.banner" class="img-responsive poster tv-poster" width="150" [src]="node.banner" alt="poster">
</div>
<div class="col-sm-8 small-padding">
<div>
<a *ngIf="node.imdbId" href="{{node.imdbId}}" target="_blank">
<h4>{{node.title}} ({{node.firstAired | amLocal | amDateFormat: 'YYYY'}})</h4>
</a>
<span class="tags">
<a *ngIf="node.homepage" id="homepageLabel" href="{{node.homepage}}" target="_blank">
<span class="label label-info">{{ 'Search.Movies.HomePage' | translate }}</span>
</a>
<a *ngIf="node.trailer" id="trailerLabel" href="{{node.trailer}}" target="_blank">
<span class="label label-info">{{ 'Search.Movies.Trailer' | translate }}</span>
</a>
<span *ngIf="node.status" class="label label-primary" id="statusLabel" target="_blank">{{node.status}}</span>
<span *ngIf="node.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.firstAired | amLocal | amUserLocale | amDateFormat: 'L'}}</span>
<span *ngIf="node.network" class="label label-info" id="networkLabel" target="_blank">{{node.network}}</span>
<span *ngIf="node.available" class="label label-success" id="availableLabel">{{ 'Common.Available' | translate }}</span>
<span *ngIf="node.partlyAvailable" class="label label-warning" id="partiallyAvailableLabel">{{ 'Common.PartlyAvailable' | translate }}</span>
</span>
<br />
<br />
</div>
<p class="tv-overview">{{node.overview}}</p>
</div>
<div class="col-sm-2 small-padding">
<div *ngIf="!node.fullyAvailable" class="dropdown">
<button class="btn btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fas fa-plus"></i> {{ 'Common.Request' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li>
<a href="#" (click)="allSeasons(node, $event)">{{ 'Search.TvShows.AllSeasons' | translate }}</a>
</li>
<li>
<a href="#" (click)="firstSeason(node, $event)">{{ 'Search.TvShows.FirstSeason' | translate }}</a>
</li>
<li>
<a href="#" (click)="latestSeason(node, $event)">{{ 'Search.TvShows.LatestSeason' | translate }}</a>
</li>
<li>
<a href="#" (click)="openClosestTab(node, $event)">{{ 'Search.TvShows.Select' | translate }}</a>
</li>
</ul>
</div>
<div *ngIf="node.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fas fa-check"></i> {{ 'Common.Available' | translate }}
</button>
</div>
<br />
<div *ngIf="node.plexUrl && node.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.plexUrl}}" target="_blank">
<i class="far fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}
</a>
</div>
<div *ngIf="node.embyUrl && node.available">
<a style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{node.embyUrl}}" target="_blank">
<i class="far fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }}
</a>
</div>
<div *ngIf="node.jellyfinUrl && node.available">
<a style="text-align: right" id="jellyfinbtn" class="btn btn-sm btn-success-outline" href="{{node.jellyfinUrl}}" target="_blank">
<i class="far fa-eye"></i> {{ 'Search.ViewOnJellyfin' | translate }}
</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fas fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories">
<a [routerLink]="" (click)="reportIssue(cat, node)">{{cat.value}}</a>
</li>
</ul>
</div>
<div *ngIf="!node.available">
<br />
<br />
</div>
</div>
</div>
</div>
<!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.open">
<seriesinformation [seriesId]="node.id" [tvRequested]="tvRequested"></seriesinformation>
</div>
<br/>
<br/>
</div>
</div>
</div>
</div>
<issue-report [movie]="false" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,238 +0,0 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, Input, OnInit, Inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchTvResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
import { ImageService, NotificationService, RequestService, SearchService } from "../services";
@Component({
selector: "tv-search",
templateUrl: "./tvsearch.component.html",
styleUrls: ["./../requests/tvrequests.component.scss"],
})
export class TvSearchComponent implements OnInit {
public searchText: string;
public searchChanged = new Subject<string>();
public tvResults: ISearchTvResult[];
public tvRequested: Subject<void> = new Subject<void>();
public result: IRequestEngineResult;
public searchApplied = false;
public defaultPoster: string;
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
private href: string;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private imageService: ImageService, private sanitizer: DomSanitizer,
@Inject(APP_BASE_HREF) href:string) {
this.href = href;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.clearResults();
return;
}
this.searchService.searchTv(this.searchText)
.subscribe(x => {
this.tvResults = x;
this.searchApplied = true;
this.getExtraInfo();
});
});
this.defaultPoster = "../../../images/default_tv_poster.png";
const base = this.href;
if (base) {
this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png";
}
}
public openClosestTab(node: ISearchTvResult,el: any) {
el.preventDefault();
node.open = !node.open;
}
public ngOnInit() {
this.searchText = "";
this.tvResults = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
this.popularShows();
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public popularShows() {
this.clearResults();
this.searchService.popularTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public trendingShows() {
this.clearResults();
this.searchService.trendingTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public mostWatchedShows() {
this.clearResults();
this.searchService.mostWatchedTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public anticipatedShows() {
this.clearResults();
this.searchService.anticipatedTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public getExtraInfo() {
this.tvResults.forEach((val, index) => {
this.imageService.getTvBanner(val.id).subscribe(x => {
if (x) {
val.background = this.sanitizer.
bypassSecurityTrustStyle
("url(" + x + ")");
}
});
this.searchService.getShowInformation(val.id)
.subscribe(x => {
if (x) {
this.setDefaults(x);
this.updateItem(val, x);
} else {
const index = this.tvResults.indexOf(val, 0);
if (index > -1) {
this.tvResults.splice(index, 1);
}
}
});
});
}
public request(searchResult: ISearchTvResult) {
searchResult.requested = true;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
const viewModel = <ITvRequestViewModel> { firstSeason: searchResult.firstSeason, latestSeason: searchResult.latestSeason, requestAll: searchResult.requestAll, tvDbId: searchResult.id };
viewModel.seasons = [];
searchResult.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel> { seasonNumber: season.seasonNumber, episodes: [] };
season.episodes.forEach(ep => {
if (!searchResult.latestSeason || !searchResult.requestAll || !searchResult.firstSeason) {
if (ep.requested) {
seasonsViewModel.episodes.push({ episodeNumber: ep.episodeNumber });
}
}
});
viewModel.seasons.push(seasonsViewModel);
});
this.requestService.requestTv(viewModel)
.subscribe(x => {
this.tvRequested.next();
this.result = x;
if (this.result.result) {
this.notificationService.success(
`Request for ${searchResult.title} has been added successfully`);
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
}
});
}
public allSeasons(searchResult: ISearchTvResult, event: any) {
event.preventDefault();
searchResult.requestAll = true;
this.request(searchResult);
}
public firstSeason(searchResult: ISearchTvResult, event: any) {
event.preventDefault();
searchResult.firstSeason = true;
this.request(searchResult);
}
public latestSeason(searchResult: ISearchTvResult, event: any) {
event.preventDefault();
searchResult.latestSeason = true;
this.request(searchResult);
}
public reportIssue(catId: IIssueCategory, req: ISearchTvResult) {
this.issueRequestId = req.id;
const firstAiredDate = new Date(req.firstAired);
this.issueRequestTitle = req.title + ` (${firstAiredDate.getFullYear()})`;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
private updateItem(key: ISearchTvResult, updated: ISearchTvResult) {
const index = this.tvResults.indexOf(key, 0);
if (index > -1) {
// Update certain properties, otherwise we will loose some data
this.tvResults[index].title = updated.title;
this.tvResults[index].banner = updated.banner;
this.tvResults[index].imdbId = updated.imdbId;
this.tvResults[index].seasonRequests = updated.seasonRequests;
this.tvResults[index].seriesId = updated.seriesId;
this.tvResults[index].fullyAvailable = updated.fullyAvailable;
this.tvResults[index].background = updated.banner;
}
}
private setDefaults(x: ISearchTvResult) {
if (x.banner === null) {
x.banner = this.defaultPoster;
}
if (x.imdbId === null) {
x.imdbId = "https://www.tvmaze.com/shows/" + x.seriesId;
} else {
x.imdbId = "http://www.imdb.com/title/" + x.imdbId + "/";
}
}
private clearResults() {
this.tvResults = [];
this.searchApplied = false;
}
}

@ -1,8 +1,8 @@
import { Injectable, EventEmitter } from '@angular/core';
import { AuthService } from '../auth/auth.service';
import { HubConnection } from '@aspnet/signalr';
import * as signalR from '@aspnet/signalr';
import { HubConnection } from '@microsoft/signalr';
import * as signalR from '@microsoft/signalr';
@Injectable()
export class SignalRNotificationService {

@ -10,9 +10,11 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2020",
"types": ["node"],
"resolveJsonModule":true,
"allowSyntheticDefaultImports":true,
"types": [
"node"
],
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"typeRoots": [
"node_modules/@types"
],
@ -20,13 +22,15 @@
"es2017",
"dom"
]
}
,
},
"files": [
"main.ts",
"polyfills.ts"
],
"include": [
"src/**/*.d.ts"
],
"exclude": [
"**/*.stories.*"
]
}

@ -1,5 +1,5 @@
// Globals
declare var __webpack_public_path__: any;
// declare var __webpack_public_path__: any;
// declare module "*.json" {
// const value: any;
// export default value;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save