Got a bit of the work done, needs some polish then tackle tv

pull/4112/head
tidusjar 4 years ago
parent 873823017c
commit c5f123b903

@ -69,19 +69,25 @@ namespace Ombi.Core.Engine
var userDetails = await GetUser(); var userDetails = await GetUser();
var canRequestOnBehalf = false; var canRequestOnBehalf = false;
if (model.RequestOnBehalf.HasValue()) var isAdmin = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin);
if (model.RequestOnBehalf.HasValue() && !isAdmin)
{ {
canRequestOnBehalf = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); return new RequestEngineResult
{
Result = false,
Message = "You do not have the correct permissions to request on behalf of users!",
ErrorMessage = $"You do not have the correct permissions to request on behalf of users!"
};
}
if (!canRequestOnBehalf) if ((model.RootFolderOverride.HasValue || model.QualityPathOverride.HasValue) && !isAdmin)
{
return new RequestEngineResult
{ {
return new RequestEngineResult Result = false,
{ Message = "You do not have the correct permissions!",
Result = false, ErrorMessage = $"You do not have the correct permissions!"
Message = "You do not have the correct permissions to request on behalf of users!", };
ErrorMessage = $"You do not have the correct permissions to request on behalf of users!"
};
}
} }
var requestModel = new MovieRequests var requestModel = new MovieRequests
@ -101,7 +107,9 @@ namespace Ombi.Core.Engine
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id, RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
Background = movieInfo.BackdropPath, Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode, LangCode = model.LanguageCode,
RequestedByAlias = model.RequestedByAlias RequestedByAlias = model.RequestedByAlias,
RootPathOverride = model.RootFolderOverride.GetValueOrDefault(),
QualityOverride = model.QualityPathOverride.GetValueOrDefault()
}; };
var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US"); var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");

@ -0,0 +1,39 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2018 Jamie Rees
// File: MovieRequestViewModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using Newtonsoft.Json;
namespace Ombi.Core.Models.Requests
{
public class BaseRequestOptions
{
public string RequestOnBehalf { get; set; }
public int? RootFolderOverride { get; set; }
public int? QualityPathOverride { get; set; }
}
}

@ -29,11 +29,10 @@ using Newtonsoft.Json;
namespace Ombi.Core.Models.Requests namespace Ombi.Core.Models.Requests
{ {
public class MovieRequestViewModel public class MovieRequestViewModel : BaseRequestOptions
{ {
public int TheMovieDbId { get; set; } public int TheMovieDbId { get; set; }
public string LanguageCode { get; set; } = "en"; public string LanguageCode { get; set; } = "en";
public string RequestOnBehalf { get; set; }
/// <summary> /// <summary>
/// This is only set from a HTTP Header /// This is only set from a HTTP Header

@ -20,7 +20,7 @@ namespace Ombi.Core.Models.Requests
} }
public class TvRequestViewModelBase public class TvRequestViewModelBase : BaseRequestOptions
{ {
public bool RequestAll { get; set; } public bool RequestAll { get; set; }
public bool LatestSeason { get; set; } public bool LatestSeason { get; set; }
@ -28,7 +28,5 @@ namespace Ombi.Core.Models.Requests
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>(); public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
[JsonIgnore] [JsonIgnore]
public string RequestedByAlias { get; set; } public string RequestedByAlias { get; set; }
public string RequestOnBehalf { get; set; }
} }
} }

@ -17,7 +17,7 @@ namespace Ombi.Core.Rule.Rules.Search
{ {
// If we have all the episodes for this season, then this season is available // If we have all the episodes for this season, then this season is available
if (season.Episodes.All(x => x.Available)) if (season.Episodes.All(x => x.Available))
{yarn {
season.SeasonAvailable = true; season.SeasonAvailable = true;
} }
} }
@ -25,7 +25,7 @@ namespace Ombi.Core.Rule.Rules.Search
{ {
search.FullyAvailable = true; search.FullyAvailable = true;
} }
else if (search.SeasonRequests.Any(x => x.Episodes.Any(e => e.Available))) else if (search.SeasonRequests.Any(x => x.Episodes.Any(e => e.Available)))
{ {
search.PartlyAvailable = true; search.PartlyAvailable = true;
} }

@ -21,5 +21,6 @@ namespace Ombi.Helpers
public const string LidarrRootFolders = nameof(LidarrRootFolders); public const string LidarrRootFolders = nameof(LidarrRootFolders);
public const string LidarrQualityProfiles = nameof(LidarrQualityProfiles); public const string LidarrQualityProfiles = nameof(LidarrQualityProfiles);
public const string FanartTv = nameof(FanartTv); public const string FanartTv = nameof(FanartTv);
public const string UsersDropdown = nameof(UsersDropdown);
} }
} }

@ -1,171 +0,0 @@
<div class="spinner-container">
<mat-spinner *ngIf="loading" [color]="'accent'"></mat-spinner>
</div>
<div *ngIf="!loading" mat-dialog-content class="background">
<div class="row">
<div class="col-4">
<a (click)="openDetails()">
<img id="cardImage" src="{{data.posterPath}}" class="poster" alt="{{data.title}}">
</a>
</div>
<div class="col-8">
<div class="row">
<div class="col-4 offset-8 text-right" id="icons">
<span *ngIf="movie">
<a *ngIf="movie.plexUrl" class="media-icons" href="{{movie.plexUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnPlex' | translate}}"
class="fas fa-play-circle fa-2x grow"></i>
</a>
<a *ngIf="movie.embyUrl" class="media-icons" href="{{movie.embyUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnEmby' | translate}}"
class="fas fa-play-circle fa-2x grow"></i>
</a>
<a *ngIf="movie.jellyfinUrl" class="media-icons" href="{{movie.jellyfinUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnJellyfin' | translate}}"
class="fas fa-play-circle fa-2x grow"></i>
</a>
</span>
<span *ngIf="tv">
<a *ngIf="tv.plexUrl" class="media-icons" href="{{tv.plexUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnPlex' | translate}}"
class="fas fa-play-circle fa-2x grow"></i>
</a>
<a *ngIf="tv.embyUrl" class="media-icons" href="{{tv.embyUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnEmby' | translate}}"
class="fas fa-play-circle fa-2x grow"></i>
</a>
<a *ngIf="tv.jellyfinUrl" class="media-icons" href="{{tv.jellyfinUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnJellyfin' | translate}}"
class="fas fa-play-circle fa-2x grow"></i>
</a>
</span>
<a class="media-icons" (click)="close()">
<i class="fas fa-window-close fa-2x grow"></i>
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<h3><strong>{{data.title}}</strong></h3>
</div>
</div>
<div class="row top-spacing details">
<div class="col-6">
<strong>{{'Discovery.CardDetails.Availability' | translate}}: </strong> <small>
<ng-template [ngIf]="data.available"><span class="label label-success" id="availableLabel"
[translate]="'Common.Available'"></span></ng-template>
<ng-template [ngIf]="!data.available"><span class="label label-success" id="availableLabel"
[translate]="'Common.NotAvailable'"></span></ng-template>
</small>
</div>
<div class="col-6">
<strong *ngIf="movie">{{'Discovery.CardDetails.Studio' | translate}}: </strong>
<small *ngIf="movie">{{movie.productionCompanies[0].name}}</small>
<strong *ngIf="tv">{{'Discovery.CardDetails.Network' | translate}}: </strong>
<small *ngIf="tv && tv.network">{{tv.network.name}}</small>
<small *ngIf="tv && !tv.network">{{'Discovery.CardDetails.UnknownNetwork' | translate}}</small>
</div>
<div class="col-6" *ngIf="!data.available">
<strong>{{'Discovery.CardDetails.RequestStatus' | translate}}: </strong> <small>
<ng-template [ngIf]="data.approved && !data.available"><span class="label label-info"
id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span>
</ng-template>
<ng-template [ngIf]="data.requested && !data.approved && !data.available"><span
class="label label-warning" id="pendingApprovalLabel"
[translate]="'Common.PendingApproval'"></span></ng-template>
<ng-template [ngIf]="!data.requested && !data.available && !data.approved"><span
class="label label-danger" id="notRequestedLabel"
[translate]="'Common.NotRequested'"></span></ng-template>
</small>
</div>
<div class="col-6">
<strong *ngIf="movie">{{'Discovery.CardDetails.Director' | translate}}: </strong>
<small *ngIf="movie">{{movie.credits.crew[0].name}}</small>
<strong *ngIf="tvCreator">Director: </strong>
<small *ngIf="tvCreator">{{tvCreator}}</small>
</div>
<div class="col-6">
<strong *ngIf="movie">{{'Discovery.CardDetails.InCinemas' | translate}}: </strong>
<small *ngIf="movie">{{movie.releaseDate | amLocal | amDateFormat: 'LL'}}</small>
<strong *ngIf="tv">{{'Discovery.CardDetails.FirstAired' | translate}}: </strong>
<small *ngIf="tv">{{tv.firstAired | amLocal | amDateFormat: 'LL'}}</small>
</div>
<div class="col-6">
<strong *ngIf="movie">{{'Discovery.CardDetails.Writer' | translate}}: </strong>
<small *ngIf="movie">{{movie.credits.crew[1].name}}</small>
<strong *ngIf="tv">{{'Discovery.CardDetails.ExecProducer' | translate}}: </strong>
<small *ngIf="tv">{{tvProducer}}</small>
</div>
</div>
<div class="row top-spacing overview">
<div class="col-12">
{{data.overview}}
</div>
</div>
</div>
</div>
</div>
<div mat-dialog-actions>
<div class="action-buttons-right">
<div class="col-md-12" *ngIf="movie">
<button mat-raised-button class="btn-green btn-spacing" *ngIf="movie.available"> {{
'Common.Available' | translate }}</button>
<span *ngIf="!movie.available">
<span *ngIf="movie.requested || movie.approved; then requestedBtn else notRequestedBtn"></span>
<ng-template #requestedBtn>
<button mat-raised-button class="btn-spacing btn-orange" [disabled]><i class="fas fa-check"></i>
{{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<i *ngIf="movie.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i> <i
*ngIf="!movie.requestProcessing && !movie.processed" class="fas fa-plus"></i>
<i *ngIf="movie.processed && !movie.requestProcessing" class="fas fa-check"></i> {{
'Common.Request' | translate }}</button>
</ng-template>
</span>
</div>
<div class="col-md-12" *ngIf="tv">
<div *ngIf="!tv.fullyAvailable" class="dropdown">
<button mat-raised-button class="btn-orange btn-spacing" type="button" (click)="request()">
<i class="fas fa-plus"></i>
{{ 'Common.Request' | translate }}
<span class="caret"></span>
</button>
</div>
<button *ngIf="tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent" [disabled]>
<i class="fas fa-check"></i> {{'Common.Available' | translate }}</button>
<button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent"
[disabled]>
<i class="fas fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>
<span *ngIf="tv.available">
<a *ngIf="tv.plexUrl" mat-raised-button style="text-align: right" class="btn-spacing btn-greem"
href="{{tv.plexUrl}}" target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnPlex' |
translate}}</a>
<a *ngIf="tv.embyUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.embyUrl}}"
target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="tv.jellyfinUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.jellyfinUrl}}"
target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</span>
<button mat-raised-button class="btn-green btn-spacing" (click)="openDetails()"> {{
'Common.ViewDetails' | translate }}</button>
</div>
</div>
</div>

@ -1,45 +0,0 @@
@import "~styles/variables.scss";
.poster {
max-width: 100%;
border-radius: 2%;
}
.details {
padding: 2%;
border-radius: 10px;
background: $backgroundTint;
div.dark & {
background: $backgroundTint-dark;
}
}
.details strong {
font-weight: bold;
}
h3 strong {
font-weight: bold;
}
.action-buttons-right {
width: 100%;
text-align: right;
}
.btn-spacing {
margin-right: 1%;
}
.media-icons {
color: $primary;
padding: 2%;
div.dark & {
color: $warn-dark;
}
}
.overview {
height:300px;
overflow-y: auto;
}

@ -1,86 +0,0 @@
import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core";
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from "@angular/material/dialog";
import { IDiscoverCardResult } from "../../interfaces";
import { SearchV2Service, RequestService, MessageService } from "../../../services";
import { RequestType } from "../../../interfaces";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2";
import { Router } from "@angular/router";
import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component";
@Component({
selector: "discover-card-details",
templateUrl: "./discover-card-details.component.html",
styleUrls: ["./discover-card-details.component.scss"],
encapsulation: ViewEncapsulation.None,
})
export class DiscoverCardDetailsComponent implements OnInit {
public movie: ISearchMovieResultV2;
public tv: ISearchTvResultV2;
public tvCreator: string;
public tvProducer: string;
public loading: boolean;
public RequestType = RequestType;
constructor(
public dialogRef: MatDialogRef<DiscoverCardDetailsComponent>,
@Inject(MAT_DIALOG_DATA) public data: IDiscoverCardResult, private searchService: SearchV2Service, private dialog: MatDialog,
private requestService: RequestService, public messageService: MessageService, private router: Router) { }
public async ngOnInit() {
this.loading = true;
if (this.data.type === RequestType.movie) {
this.movie = await this.searchService.getFullMovieDetailsPromise(+this.data.id);
} else if (this.data.type === RequestType.tvShow) {
this.tv = await this.searchService.getTvInfo(+this.data.id);
const creator = this.tv.crew.filter(tv => {
return tv.type === "Creator";
})[0];
if (creator && creator.person) {
this.tvCreator = creator.person.name;
}
const crewResult = this.tv.crew.filter(tv => {
return tv.type === "Executive Producer";
})[0]
if (crewResult && crewResult.person) {
this.tvProducer = crewResult.person.name;
}
}
this.loading = false;
}
public close(): void {
this.dialogRef.close();
}
public openDetails() {
if (this.data.type === RequestType.movie) {
this.router.navigate(['/details/movie/', this.data.id]);
} else if (this.data.type === RequestType.tvShow) {
this.router.navigate(['/details/tv/', this.data.id]);
}
this.dialogRef.close();
}
public async request() {
this.loading = true;
if (this.data.type === RequestType.movie) {
const result = await this.requestService.requestMovie({ theMovieDbId: +this.data.id, languageCode: "", requestOnBehalf: null }).toPromise();
this.loading = false;
if (result.result) {
this.movie.requested = true;
this.messageService.send(result.message, "Ok");
} else {
this.messageService.send(result.errorMessage, "Ok");
}
} else if (this.data.type === RequestType.tvShow) {
this.dialog.open(EpisodeRequestComponent, { width: "700px", data: {series: this.tv }, panelClass: 'modal-panel' })
}
this.loading = false;
this.dialogRef.close();
}
}

@ -3,10 +3,10 @@ import { IDiscoverCardResult } from "../../interfaces";
import { RequestType } from "../../../interfaces"; import { RequestType } from "../../../interfaces";
import { MessageService, RequestService, SearchV2Service } from "../../../services"; import { MessageService, RequestService, SearchV2Service } from "../../../services";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { DiscoverCardDetailsComponent } from "./discover-card-details.component";
import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component";
import { AdminRequestDialogComponent } from "../../../shared/admin-request-dialog/admin-request-dialog.component";
@Component({ @Component({
selector: "discover-card", selector: "discover-card",
@ -16,6 +16,7 @@ import { EpisodeRequestComponent } from "../../../shared/episode-request/episode
export class DiscoverCardComponent implements OnInit { export class DiscoverCardComponent implements OnInit {
@Input() public result: IDiscoverCardResult; @Input() public result: IDiscoverCardResult;
@Input() public isAdmin: boolean;
public RequestType = RequestType; public RequestType = RequestType;
public hide: boolean; public hide: boolean;
public fullyLoaded = false; public fullyLoaded = false;
@ -40,10 +41,6 @@ export class DiscoverCardComponent implements OnInit {
} }
} }
public openDetails(details: IDiscoverCardResult) {
this.dialog.open(DiscoverCardDetailsComponent, { width: "700px", data: details, panelClass: 'modal-panel' })
}
public async getExtraTvInfo() { public async getExtraTvInfo() {
// if (this.result.tvMovieDb) { // if (this.result.tvMovieDb) {
this.tvSearchResult = await this.searchService.getTvInfoWithMovieDbId(+this.result.id); this.tvSearchResult = await this.searchService.getTvInfoWithMovieDbId(+this.result.id);
@ -125,7 +122,10 @@ export class DiscoverCardComponent implements OnInit {
dia.afterClosed().subscribe(x => this.loading = false); dia.afterClosed().subscribe(x => this.loading = false);
return; return;
case RequestType.movie: case RequestType.movie:
this.requestService.requestMovie({ theMovieDbId: +this.result.id, languageCode: null, requestOnBehalf: null }).subscribe(x => { if (this.isAdmin) {
this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.movie, id: this.result.id }, panelClass: 'modal-panel' });
} else {
this.requestService.requestMovie({ theMovieDbId: +this.result.id, languageCode: null, requestOnBehalf: null, qualityPathOverride: null, rootFolderOverride: null }).subscribe(x => {
if (x.result) { if (x.result) {
this.result.requested = true; this.result.requested = true;
this.messageService.send(x.message, "Ok"); this.messageService.send(x.message, "Ok");
@ -135,6 +135,7 @@ export class DiscoverCardComponent implements OnInit {
this.loading = false; this.loading = false;
}); });
return; return;
}
} }
} }

@ -8,6 +8,6 @@
<p-carousel #carousel [numVisible]="10" [numScroll]="10" [page]="0" [value]="discoverResults" [responsiveOptions]="responsiveOptions" (onPage)="newPage()"> <p-carousel #carousel [numVisible]="10" [numScroll]="10" [page]="0" [value]="discoverResults" [responsiveOptions]="responsiveOptions" (onPage)="newPage()">
<ng-template let-result pTemplate="item"> <ng-template let-result pTemplate="item">
<discover-card [result]="result"></discover-card> <discover-card [isAdmin]="isAdmin" [result]="result"></discover-card>
</ng-template> </ng-template>
</p-carousel> </p-carousel>

@ -22,6 +22,7 @@ export class CarouselListComponent implements OnInit {
@Input() public discoverType: DiscoverType; @Input() public discoverType: DiscoverType;
@Input() public id: string; @Input() public id: string;
@Input() public isAdmin: boolean;
@ViewChild('carousel', {static: false}) carousel: Carousel; @ViewChild('carousel', {static: false}) carousel: Carousel;
public DiscoverOption = DiscoverOption; public DiscoverOption = DiscoverOption;

@ -34,7 +34,7 @@ export class DiscoverCollectionsComponent implements OnInit {
public async requestCollection() { public async requestCollection() {
await this.collection.collection.forEach(async (movie) => { await this.collection.collection.forEach(async (movie) => {
await this.requestService.requestMovie({theMovieDbId: movie.id, languageCode: null, requestOnBehalf: null}).toPromise(); await this.requestService.requestMovie({theMovieDbId: movie.id, languageCode: null, requestOnBehalf: null, qualityPathOverride: null, rootFolderOverride: null}).toPromise();
}); });
this.messageService.send("Requested Collection"); this.messageService.send("Requested Collection");
} }

@ -2,7 +2,7 @@
<div class="section"> <div class="section">
<h2>{{'Discovery.PopularTab' | translate}}</h2> <h2>{{'Discovery.PopularTab' | translate}}</h2>
<div> <div>
<carousel-list [id]="'popular'" [discoverType]="DiscoverType.Popular"></carousel-list> <carousel-list [id]="'popular'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Popular"></carousel-list>
</div> </div>
</div> </div>
@ -10,7 +10,7 @@
<div class="section"> <div class="section">
<h2>{{'Discovery.TrendingTab' | translate}}</h2> <h2>{{'Discovery.TrendingTab' | translate}}</h2>
<div > <div >
<carousel-list [id]="'trending'" [discoverType]="DiscoverType.Trending"></carousel-list> <carousel-list [id]="'trending'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Trending"></carousel-list>
</div> </div>
</div> </div>
@ -18,7 +18,7 @@
<div class="section"> <div class="section">
<h2>{{'Discovery.UpcomingTab' | translate}}</h2> <h2>{{'Discovery.UpcomingTab' | translate}}</h2>
<div> <div>
<carousel-list [id]="'upcoming'" [discoverType]="DiscoverType.Upcoming"></carousel-list> <carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
</div> </div>
</div> </div>
<!-- <div class="section"> <!-- <div class="section">

@ -1,15 +1,21 @@
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../../auth/auth.service";
import { DiscoverType } from "../carousel-list/carousel-list.component"; import { DiscoverType } from "../carousel-list/carousel-list.component";
@Component({ @Component({
templateUrl: "./discover.component.html", templateUrl: "./discover.component.html",
styleUrls: ["./discover.component.scss"], styleUrls: ["./discover.component.scss"],
}) })
export class DiscoverComponent { export class DiscoverComponent implements OnInit {
public DiscoverType = DiscoverType; public DiscoverType = DiscoverType;
public isAdmin: boolean;
constructor() { } constructor(private authService: AuthService) { }
public ngOnInit(): void {
this.isAdmin = this.authService.isAdmin();
}
} }

@ -1,168 +0,0 @@
<!-- <div class="card-spacing" *ngIf="result">
<mat-card class="mat-elevation-z8 dark-card grow">
<a [routerLink]="result.type === RequestType.movie ? '/details/movie/' + result.id : '/details/tv/' + result.id">
<img id="cardImage" mat-card-image src="{{result.posterPath}}" class="card-poster" [ngClass]="getStatusClass()" alt="{{result.title}}">
</a>
<mat-card-content>
<h6 *ngIf="result.title.length <= 20">{{result.title}}</h6>
<h6 *ngIf="result.title.length > 20" matTooltip="{{result.title}}">{{result.title | truncate:20}}</h6>
<div class="fade-text">
<small class="overview-text">{{result.overview | truncate: 75}}</small>
</div>
</mat-card-content>
</mat-card>
</div> -->
<div class="top-spacing">
<mat-card class="mat-elevation-z8 dark-card backdrop" [style.background-image]="result.background">
<div class="row main-container">
<div class="col-md-2 col-12">
<img src="{{result.posterPath}}" class="card-poster" alt="{{result.title}}">
</div>
<div class="col-md-8 col-12">
<div class="row">
<h1>{{result.title}}</h1>
</div>
<div class="row">
<mat-chip-list>
<mat-chip *ngIf="result.available" class="available">
{{'Common.Available' | translate}}
</mat-chip>
<mat-chip *ngIf="result.approved && !result.available" class="approved">
{{'Common.ProcessingRequest' | translate}}
</mat-chip>
<mat-chip *ngIf="result.denied" class="denied">
{{'Common.RequestDenied' | translate}}
</mat-chip>
<mat-chip *ngIf="result.requested && !result.approved && !result.available && !result.denied"
class="requested">
{{'Common.PendingApproval' | translate}}
</mat-chip>
<mat-chip *ngIf="movie && movie.plexUrl"> <a href="{{movie.plexUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnPlex' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="movie && movie.embyUrl"> <a href="{{movie.embyUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnEmby' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="movie && movie.jellyfinUrl"> <a href="{{movie.jellyfinUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnJellyfin' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="tv && tv.plexUrl"> <a href="{{tv.plexUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnPlex' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="tv &&tv.embyUrl"> <a href="{{movie.embyUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnEmby' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="tv &&tv.jellyfinUrl"> <a href="{{movie.jellyfinUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnJellyfin' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
</mat-chip-list>
</div>
<div class="row">
<mat-chip-list class="top-spacing">
<mat-chip *ngIf="movie && movie.productionCompanies[0]?.name">
{{'Discovery.CardDetails.Studio' | translate}}: {{movie.productionCompanies[0].name}}
</mat-chip>
<mat-chip *ngIf="tv && tv.network?.name">{{'Discovery.CardDetails.Network' | translate}}:
{{tv.network.name}}</mat-chip>
<mat-chip *ngIf="movie && movie.credits?.crew[0]?.name">
{{'Discovery.CardDetails.Director' | translate}}: {{movie.credits.crew[0].name}}</mat-chip>
<mat-chip *ngIf="tvCreator">Director: {{tvCreator}}</mat-chip>
<mat-chip *ngIf="movie">{{'Discovery.CardDetails.InCinemas' | translate}}:
{{movie.releaseDate | amLocal | amDateFormat: 'LL'}}</mat-chip>
<mat-chip *ngIf="tv">{{'Discovery.CardDetails.FirstAired' | translate}}:
{{tv.firstAired | amLocal | amDateFormat: 'LL'}}</mat-chip>
<mat-chip *ngIf="movie && movie.credits?.crew[1]?.name">
{{'Discovery.CardDetails.Writer' | translate}}: {{movie.credits.crew[1].name}}</mat-chip>
<mat-chip *ngIf="tv">{{'Discovery.CardDetails.ExecProducer' | translate}}: {{tvProducer}}
</mat-chip>
</mat-chip-list>
</div>
<div class="row">
<p class="overview top-spacing">{{result.overview}}</p>
</div>
</div>
<div class="col-md-2 col-12">
<div style="float:right;">
<button mat-raised-button class="btn-green btn-spacing" (click)="openDetails()"> {{
'Common.ViewDetails' | translate }}</button>
<div *ngIf="movie">
<button mat-raised-button class="btn-green btn-spacing" *ngIf="movie.available"> {{
'Common.Available' | translate }}</button>
<span *ngIf="!movie.available">
<span
*ngIf="movie.requested || movie.approved; then requestedBtn else notRequestedBtn"></span>
<ng-template #requestedBtn>
<button mat-raised-button class="btn-spacing btn-orange" [disabled]><i
class="fas fa-check"></i>
{{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<i *ngIf="movie.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!movie.requestProcessing && !movie.processed" class="fas fa-plus"></i>
<i *ngIf="movie.processed && !movie.requestProcessing" class="fas fa-check"></i> {{
'Common.Request' | translate }}</button>
</ng-template>
</span>
</div>
<div *ngIf="tv">
<div *ngIf="!tv.fullyAvailable" class="dropdown">
<button mat-raised-button class="btn-orange btn-spacing" type="button" (click)="request()">
<i class="fas fa-plus"></i>
{{ 'Common.Request' | translate }}
<span class="caret"></span>
</button>
</div>
<button *ngIf="tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent"
[disabled]>
<i class="fas fa-check"></i> {{'Common.Available' | translate }}</button>
<button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" mat-raised-button class="btn-spacing"
color="accent" [disabled]>
<i class="fas fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>
<span *ngIf="tv.available">
<a *ngIf="tv.plexUrl" mat-raised-button style="text-align: right"
class="btn-spacing btn-greem" href="{{tv.plexUrl}}" target="_blank"><i
class="far fa-eye"></i> {{'Search.ViewOnPlex' |
translate}}</a>
<a *ngIf="tv.embyUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.embyUrl}}"
target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="tv.jellyfinUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.jellyfinUrl}}"
target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</span>
</div>
</div>
</div>
</div>
</mat-card>
</div>

@ -1,137 +0,0 @@
$ombi-primary:#3f3f3f;
$card-background: #2b2b2b;
$blue: #1976D2;
$pink: #C2185B;
$green:#1DE9B6;
$orange:#F57C00;
.btn-blue {
background-color: $blue;
}
.btn-pink {
background-color: $pink;
}
.btn-green {
background-color: $green;
}
.btn-orange {
background-color: $orange;
}
.btn-spacing {
margin-top:10%;
}
#cardImage {
border-radius: 5px 5px 0px 0px;
height: 75%;
}
.dark-card {
border-radius: 8px;
}
// Changed height to 100% to make all cards the same height
.top-spacing {
margin-top: 1%;
}
.card-poster {
width: 100%;
border-radius: 8px 0px 0px 8px;
margin-top: -6.5%;
margin-bottom: -6.6%;
}
.main-container {
margin-left: -2%;
}
.rating {
position: absolute;
font-weight: bold;
}
$border-width: 3px;
.available {
background-color: #1DE9B6 !important;
color: black !important;
}
.approved {
background-color: #ff5722 !important;
}
.requested {
background-color: #ffd740 !important;
color: black !important;
}
.denied {
background-color: #C2185B !important;
}
.notrequested {
background-color: #303030 !important;
}
.expand {
text-align: center;
}
@media (min-width: 1025px) {
// Changed height to 100% to make all cards the same height
.grow {
transition: all .2s ease-in-out;
height: 100%;
}
.grow:hover {
transform: scale(1.1);
}
}
::ng-deep mat-dialog-container.mat-dialog-container {
// background-color: $ombi-primary;
// color: white;
border-radius: 2%
}
/* Title adjust for the Discover page */
.mat-card-content h6 {
overflow: hidden;
white-space: nowrap;
font-weight: 400;
font-size: 1.1rem;
}
/* Summary adjust for Discover page */
.small,
small {
font-size: 0.8rem;
}
@media (min-width: 2000px) {
#cardImage {
height: 80%;
object-fit: cover;
display: block;
}
}
.overview {
font-size: 1.2em;
}
.backdrop {
background-position: 50% 33%;
background-size: cover;
}

@ -1,154 +0,0 @@
import { Component, OnInit, Input } from "@angular/core";
import { IDiscoverCardResult } from "../../interfaces";
import { RequestType, ISearchTvResult, ISearchMovieResult, ISearchMovieResultContainer } from "../../../interfaces";
import { ImageService, RequestService, SearchV2Service } from "../../../services";
import { MatDialog } from "@angular/material/dialog";
import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { EpisodeRequestComponent, EpisodeRequestData } from "../../../shared/episode-request/episode-request.component";
import { MatSnackBar } from "@angular/material/snack-bar";
import { Router } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
@Component({
selector: "discover-grid",
templateUrl: "./discover-grid.component.html",
styleUrls: ["./discover-grid.component.scss"],
})
export class DiscoverGridComponent implements OnInit {
@Input() public result: IDiscoverCardResult;
public RequestType = RequestType;
public requesting: boolean;
public tv: ISearchTvResultV2;
public tvCreator: string;
public tvProducer: string;
public movie: ISearchMovieResultV2;
constructor(private searchService: SearchV2Service, private dialog: MatDialog,
private requestService: RequestService, private notification: MatSnackBar,
private router: Router, private sanitizer: DomSanitizer, private imageService: ImageService) { }
public ngOnInit() {
if (this.result.type == RequestType.tvShow) {
this.getExtraTvInfo();
}
if (this.result.type == RequestType.movie) {
this.getExtraMovieInfo();
}
}
public async getExtraTvInfo() {
this.tv = await this.searchService.getTvInfo(+this.result.id);
this.setTvDefaults(this.tv);
this.updateTvItem(this.tv);
const creator = this.tv.crew.filter(tv => {
return tv.type === "Creator";
})[0];
if (creator && creator.person) {
this.tvCreator = creator.person.name;
}
const crewResult = this.tv.crew.filter(tv => {
return tv.type === "Executive Producer";
})[0]
if (crewResult && crewResult.person) {
this.tvProducer = crewResult.person.name;
}
this.setTvBackground();
}
public openDetails() {
if (this.result.type === RequestType.movie) {
this.router.navigate(['/details/movie/', this.result.id]);
} else if (this.result.type === RequestType.tvShow) {
this.router.navigate(['/details/tv/', this.result.id]);
}
}
public getStatusClass(): string {
if (this.result.available) {
return "available";
}
if (this.result.approved) {
return "approved";
}
if (this.result.requested) {
return "requested";
}
return "notrequested";
}
private getExtraMovieInfo() {
this.searchService.getFullMovieDetails(+this.result.id)
.subscribe(m => {
this.movie = m;
this.updateMovieItem(m);
});
this.setMovieBackground()
}
private setMovieBackground(): void {
this.result.background = this.sanitizer.bypassSecurityTrustStyle
("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(" + "https://image.tmdb.org/t/p/original" + this.result.background + ")");
}
private setTvBackground(): void {
if (this.result.background != null) {
this.result.background = this.sanitizer.bypassSecurityTrustStyle
("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(https://image.tmdb.org/t/p/original" + this.result.background + ")");
} else {
this.imageService.getTvBanner(+this.result.id).subscribe(x => {
if (x) {
this.result.background = this.sanitizer.bypassSecurityTrustStyle
("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(" + x + ")");
}
});
}
}
private updateMovieItem(updated: ISearchMovieResultV2) {
this.result.url = "http://www.imdb.com/title/" + updated.imdbId + "/";
this.result.available = updated.available;
this.result.requested = updated.requested;
this.result.requested = updated.requestProcessing;
this.result.rating = updated.voteAverage;
}
private setTvDefaults(x: ISearchTvResultV2) {
if (!x.imdbId) {
x.imdbId = "https://www.tvmaze.com/shows/" + x.seriesId;
} else {
x.imdbId = "http://www.imdb.com/title/" + x.imdbId + "/";
}
}
private updateTvItem(updated: ISearchTvResultV2) {
this.result.title = updated.title;
this.result.id = updated.id;
this.result.available = updated.fullyAvailable;
this.result.posterPath = updated.banner;
this.result.requested = updated.requested;
this.result.url = updated.imdbId;
}
public async request() {
this.requesting = true;
if (this.result.type === RequestType.movie) {
const result = await this.requestService.requestMovie({ theMovieDbId: +this.result.id, languageCode: "", requestOnBehalf: null }).toPromise();
if (result.result) {
this.result.requested = true;
this.notification.open(result.message, "Ok");
} else {
this.notification.open(result.errorMessage, "Ok");
}
} else if (this.result.type === RequestType.tvShow) {
this.dialog.open(EpisodeRequestComponent, { width: "700px", data: <EpisodeRequestData>{ series: this.tv, requestOnBehalf: null }, panelClass: 'modal-panel' })
}
this.requesting = false;
}
}

@ -1,13 +1,11 @@
import { DiscoverComponent } from "./discover/discover.component"; import { DiscoverComponent } from "./discover/discover.component";
import { DiscoverCardDetailsComponent } from "./card/discover-card-details.component";
import { DiscoverCollectionsComponent } from "./collections/discover-collections.component"; import { DiscoverCollectionsComponent } from "./collections/discover-collections.component";
import { DiscoverActorComponent } from "./actor/discover-actor.component"; import { DiscoverActorComponent } from "./actor/discover-actor.component";
import { DiscoverCardComponent } from "./card/discover-card.component"; import { DiscoverCardComponent } from "./card/discover-card.component";
import { Routes } from "@angular/router"; import { Routes } from "@angular/router";
import { AuthGuard } from "../../auth/auth.guard"; import { AuthGuard } from "../../auth/auth.guard";
import { SearchService, RequestService } from "../../services"; import { SearchService, RequestService, SonarrService, RadarrService } from "../../services";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { DiscoverGridComponent } from "./grid/discover-grid.component";
import { DiscoverSearchResultsComponent } from "./search-results/search-results.component"; import { DiscoverSearchResultsComponent } from "./search-results/search-results.component";
import { CarouselListComponent } from "./carousel-list/carousel-list.component"; import { CarouselListComponent } from "./carousel-list/carousel-list.component";
import { RequestServiceV2 } from "../../services/requestV2.service"; import { RequestServiceV2 } from "../../services/requestV2.service";
@ -16,10 +14,8 @@ import { RequestServiceV2 } from "../../services/requestV2.service";
export const components: any[] = [ export const components: any[] = [
DiscoverComponent, DiscoverComponent,
DiscoverCardComponent, DiscoverCardComponent,
DiscoverCardDetailsComponent,
DiscoverCollectionsComponent, DiscoverCollectionsComponent,
DiscoverActorComponent, DiscoverActorComponent,
DiscoverGridComponent,
DiscoverSearchResultsComponent, DiscoverSearchResultsComponent,
CarouselListComponent, CarouselListComponent,
]; ];
@ -29,6 +25,8 @@ export const providers: any[] = [
MatDialog, MatDialog,
RequestService, RequestService,
RequestServiceV2, RequestServiceV2,
SonarrService,
RadarrService,
]; ];
export const routes: Routes = [ export const routes: Routes = [

@ -4,7 +4,7 @@
</div> </div>
<div *ngIf="discoverResults && discoverResults.length > 0" class="row full-height discoverResults col" > <div *ngIf="discoverResults && discoverResults.length > 0" class="row full-height discoverResults col" >
<div id="searchResults" class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults" data-test="searchResultsCount" attr.search-count="{{discoverResults.length}}"> <div id="searchResults" class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults" data-test="searchResultsCount" attr.search-count="{{discoverResults.length}}">
<discover-card [result]="result"></discover-card> <discover-card [isAdmin]="isAdmin" [result]="result"></discover-card>
</div> </div>
</div> </div>
<div *ngIf="!discoverResults || discoverResults.length === 0"> <div *ngIf="!discoverResults || discoverResults.length === 0">

@ -8,6 +8,7 @@ import { SearchFilter } from "../../../my-nav/SearchFilter";
import { StorageService } from "../../../shared/storage/storage-service"; import { StorageService } from "../../../shared/storage/storage-service";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { AuthService } from "../../../auth/auth.service";
@Component({ @Component({
templateUrl: "./search-results.component.html", templateUrl: "./search-results.component.html",
@ -18,6 +19,7 @@ export class DiscoverSearchResultsComponent implements OnInit {
public loadingFlag: boolean; public loadingFlag: boolean;
public searchTerm: string; public searchTerm: string;
public results: IMultiSearchResult[]; public results: IMultiSearchResult[];
public isAdmin: boolean;
public discoverResults: IDiscoverCardResult[] = []; public discoverResults: IDiscoverCardResult[] = [];
@ -26,7 +28,8 @@ export class DiscoverSearchResultsComponent implements OnInit {
constructor(private searchService: SearchV2Service, constructor(private searchService: SearchV2Service,
private route: ActivatedRoute, private route: ActivatedRoute,
private filterService: FilterService, private filterService: FilterService,
private store: StorageService) { private store: StorageService,
private authService: AuthService) {
this.route.params.subscribe((params: any) => { this.route.params.subscribe((params: any) => {
this.searchTerm = params.searchTerm; this.searchTerm = params.searchTerm;
this.clear(); this.clear();
@ -36,6 +39,7 @@ export class DiscoverSearchResultsComponent implements OnInit {
public async ngOnInit() { public async ngOnInit() {
this.loadingFlag = true; this.loadingFlag = true;
this.isAdmin = this.authService.isAdmin();
this.filterService.onFilterChange.subscribe(async x => { this.filterService.onFilterChange.subscribe(async x => {
if (!isEqual(this.filter, x)) { if (!isEqual(this.filter, x)) {

@ -168,10 +168,9 @@ export interface IEpisodesRequests {
selected: boolean; // This is for the UI only selected: boolean; // This is for the UI only
} }
export interface IMovieRequestModel { export interface IMovieRequestModel extends BaseRequestOptions {
theMovieDbId: number; theMovieDbId: number;
languageCode: string | undefined; languageCode: string | undefined;
requestOnBehalf: string | undefined;
} }
export interface IFilter { export interface IFilter {
@ -187,3 +186,9 @@ export enum FilterType {
Processing = 4, Processing = 4,
PendingApproval = 5, PendingApproval = 5,
} }
export class BaseRequestOptions {
requestOnBehalf: string | undefined;
rootFolderOverride: number | undefined;
qualityPathOverride: number | undefined;
}

@ -1,4 +1,4 @@
import { INewSeasonRequests } from "./IRequestModel"; import { BaseRequestOptions, INewSeasonRequests } from "./IRequestModel";
export interface ISearchTvResult { export interface ISearchTvResult {
id: number; id: number;
@ -47,12 +47,11 @@ export interface ITvRequestViewModelV2 extends ITvRequestViewModelBase {
} }
export interface ITvRequestViewModelBase { export interface ITvRequestViewModelBase extends BaseRequestOptions {
requestAll: boolean; requestAll: boolean;
firstSeason: boolean; firstSeason: boolean;
latestSeason: boolean; latestSeason: boolean;
seasons: ISeasonsViewModel[]; seasons: ISeasonsViewModel[];
requestOnBehalf: string | undefined;
} }
export interface ISeasonsViewModel { export interface ISeasonsViewModel {

@ -13,6 +13,7 @@ import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/m
import { RequestServiceV2 } from "../../../services/requestV2.service"; import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component"; import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
import { forkJoin } from "rxjs"; import { forkJoin } from "rxjs";
import { AdminRequestDialogComponent } from "../../../shared/admin-request-dialog/admin-request-dialog.component";
@Component({ @Component({
templateUrl: "./movie-details.component.html", templateUrl: "./movie-details.component.html",
@ -84,7 +85,10 @@ export class MovieDetailsComponent {
} }
public async request(userId?: string) { public async request(userId?: string) {
const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null, requestOnBehalf: userId }).toPromise(); if (this.isAdmin) {
this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.movie, id: this.movie.id }, panelClass: 'modal-panel' });
} else {
const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null, requestOnBehalf: userId, qualityPathOverride: 0, rootFolderOverride: 0 }).toPromise();
if (result.result) { if (result.result) {
this.movie.requested = true; this.movie.requested = true;
this.messageService.send(result.message, "Ok"); this.messageService.send(result.message, "Ok");
@ -92,6 +96,7 @@ export class MovieDetailsComponent {
this.messageService.send(result.errorMessage, "Ok"); this.messageService.send(result.errorMessage, "Ok");
} }
} }
}
public openDialog() { public openDialog() {
this.dialog.open(YoutubeTrailerComponent, { this.dialog.open(YoutubeTrailerComponent, {

@ -13,7 +13,7 @@ import { IStreamingData } from "../../../../interfaces/IStreams";
}) })
export class MovieInformationPanelComponent implements OnInit { export class MovieInformationPanelComponent implements OnInit {
constructor(private searchService: SearchV2Service, @Inject(APP_BASE_HREF) public baseUrl: string) { } constructor(private searchService: SearchV2Service, @Inject(APP_BASE_HREF) public internalBaseUrl: string) { }
@Input() public movie: ISearchMovieResultV2; @Input() public movie: ISearchMovieResultV2;
@Input() public request: IMovieRequests; @Input() public request: IMovieRequests;
@ -22,7 +22,12 @@ export class MovieInformationPanelComponent implements OnInit {
public ratings: IMovieRatings; public ratings: IMovieRatings;
public streams: IStreamingData[]; public streams: IStreamingData[];
public baseUrl: string;
public ngOnInit() { public ngOnInit() {
if (this.internalBaseUrl.length > 1) {
this.baseUrl = this.internalBaseUrl;
}
this.searchService.getRottenMovieRatings(this.movie.title, +this.movie.releaseDate.toString().substring(0,4)) this.searchService.getRottenMovieRatings(this.movie.title, +this.movie.releaseDate.toString().substring(0,4))
.subscribe(x => this.ratings = x); .subscribe(x => this.ratings = x);

@ -18,6 +18,6 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<button mat-button [mat-dialog-close]="" cdkFocusInitial>Close</button> <button mat-raised-button [mat-dialog-close]="" color="warn">Close</button>
<button mat-button [mat-dialog-close]="data" cdkFocusInitial>Save</button> <button mat-raised-button [mat-dialog-close]="data" color="accent" cdkFocusInitial>Save</button>
</div> </div>

@ -1,4 +1,5 @@
import { Component, ViewEncapsulation, Input, OnInit } from "@angular/core"; import { APP_BASE_HREF } from "@angular/common";
import { Component, ViewEncapsulation, Input, OnInit, Inject } from "@angular/core";
import { ITvRequests } from "../../../../../interfaces"; import { ITvRequests } from "../../../../../interfaces";
import { ITvRatings } from "../../../../../interfaces/IRatings"; import { ITvRatings } from "../../../../../interfaces/IRatings";
import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2";
@ -13,7 +14,7 @@ import { SearchV2Service } from "../../../../../services";
}) })
export class TvInformationPanelComponent implements OnInit { export class TvInformationPanelComponent implements OnInit {
constructor(private searchService: SearchV2Service) { } constructor(private searchService: SearchV2Service, @Inject(APP_BASE_HREF) public internalBaseUrl: string) { }
@Input() public tv: ISearchTvResultV2; @Input() public tv: ISearchTvResultV2;
@Input() public request: ITvRequests; @Input() public request: ITvRequests;
@ -24,8 +25,12 @@ export class TvInformationPanelComponent implements OnInit {
public seasonCount: number; public seasonCount: number;
public totalEpisodes: number = 0; public totalEpisodes: number = 0;
public nextEpisode: any; public nextEpisode: any;
public baseUrl: string;
public ngOnInit(): void { public ngOnInit(): void {
if (this.internalBaseUrl.length > 1) {
this.baseUrl = this.internalBaseUrl;
}
this.searchService.getRottenTvRatings(this.tv.title, +this.tv.firstAired.toString().substring(0,4)) this.searchService.getRottenTvRatings(this.tv.title, +this.tv.firstAired.toString().substring(0,4))
.subscribe(x => this.ratings = x); .subscribe(x => this.ratings = x);

@ -53,9 +53,10 @@
(click)="request()"><i class="fas fa-plus"></i> (click)="request()"><i class="fas fa-plus"></i>
{{ 'Common.Request' | translate }}</button> {{ 'Common.Request' | translate }}</button>
<button *ngIf="tv.fullyAvailable && !tv.partlyAvailable" id="availableBtn" mat-raised-button class="btn-spacing" color="accent" <button *ngIf="tv.fullyAvailable && !tv.partlyAvailable" id="availableBtn" mat-raised-button class="btn-spacing" color="accent"
[disabled]> [disabled]>
<i class="fas fa-check"></i> {{'Common.Available' | translate }}</button> <i class="fas fa-check"></i> {{'Common.Available' | translate }}</button>
<button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" id="partiallyAvailableBtn" mat-raised-button <button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" id="partiallyAvailableBtn" mat-raised-button
class="btn-spacing" color="accent" [disabled]> class="btn-spacing" color="accent" [disabled]>
<i class="fas fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button> <i class="fas fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>

@ -0,0 +1,82 @@
<form [formGroup]="form" *ngIf="form">
<h1>Advanced Options</h1>
<div class="alert alert-info" role="alert">
You can configure the request here, once requested it will be send to your DVR application and will be auto approved!
</div>
<div style="max-width: 0; max-height: 0; overflow: hidden;">
<input autofocus="true" />
</div>
<!-- User area -->
<h3>{{'MediaDetails.RequestOnBehalf' | translate }}</h3>
<mat-form-field class="example-full-width" appearance="outline" floatLabel=auto>
<mat-label>{{ 'MediaDetails.PleaseSelectUser' | translate}}</mat-label>
<input type="text"
matInput
formControlName="username"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{displayFn(option)}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<!-- End User area -->
<hr />
<!-- Sonarr -->
<div *ngIf="data.type === RequestType.tvShow">
<div>
<h3>Sonarr Overrides</h3>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select formControlName="sonarrPathId">
<mat-option *ngFor="let profile of sonarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div >
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
<mat-select formControlName="sonarrFolderId">
<mat-option *ngFor="let profile of sonarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- End Sonarr-->
<!-- Radarr -->
<div *ngIf="data.type === RequestType.movie">
<div>
<h3>Radarr Overrides</h3>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select formControlName="radarrPathId">
<mat-option *ngFor="let profile of radarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div mat-dialog-content>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
<mat-select formControlName="radarrFolderId">
<mat-option *ngFor="let profile of radarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- End Radarr-->
<div mat-dialog-actions>
<button mat-raised-button [mat-dialog-close]="" color="warn">{{ 'Common.Cancel' | translate }}</button>
<button mat-raised-button (click)="submitRequest()" color="accent">{{ 'Common.Request' | 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,124 @@
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { Observable } from "rxjs";
import { startWith, map } from "rxjs/operators";
import { IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUserDropdown, RequestType } from "../../interfaces";
import { IdentityService, MessageService, RadarrService, RequestService, SonarrService } from "../../services";
import { RequestServiceV2 } from "../../services/requestV2.service";
export interface IAdminRequestDialogData {
type: RequestType,
id: number
}
@Component({
selector: "admin-request-dialog",
templateUrl: "admin-request-dialog.component.html",
styleUrls: [ "admin-request-dialog.component.scss" ]
})
export class AdminRequestDialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<AdminRequestDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: IAdminRequestDialogData,
private requestServiceV2: RequestServiceV2,
private notificationService: MessageService,
private identityService: IdentityService,
private sonarrService: SonarrService,
private radarrService: RadarrService,
private requestService: RequestService,
private fb: FormBuilder
) {}
public form: FormGroup;
public RequestType = RequestType;
public options: IUserDropdown[];
public filteredOptions: Observable<IUserDropdown[]>;
public userId: string;
public radarrEnabled: boolean;
public sonarrProfiles: ISonarrProfile[];
public sonarrRootFolders: ISonarrRootFolder[];
public radarrProfiles: IRadarrProfile[];
public radarrRootFolders: IRadarrRootFolder[];
public async ngOnInit() {
this.form = this.fb.group({
username: [null],
sonarrPathId: [null],
sonarrFolderId: [null],
radarrPathId: [null],
radarrFolderId: [null]
})
this.options = await this.identityService.getUsersDropdown().toPromise();
this.filteredOptions = this.form.controls['username'].valueChanges.pipe(
startWith(""),
map((value) => this._filter(value))
);
if (this.data.type === RequestType.tvShow) {
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => {
this.sonarrProfiles = c;
});
this.sonarrService.getRootFoldersWithoutSettings().subscribe(c => {
this.sonarrRootFolders = c;
});
}
if (this.data.type === RequestType.movie) {
this.radarrEnabled = await this.radarrService.isRadarrEnabled();
if (this.radarrEnabled) {
this.radarrService.getQualityProfilesFromSettings().subscribe(c => {
this.radarrProfiles = c;
});
this.radarrService.getRootFoldersFromSettings().subscribe(c => {
this.radarrRootFolders = c;
});
}
}
}
public displayFn(user: IUserDropdown): string {
const username = user?.username ? user.username : "";
const email = user?.email ? `(${user.email})` : "";
return `${username} ${email}`;
}
private _filter(value: string | IUserDropdown): IUserDropdown[] {
const filterValue =
typeof value === "string"
? value.toLowerCase()
: value.username.toLowerCase();
return this.options.filter((option) =>
option.username.toLowerCase().includes(filterValue)
);
}
public async submitRequest() {
const model = this.form.value;
if (this.data.type === RequestType.movie) {
this.requestService.requestMovie({
qualityPathOverride: model.radarrPathId,
requestOnBehalf: model.username?.id,
rootFolderOverride: model.radarrFolderId,
theMovieDbId: this.data.id,
languageCode: null
}).subscribe((x) => {
if (x.result) {
this.notificationService.send(x.message, "Ok");
} else {
this.notificationService.send(x.errorMessage, "Ok");
}
this.dialogRef.close();
})
}
}
}

@ -1,6 +1,6 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { TranslateModule } from "@ngx-translate/core"; import { TranslateModule } from "@ngx-translate/core";
import { TruncateModule } from "@yellowspot/ng-truncate"; import { TruncateModule } from "@yellowspot/ng-truncate";
import { MomentModule } from "ngx-moment"; import { MomentModule } from "ngx-moment";
@ -37,15 +37,18 @@ import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatTabsModule } from "@angular/material/tabs"; import { MatTabsModule } from "@angular/material/tabs";
import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; import { EpisodeRequestComponent } from "./episode-request/episode-request.component";
import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component";
import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
IssuesReportComponent, IssuesReportComponent,
EpisodeRequestComponent, EpisodeRequestComponent,
DetailsGroupComponent, DetailsGroupComponent,
AdminRequestDialogComponent,
], ],
imports: [ imports: [
SidebarModule, SidebarModule,
ReactiveFormsModule,
FormsModule, FormsModule,
CommonModule, CommonModule,
InputSwitchModule, InputSwitchModule,
@ -85,6 +88,7 @@ import { DetailsGroupComponent } from "../issues/components/details-group/detail
MatProgressSpinnerModule, MatProgressSpinnerModule,
IssuesReportComponent, IssuesReportComponent,
EpisodeRequestComponent, EpisodeRequestComponent,
AdminRequestDialogComponent,
DetailsGroupComponent, DetailsGroupComponent,
TruncateModule, TruncateModule,
InputSwitchModule, InputSwitchModule,

@ -114,7 +114,7 @@
</mat-tab> </mat-tab>
<mat-tab label="Preferences"> Coming Soon... </mat-tab> <!-- <mat-tab label="Preferences"> Coming Soon... </mat-tab> -->
<mat-tab label="Mobile"> <mat-tab label="Mobile">

@ -142,8 +142,8 @@
background-color: $ombi-active; background-color: $ombi-active;
} }
hr{ hr {
border-top: 1px solid $ombi-background-primary; border-top: 1px solid $accent-dark;
} }
.form-control{ .form-control{

@ -67,7 +67,8 @@ namespace Ombi.Controllers.V1
IMovieRequestEngine movieRequestEngine, IMovieRequestEngine movieRequestEngine,
ITvRequestEngine tvRequestEngine, ITvRequestEngine tvRequestEngine,
IMusicRequestEngine musicEngine, IMusicRequestEngine musicEngine,
IUserDeletionEngine deletionEngine) IUserDeletionEngine deletionEngine,
ICacheService cacheService)
{ {
UserManager = user; UserManager = user;
Mapper = mapper; Mapper = mapper;
@ -95,10 +96,13 @@ namespace Ombi.Controllers.V1
_userQualityProfiles = userProfiles; _userQualityProfiles = userProfiles;
MusicRequestEngine = musicEngine; MusicRequestEngine = musicEngine;
_deletionEngine = deletionEngine; _deletionEngine = deletionEngine;
_cacheService = cacheService;
} }
private OmbiUserManager UserManager { get; } private OmbiUserManager UserManager { get; }
private readonly IUserDeletionEngine _deletionEngine; private readonly IUserDeletionEngine _deletionEngine;
private readonly ICacheService _cacheService;
private RoleManager<IdentityRole> RoleManager { get; } private RoleManager<IdentityRole> RoleManager { get; }
private IMapper Mapper { get; } private IMapper Mapper { get; }
private IEmailProvider EmailProvider { get; } private IEmailProvider EmailProvider { get; }
@ -289,8 +293,8 @@ namespace Ombi.Controllers.V1
[PowerUser] [PowerUser]
public async Task<IEnumerable<UserViewModelDropdown>> GetAllUsersDropdown() public async Task<IEnumerable<UserViewModelDropdown>> GetAllUsersDropdown()
{ {
var users = await UserManager.Users.Where(x => x.UserType != UserType.SystemUser) var users = await _cacheService.GetOrAdd(CacheKeys.UsersDropdown,
.ToListAsync(); async () => await UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync());
var model = new List<UserViewModelDropdown>(); var model = new List<UserViewModelDropdown>();

Loading…
Cancel
Save