Merge pull request #4112 from Ombi-app/feature/admin-request-options

Feature/admin request options
pull/4113/head v4.0.1275
Jamie 4 years ago committed by GitHub
commit 4c571101c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -67,21 +67,27 @@ namespace Ombi.Core.Engine
$"{movieInfo.Title}{(!string.IsNullOrEmpty(movieInfo.ReleaseDate) ? $" ({DateTime.Parse(movieInfo.ReleaseDate).Year})" : string.Empty)}";
var userDetails = await GetUser();
var canRequestOnBehalf = false;
var canRequestOnBehalf = model.RequestOnBehalf.HasValue();
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 to request on behalf of users!",
ErrorMessage = $"You do not have the correct permissions to request on behalf of users!"
};
}
Result = false,
Message = "You do not have the correct permissions!",
ErrorMessage = $"You do not have the correct permissions!"
};
}
var requestModel = new MovieRequests
@ -101,7 +107,9 @@ namespace Ombi.Core.Engine
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
Background = movieInfo.BackdropPath,
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");

@ -140,7 +140,7 @@ namespace Ombi.Core.Engine
ErrorMessage = "This has already been requested"
};
}
return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf);
return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault());
}
// This is a new request
@ -151,21 +151,27 @@ namespace Ombi.Core.Engine
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModelV2 tv)
{
var user = await GetUser();
var canRequestOnBehalf = false;
var canRequestOnBehalf = tv.RequestOnBehalf.HasValue();
if (tv.RequestOnBehalf.HasValue())
var isAdmin = await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, OmbiRoles.Admin);
if (tv.RequestOnBehalf.HasValue() && !isAdmin)
{
canRequestOnBehalf = await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, 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 ((tv.RootFolderOverride.HasValue || tv.QualityPathOverride.HasValue) && !isAdmin)
{
return new RequestEngineResult
{
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!"
};
}
Result = false,
Message = "You do not have the correct permissions!",
ErrorMessage = $"You do not have the correct permissions!"
};
}
var tvBuilder = new TvShowRequestBuilderV2(MovieDbApi);
@ -240,11 +246,11 @@ namespace Ombi.Core.Engine
ErrorMessage = "This has already been requested"
};
}
return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf);
return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault());
}
// This is a new request
var newRequest = tvBuilder.CreateNewRequest(tv);
var newRequest = tvBuilder.CreateNewRequest(tv, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault());
return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf);
}
@ -852,10 +858,18 @@ namespace Ombi.Core.Engine
}
}
private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf)
private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf, int rootFolder, int qualityProfile)
{
// Add the child
existingRequest.ChildRequests.Add(newRequest);
if (qualityProfile > 0)
{
existingRequest.QualityOverride = qualityProfile;
}
if (rootFolder > 0)
{
existingRequest.RootFolder = rootFolder;
}
await TvRepository.Update(existingRequest);

@ -217,7 +217,7 @@ namespace Ombi.Core.Helpers
}
public TvShowRequestBuilderV2 CreateNewRequest(TvRequestViewModelV2 tv)
public TvShowRequestBuilderV2 CreateNewRequest(TvRequestViewModelV2 tv, int rootPathOverride, int qualityOverride)
{
int.TryParse(TheMovieDbRecord.ExternalIds?.TvDbId, out var tvdbId);
NewRequest = new TvRequests
@ -232,7 +232,9 @@ namespace Ombi.Core.Helpers
TvDbId = tvdbId,
ChildRequests = new List<ChildRequests>(),
TotalSeasons = tv.Seasons.Count(),
Background = BackdropPath
Background = BackdropPath,
RootFolder = rootPathOverride,
QualityOverride = qualityOverride
};
NewRequest.ChildRequests.Add(ChildRequest);

@ -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
{
public class MovieRequestViewModel
public class MovieRequestViewModel : BaseRequestOptions
{
public int TheMovieDbId { get; set; }
public string LanguageCode { get; set; } = "en";
public string RequestOnBehalf { get; set; }
/// <summary>
/// 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 LatestSeason { get; set; }
@ -28,7 +28,5 @@ namespace Ombi.Core.Models.Requests
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
[JsonIgnore]
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 (season.Episodes.All(x => x.Available))
{yarn
{
season.SeasonAvailable = true;
}
}
@ -25,11 +25,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
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;
}
else
}
if (!search.FullyAvailable)
{
var airedButNotAvailable = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate <= DateTime.Now.Date && c.AirDate != DateTime.MinValue));

@ -208,6 +208,10 @@ namespace Ombi.Core.Senders
{
qualityToUse = model.ParentRequest.QualityOverride.Value;
}
if (model.ParentRequest.RootFolder.HasValue)
{
rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder.Value, s);
}
// Are we using v3 sonarr?
var sonarrV3 = s.V3;

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

@ -5,7 +5,7 @@
</div>
<div *ngIf="discoverResults" class="row full-height">
<div class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults">
<discover-card [result]="result"></discover-card>
<discover-card [isAdmin]="isAdmin" [result]="result"></discover-card>
</div>
</div>
</div>

@ -1,25 +1,29 @@
import { Component, AfterViewInit } from "@angular/core";
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { SearchV2Service } from "../../../services";
import { IActorCredits } from "../../../interfaces/ISearchTvResultV2";
import { IDiscoverCardResult } from "../../interfaces";
import { RequestType } from "../../../interfaces";
import { AuthService } from "../../../auth/auth.service";
@Component({
templateUrl: "./discover-actor.component.html",
styleUrls: ["./discover-actor.component.scss"],
})
export class DiscoverActorComponent implements AfterViewInit {
export class DiscoverActorComponent {
public actorId: number;
public actorCredits: IActorCredits;
public loadingFlag: boolean;
public isAdmin: boolean;
public discoverResults: IDiscoverCardResult[] = [];
constructor(private searchService: SearchV2Service,
private route: ActivatedRoute) {
private route: ActivatedRoute,
private auth: AuthService) {
this.route.params.subscribe((params: any) => {
this.actorId = params.actorId;
this.isAdmin = this.auth.isAdmin();
this.loading();
this.searchService.getMoviesByActor(this.actorId).subscribe(res => {
this.actorCredits = res;
@ -28,18 +32,6 @@ export class DiscoverActorComponent implements AfterViewInit {
});
}
public async ngAfterViewInit() {
// this.discoverResults.forEach((result) => {
// this.searchService.getFullMovieDetails(result.id).subscribe(x => {
// result.available = x.available;
// result.approved = x.approved;
// result.rating = x.voteAverage;
// result.requested = x.requested;
// result.url = x.homepage;
// });
// });
}
private createModel() {
this.finishLoading();
this.discoverResults = [];

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

@ -21,7 +21,7 @@
</div>
<div class="row button-request-container" *ngIf="!result.available && !result.approved && !result.requested">
<div class="button-request poster-overlay">
<button id="requestButton{{result.id}}{{result.type}}" mat-raised-button class="btn-green full-width poster-request-btn" (click)="request($event)">
<button id="requestButton{{result.id}}{{result.type}}{{discoverType}}" mat-raised-button class="btn-green full-width poster-request-btn" (click)="request($event)">
<i *ngIf="!loading" class="fa-lg fas fa-cloud-download-alt"></i>
<i *ngIf="loading" class="fas fa-spinner fa-pulse fa-2x fa-fw" aria-hidden="true"></i>
</button>

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

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

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

@ -15,7 +15,7 @@
</div>
<div *ngIf="discoverResults" class="row full-height">
<div class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults">
<discover-card [result]="result"></discover-card>
<discover-card [isAdmin]="isAdmins" [result]="result"></discover-card>
</div>
</div>
</div>

@ -4,6 +4,7 @@ import { SearchV2Service, RequestService, MessageService } from "../../../servic
import { IMovieCollectionsViewModel } from "../../../interfaces/ISearchTvResultV2";
import { IDiscoverCardResult } from "../../interfaces";
import { RequestType } from "../../../interfaces";
import { AuthService } from "../../../auth/auth.service";
@Component({
templateUrl: "./discover-collections.component.html",
@ -14,13 +15,15 @@ export class DiscoverCollectionsComponent implements OnInit {
public collectionId: number;
public collection: IMovieCollectionsViewModel;
public loadingFlag: boolean;
public isAdmin: boolean;
public discoverResults: IDiscoverCardResult[] = [];
constructor(private searchService: SearchV2Service,
private route: ActivatedRoute,
private requestService: RequestService,
private messageService: MessageService) {
private messageService: MessageService,
private auth: AuthService) {
this.route.params.subscribe((params: any) => {
this.collectionId = params.collectionId;
});
@ -28,13 +31,14 @@ export class DiscoverCollectionsComponent implements OnInit {
public async ngOnInit() {
this.loadingFlag = true;
this.isAdmin = this.auth.isAdmin();
this.collection = await this.searchService.getMovieCollections(this.collectionId);
this.createModel();
}
public async requestCollection() {
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");
}

@ -2,7 +2,7 @@
<div class="section">
<h2>{{'Discovery.PopularTab' | translate}}</h2>
<div>
<carousel-list [id]="'popular'" [discoverType]="DiscoverType.Popular"></carousel-list>
<carousel-list [id]="'popular'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Popular"></carousel-list>
</div>
</div>
@ -10,7 +10,7 @@
<div class="section">
<h2>{{'Discovery.TrendingTab' | translate}}</h2>
<div >
<carousel-list [id]="'trending'" [discoverType]="DiscoverType.Trending"></carousel-list>
<carousel-list [id]="'trending'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Trending"></carousel-list>
</div>
</div>
@ -18,7 +18,7 @@
<div class="section">
<h2>{{'Discovery.UpcomingTab' | translate}}</h2>
<div>
<carousel-list [id]="'upcoming'" [discoverType]="DiscoverType.Upcoming"></carousel-list>
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
</div>
</div>
<!-- <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";
@Component({
templateUrl: "./discover.component.html",
styleUrls: ["./discover.component.scss"],
})
export class DiscoverComponent {
export class DiscoverComponent implements OnInit {
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 { DiscoverCardDetailsComponent } from "./card/discover-card-details.component";
import { DiscoverCollectionsComponent } from "./collections/discover-collections.component";
import { DiscoverActorComponent } from "./actor/discover-actor.component";
import { DiscoverCardComponent } from "./card/discover-card.component";
import { Routes } from "@angular/router";
import { AuthGuard } from "../../auth/auth.guard";
import { SearchService, RequestService } from "../../services";
import { SearchService, RequestService, SonarrService, RadarrService } from "../../services";
import { MatDialog } from "@angular/material/dialog";
import { DiscoverGridComponent } from "./grid/discover-grid.component";
import { DiscoverSearchResultsComponent } from "./search-results/search-results.component";
import { CarouselListComponent } from "./carousel-list/carousel-list.component";
import { RequestServiceV2 } from "../../services/requestV2.service";
@ -16,10 +14,8 @@ import { RequestServiceV2 } from "../../services/requestV2.service";
export const components: any[] = [
DiscoverComponent,
DiscoverCardComponent,
DiscoverCardDetailsComponent,
DiscoverCollectionsComponent,
DiscoverActorComponent,
DiscoverGridComponent,
DiscoverSearchResultsComponent,
CarouselListComponent,
];
@ -29,6 +25,8 @@ export const providers: any[] = [
MatDialog,
RequestService,
RequestServiceV2,
SonarrService,
RadarrService,
];
export const routes: Routes = [

@ -4,7 +4,7 @@
</div>
<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}}">
<discover-card [result]="result"></discover-card>
<discover-card [isAdmin]="isAdmin" [result]="result"></discover-card>
</div>
</div>
<div *ngIf="!discoverResults || discoverResults.length === 0">

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

@ -2,4 +2,5 @@
result: boolean;
message: string;
errorMessage: string;
requestId: number | undefined;
}

@ -168,10 +168,9 @@ export interface IEpisodesRequests {
selected: boolean; // This is for the UI only
}
export interface IMovieRequestModel {
export interface IMovieRequestModel extends BaseRequestOptions {
theMovieDbId: number;
languageCode: string | undefined;
requestOnBehalf: string | undefined;
}
export interface IFilter {
@ -187,3 +186,9 @@ export enum FilterType {
Processing = 4,
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 {
id: number;
@ -47,12 +47,11 @@ export interface ITvRequestViewModelV2 extends ITvRequestViewModelBase {
}
export interface ITvRequestViewModelBase {
export interface ITvRequestViewModelBase extends BaseRequestOptions {
requestAll: boolean;
firstSeason: boolean;
latestSeason: boolean;
seasons: ISeasonsViewModel[];
requestOnBehalf: string | undefined;
}
export interface ISeasonsViewModel {

@ -9,6 +9,7 @@ export interface IUser {
emailAddress: string;
password: string;
userType: UserType;
userAlias: string;
lastLoggedIn: Date;
hasLoggedIn: boolean;
movieRequestLimit: number;

@ -20,11 +20,9 @@
[embyUrl]="movie.embyUrl"
[jellyfinUrl]="movie.jellyfinUrl"
[isAdmin]="isAdmin"
[canRequestOnBehalf]="!hasRequest && !movie.available"
[canShowAdvanced]="showAdvanced && movieRequest"
[type]="requestType"
(openTrailer)="openDialog()"
(onRequestBehalf)="openRequestOnBehalf()"
(onAdvancedOptions)="openAdvancedOptions()"
>
</social-icons>
@ -75,7 +73,7 @@
</ng-template>
</span>
<span *ngIf="isAdmin && hasRequest">
<button id="approveBtn" *ngIf="!movie.approved" (click)="approve()" mat-raised-button class="btn-spacing" color="accent">
<button id="approveBtn" *ngIf="!movie.approved " (click)="approve()" mat-raised-button class="btn-spacing" color="accent">
<i class="fas fa-plus"></i> {{ 'Common.Approve' | translate }}
</button>
<button id="markAvailableBtn" *ngIf="!movie.available" (click)="markAvailable()" mat-raised-button class="btn-spacing"

@ -13,6 +13,7 @@ import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/m
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
import { forkJoin } from "rxjs";
import { AdminRequestDialogComponent } from "../../../shared/admin-request-dialog/admin-request-dialog.component";
@Component({
templateUrl: "./movie-details.component.html",
@ -84,14 +85,37 @@ export class MovieDetailsComponent {
}
public async request(userId?: string) {
const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null, requestOnBehalf: userId }).toPromise();
if (this.isAdmin) {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.movie, id: this.movie.id }, panelClass: 'modal-panel' });
dialog.afterClosed().subscribe(async (result) => {
if (result) {
const requestResult = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId,
languageCode: null,
qualityPathOverride: result.radarrPathId,
requestOnBehalf: result.username?.id,
rootFolderOverride: result.radarrFolderId, }).toPromise();
if (requestResult.result) {
this.movie.requested = true;
this.movie.requestId = requestResult.requestId;
this.messageService.send(requestResult.message, "Ok");
this.movieRequest = await this.requestService.getMovieRequest(this.movie.requestId);
} else {
this.messageService.send(requestResult.errorMessage, "Ok");
}
}
});
} else {
const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null, requestOnBehalf: userId, qualityPathOverride: undefined, rootFolderOverride: undefined }).toPromise();
if (result.result) {
this.movie.requested = true;
this.movie.requestId = result.requestId;
this.movieRequest = await this.requestService.getMovieRequest(this.movie.requestId);
this.messageService.send(result.message, "Ok");
} else {
this.messageService.send(result.errorMessage, "Ok");
}
}
}
public openDialog() {
this.dialog.open(YoutubeTrailerComponent, {
@ -166,15 +190,6 @@ export class MovieDetailsComponent {
});
}
public async openRequestOnBehalf() {
const dialog = this.dialog.open(RequestBehalfComponent, { width: "700px", panelClass: 'modal-panel' })
await dialog.afterClosed().subscribe(async result => {
if (result) {
await this.request(result.id);
}
});
}
private loadBanner() {
this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => {
if (!this.movie.backdropPath) {

@ -2,7 +2,7 @@
<div class="rating medium-font">
<span *ngIf="movie.voteAverage"
matTooltip="{{'MediaDetails.Votes' | translate }} {{movie.voteCount | thousandShort: 1}}">
<img class="rating-small" src="{{baseUrl}}/images/tmdb-logo.svg"> {{movie.voteAverage | number:'1.0-1'}}/10
<img class="rating-small" src="{{baseUrl}}images/tmdb-logo.svg"> {{movie.voteAverage | number:'1.0-1'}}/10
</span>
<span *ngIf="ratings?.critics_rating && ratings?.critics_score">
<img class="rating-small"
@ -45,7 +45,7 @@
<div *ngIf="request">
<span class="label">{{'Requests.RequestedBy' | translate }}:</span>
{{request.requestedUser.userAlias}}
<span id="requestedByInfo">{{request.requestedUser.userAlias}}</span>
</div>
<div *ngIf="request">

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

@ -16,8 +16,10 @@
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>
<button mat-raised-button (click)="onNoClick()">{{'Common.Cancel' | translate}}</button>
<button mat-raised-button (click)="request()" color="accent" [mat-dialog-close]="userId" cdkFocusInitial>{{'Common.Request' | translate}}</button>
<div mat-dialog-actions class="right-buttons">
<button mat-raised-button (click)="onNoClick()" color="warn"><i class="fas fa-times"></i> {{'Common.Cancel' | translate}}</button>
<button mat-raised-button (click)="request()" color="accent" [mat-dialog-close]="userId" cdkFocusInitial><i class="fas fa-plus"></i> {{'Common.Request' | translate}}</button>
</div>

@ -30,10 +30,6 @@
<i class="fas fa-cog fa-2x "></i>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="openRequestOnBehalf()" [disabled]="!canRequestOnBehalf">
<i class="fas fa-user-friends icon-spacing"></i>
<span> {{'MediaDetails.RequestOnBehalf' | translate}}</span>
</button>
<button mat-menu-item [disabled]="!canShowAdvanced" (click)="openAdvancedOptions()">
<i class="fas fa-ticket-alt icon-spacing"></i>
<span *ngIf="type === RequestType.movie"> {{ 'MediaDetails.RadarrConfiguration' | translate}}</span>

@ -22,11 +22,9 @@ export class SocialIconsComponent {
@Input() type: RequestType;
@Input() isAdmin: boolean;
@Input() canRequestOnBehalf: boolean;
@Input() canShowAdvanced: boolean;
@Output() openTrailer: EventEmitter<any> = new EventEmitter();
@Output() onRequestBehalf: EventEmitter<any> = new EventEmitter();
@Output() onAdvancedOptions: EventEmitter<any> = new EventEmitter();
public RequestType = RequestType;
@ -36,10 +34,6 @@ export class SocialIconsComponent {
this.openTrailer.emit();
}
public openRequestOnBehalf() {
this.onRequestBehalf.emit();
}
public openAdvancedOptions() {
this.onAdvancedOptions.emit();
}

@ -18,6 +18,6 @@
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="" cdkFocusInitial>Close</button>
<button mat-button [mat-dialog-close]="data" cdkFocusInitial>Save</button>
<button mat-raised-button [mat-dialog-close]="" color="warn">Close</button>
<button mat-raised-button [mat-dialog-close]="data" color="accent" cdkFocusInitial>Save</button>
</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 { ITvRatings } from "../../../../../interfaces/IRatings";
import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2";
@ -13,7 +14,7 @@ import { SearchV2Service } from "../../../../../services";
})
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 request: ITvRequests;
@ -24,8 +25,12 @@ export class TvInformationPanelComponent implements OnInit {
public seasonCount: number;
public totalEpisodes: number = 0;
public nextEpisode: any;
public baseUrl: string;
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))
.subscribe(x => this.ratings = x);

@ -1,5 +1,5 @@
import { Component, Input } from "@angular/core";
import { IChildRequests, IEpisodesRequests, INewSeasonRequests, ISeasonsViewModel, ITvRequestViewModelV2, RequestType } from "../../../../../interfaces";
import { IChildRequests, IEpisodesRequests, INewSeasonRequests, IRequestEngineResult, ISeasonsViewModel, ITvRequestViewModelV2, RequestType } from "../../../../../interfaces";
import { RequestService } from "../../../../../services/request.service";
import { MessageService } from "../../../../../services";
import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component";
@ -7,6 +7,7 @@ import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2";
import { MatDialog } from "@angular/material/dialog";
import { SelectionModel } from "@angular/cdk/collections";
import { RequestServiceV2 } from "../../../../../services/requestV2.service";
import { AdminRequestDialogComponent } from "../../../../../shared/admin-request-dialog/admin-request-dialog.component";
@Component({
templateUrl: "./tv-request-grid.component.html",
@ -59,38 +60,21 @@ export class TvRequestGridComponent {
viewModel.seasons.push(seasonsViewModel);
});
const requestResult = await this.requestServiceV2.requestTv(viewModel).toPromise();
if (requestResult.result) {
this.notificationService.send(
`Request for ${this.tv.title} has been added successfully`);
debugger;
this.selection.clear();
if (this.tv.firstSeason) {
this.tv.seasonRequests[0].episodes.forEach(ep => {
ep.requested = true;
ep.requestStatus = "Common.PendingApproval";
});
}
if (this.tv.requestAll) {
this.tv.seasonRequests.forEach(season => {
season.episodes.forEach(ep => {
ep.requested = true;
ep.requestStatus = "Common.PendingApproval";
});
});
}
if (this.tv.latestSeason) {
this.tv.seasonRequests[this.tv.seasonRequests.length - 1].episodes.forEach(ep => {
ep.requested = true;
ep.requestStatus = "Common.PendingApproval";
});
}
if (this.isAdmin) {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.tvShow, id: this.tv.id }, panelClass: 'modal-panel' });
dialog.afterClosed().subscribe(async (result) => {
if (result) {
viewModel.requestOnBehalf = result.username?.id;
viewModel.qualityPathOverride = result?.sonarrPathId;
viewModel.rootFolderOverride = result?.sonarrFolderId;
const requestResult = await this.requestServiceV2.requestTv(viewModel).toPromise();
this.postRequest(requestResult);
}
});
} else {
this.notificationService.send(requestResult.errorMessage ? requestResult.errorMessage : requestResult.message);
const requestResult = await this.requestServiceV2.requestTv(viewModel).toPromise();
this.postRequest(requestResult);
}
}
@ -236,4 +220,37 @@ export class TvRequestGridComponent {
}
return "";
}
private postRequest(requestResult: IRequestEngineResult) {
if (requestResult.result) {
this.notificationService.send(
`Request for ${this.tv.title} has been added successfully`);
this.selection.clear();
if (this.tv.firstSeason) {
this.tv.seasonRequests[0].episodes.forEach(ep => {
ep.requested = true;
ep.requestStatus = "Common.PendingApproval";
});
}
if (this.tv.requestAll) {
this.tv.seasonRequests.forEach(season => {
season.episodes.forEach(ep => {
ep.requested = true;
ep.requestStatus = "Common.PendingApproval";
});
});
}
if (this.tv.latestSeason) {
this.tv.seasonRequests[this.tv.seasonRequests.length - 1].episodes.forEach(ep => {
ep.requested = true;
ep.requestStatus = "Common.PendingApproval";
});
}
} else {
this.notificationService.send(requestResult.errorMessage ? requestResult.errorMessage : requestResult.message);
}
}
}

@ -31,10 +31,8 @@
[embyUrl]="tv.embyUrl"
[jellyfinUrl]="tv.jellyfinUrl"
[isAdmin]="isAdmin"
[canRequestOnBehalf]="!showRequest"
[canShowAdvanced]="showAdvanced && showRequest"
[type]="requestType"
(onRequestBehalf)="openRequestOnBehalf()"
(onAdvancedOptions)="openAdvancedOptions()"
>
</social-icons>
@ -53,9 +51,10 @@
(click)="request()"><i class="fas fa-plus"></i>
{{ '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]>
<i class="fas fa-check"></i> {{'Common.Available' | translate }}</button>
<button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" id="partiallyAvailableBtn" mat-raised-button
class="btn-spacing" color="accent" [disabled]>
<i class="fas fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>

@ -76,7 +76,7 @@ export class TvDetailsComponent implements OnInit {
}
public async request(userId: string) {
this.dialog.open(EpisodeRequestComponent, { width: "800px", data: <EpisodeRequestData> { series: this.tv, requestOnBehalf: userId }, panelClass: 'modal-panel' })
this.dialog.open(EpisodeRequestComponent, { width: "800px", data: <EpisodeRequestData> { series: this.tv, requestOnBehalf: userId, isAdmin: this.isAdmin }, panelClass: 'modal-panel' })
}
public async issue() {
@ -108,15 +108,6 @@ export class TvDetailsComponent implements OnInit {
});
}
public async openRequestOnBehalf() {
const dialog = this.dialog.open(RequestBehalfComponent, { width: "700px", panelClass: 'modal-panel' })
await dialog.afterClosed().subscribe(async result => {
if (result) {
await this.request(result.id);
}
});
}
public setAdvancedOptions(data: IAdvancedData) {
this.advancedOptions = data;
console.log(this.advancedOptions);

@ -1,14 +1,14 @@

<button mat-button [matMenuTriggerFor]="configurationmenu"><i class="fas fa-wrench" aria-hidden="true"></i> Configuration</button>
<mat-menu #configurationmenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/Ombi']">General</button>
<button mat-menu-item [routerLink]="['/Settings/Customization']">Customization</button>
<button mat-menu-item [routerLink]="['/Settings/LandingPage']">Landing Page</button>
<button mat-menu-item [routerLink]="['/Settings/Issues']">Issues</button>
<button mat-menu-item [routerLink]="['/Settings/UserManagement']">User Management</button>
<button mat-menu-item [routerLink]="['/Settings/Authentication']">Authentication</button>
<button mat-menu-item [routerLink]="['/Settings/Ombi']"><i class="far fa-grin-stars icon-spacing"></i> General</button>
<button mat-menu-item [routerLink]="['/Settings/Customization']"><i class="fas fa-paint-brush icon-spacing"></i> Customization</button>
<button mat-menu-item [routerLink]="['/Settings/LandingPage']"><i class="far fa-file icon-spacing"></i> Landing Page</button>
<button mat-menu-item [routerLink]="['/Settings/Issues']"><i class="fas fa-exclamation-triangle icon-spacing"></i> Issues</button>
<button mat-menu-item [routerLink]="['/Settings/UserManagement']"><i class="fas fa-users-cog icon-spacing"></i> User Management</button>
<button mat-menu-item [routerLink]="['/Settings/Authentication']"><i class="fas fa-sign-in-alt icon-spacing"></i> Authentication</button>
<!-- <button mat-menu-item [routerLink]="['/Settings/Vote']">Vote</button> -->
<button mat-menu-item [routerLink]="['/Settings/TheMovieDb']">The Movie Database</button>
<button mat-menu-item [routerLink]="['/Settings/TheMovieDb']"><i class="fas fa-film icon-spacing"></i> The Movie Database</button>
</mat-menu>
<button mat-button [matMenuTriggerFor]="mediaservermenu"><i class="fas fa-server" aria-hidden="true"></i> Media Server</button>
@ -39,27 +39,27 @@
<button mat-button [matMenuTriggerFor]="notificationMenu"><i class="fas fa-bell" aria-hidden="true"></i> Notifications</button>
<mat-menu #notificationMenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/CloudMobile']">Mobile</button>
<button mat-menu-item [routerLink]="['/Settings/Mobile']">Legacy Mobile</button>
<button mat-menu-item [routerLink]="['/Settings/Email']">Email</button>
<button mat-menu-item [routerLink]="['/Settings/MassEmail']">MassEmail</button>
<button mat-menu-item [routerLink]="['/Settings/Newsletter']">Newsletter</button>
<button mat-menu-item [routerLink]="['/Settings/Discord']">Discord</button>
<button mat-menu-item [routerLink]="['/Settings/Slack']">Slack</button>
<button mat-menu-item [routerLink]="['/Settings/Pushbullet']">Pushbullet</button>
<button mat-menu-item [routerLink]="['/Settings/Pushover']">Pushover</button>
<button mat-menu-item [routerLink]="['/Settings/Mattermost']">Mattermost</button>
<button mat-menu-item [routerLink]="['/Settings/Telegram']">Telegram</button>
<button mat-menu-item [routerLink]="['/Settings/Gotify']">Gotify</button>
<button mat-menu-item [routerLink]="['/Settings/Twilio']">Twilio</button>
<button mat-menu-item [routerLink]="['/Settings/Webhook']">Webhook</button>
<button mat-menu-item [routerLink]="['/Settings/CloudMobile']"><i class="fas fa-mobile-alt icon-spacing"></i> Mobile</button>
<button mat-menu-item [routerLink]="['/Settings/Mobile']"><i class="fas fa-mobile icon-spacing"></i> Legacy Mobile</button>
<button mat-menu-item [routerLink]="['/Settings/Email']"> <i class="far fa-envelope icon-spacing"></i> Email</button>
<button mat-menu-item [routerLink]="['/Settings/MassEmail']"><i class="fas fa-mail-bulk icon-spacing"></i> MassEmail</button>
<button mat-menu-item [routerLink]="['/Settings/Newsletter']"><i class="fas fa-inbox icon-spacing"></i> Newsletter</button>
<button mat-menu-item [routerLink]="['/Settings/Discord']"><i class="fab fa-discord icon-spacing"></i> Discord</button>
<button mat-menu-item [routerLink]="['/Settings/Slack']"><i class="fab fa-slack icon-spacing"></i> Slack</button>
<button mat-menu-item [routerLink]="['/Settings/Pushbullet']"><i class="far fa-comments icon-spacing"></i> Pushbullet</button>
<button mat-menu-item [routerLink]="['/Settings/Pushover']"><i class="fas fa-comments icon-spacing"></i> Pushover</button>
<button mat-menu-item [routerLink]="['/Settings/Mattermost']"><i class="far fa-comments icon-spacing"></i> Mattermost</button>
<button mat-menu-item [routerLink]="['/Settings/Telegram']"><i class="fab fa-telegram icon-spacing"></i> Telegram</button>
<button mat-menu-item [routerLink]="['/Settings/Gotify']"><i class="fas fa-comments icon-spacing"></i> Gotify</button>
<button mat-menu-item [routerLink]="['/Settings/Twilio']"><i class="fas fa-sms icon-spacing"></i> Twilio</button>
<button mat-menu-item [routerLink]="['/Settings/Webhook']"><i class="fas fa-sync icon-spacing"></i> Webhook</button>
</mat-menu>
<button mat-button [matMenuTriggerFor]="systemMenu"><i class="fas fa-sliders-h" aria-hidden="true"></i> System</button>
<mat-menu #systemMenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/About']">About</button>
<button mat-menu-item [routerLink]="['/Settings/FailedRequests']">Failed Requests</button>
<button mat-menu-item [routerLink]="['/Settings/About']"><i class="fas fa-question icon-spacing"></i> About</button>
<button mat-menu-item [routerLink]="['/Settings/FailedRequests']"><i class="fas fa-times icon-spacing"></i> Failed Requests</button>
<!-- <button mat-menu-item [routerLink]="['/Settings/Update']">Update</button> -->
<button mat-menu-item [routerLink]="['/Settings/Jobs']">Scheduled Tasks</button>
<button mat-menu-item [routerLink]="['/Settings/Logs']">Logs</button>
<button mat-menu-item [routerLink]="['/Settings/Jobs']"><i class="fas fa-clock icon-spacing"></i> Scheduled Tasks</button>
<button mat-menu-item [routerLink]="['/Settings/Logs']"><i class="fas fa-stream icon-spacing"></i> Logs</button>
</mat-menu>

@ -0,0 +1,3 @@
.icon-spacing {
padding-right: 5%;
}

@ -2,6 +2,7 @@
@Component({
selector: "settings-menu",
templateUrl: "./settingsmenu.component.html",
styleUrls: ["./settingsmenu.component.scss"]
})
export class SettingsMenuComponent {
public ignore(event: any): void {

@ -0,0 +1,86 @@
<form [formGroup]="form" *ngIf="form">
<h1 id="advancedOptionsTitle"><i class="fas fa-sliders-h"></i> {{'MediaDetails.AdvancedOptions' | translate }}</h1>
<hr />
<div class="alert alert-info" role="alert">
<i class="fas fa-x7 fa-exclamation-triangle glyphicon"></i>
<span *ngIf="data.type === RequestType.movie">{{'MediaDetails.AutoApproveOptions' | translate }}</span>
<span *ngIf="data.type === RequestType.tvShow">{{'MediaDetails.AutoApproveOptionsTv' | translate }}</span>
</div>
<div style="max-width: 0; max-height: 0; overflow: hidden;">
<input autofocus="true" />
</div>
<!-- User area -->
<h3><i class="fas fa-user-friends"></i> {{'MediaDetails.RequestOnBehalf' | translate }}</h3>
<mat-form-field class="example-full-width" appearance="outline" floatLabel=auto>
<mat-label>{{ 'MediaDetails.PleaseSelectUser' | translate}}</mat-label>
<input id="requestOnBehalfUserInput"
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 -->
<!-- Sonarr -->
<div *ngIf="data.type === RequestType.tvShow && sonarrEnabled"><hr />
<div>
<h3>Sonarr Overrides</h3>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select id="sonarrQualitySelect" formControlName="sonarrPathId">
<mat-option id="sonarrQualitySelect{{profile.id}}" *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 id="sonarrFolderSelect" formControlName="sonarrFolderId">
<mat-option id="sonarrFolderSelect{{profile.id}}" *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 && radarrEnabled"><hr />
<div>
<h3>Radarr Overrides</h3>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select id="radarrQualitySelect" formControlName="radarrPathId">
<mat-option id="radarrQualitySelect{{profile.id}}" *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 id="radarrFolderSelect" formControlName="radarrFolderId">
<mat-option id="radarrFolderSelect{{profile.id}}" *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 class="right-buttons">
<button mat-raised-button id="cancelButton" [mat-dialog-close]="" color="warn"><i class="fas fa-times"></i> {{ 'Common.Cancel' | translate }}</button>
<button mat-raised-button id="requestButton" (click)="submitRequest()" color="accent"><i class="fas fa-plus"></i> {{ '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,111 @@
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 identityService: IdentityService,
private sonarrService: SonarrService,
private radarrService: RadarrService,
private fb: FormBuilder
) {}
public form: FormGroup;
public RequestType = RequestType;
public options: IUserDropdown[];
public filteredOptions: Observable<IUserDropdown[]>;
public userId: string;
public radarrEnabled: boolean;
public sonarrEnabled: 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.sonarrEnabled = await this.sonarrService.isEnabled();
if (this.sonarrEnabled) {
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;
model.radarrQualityOverrideTitle = this.radarrProfiles?.filter(x => x.id == model.radarrPathId)[0]?.name;
model.radarrRootFolderTitle = this.radarrRootFolders?.filter(x => x.id == model.radarrFolderId)[0]?.path;
model.sonarrRootFolderTitle = this.sonarrRootFolders?.filter(x => x.id == model.sonarrFolderId)[0]?.path;
model.sonarrQualityOverrideTitle = this.sonarrProfiles?.filter(x => x.id == model.sonarrPathId)[0]?.name;
this.dialogRef.close(model);
}
}

@ -1,13 +1,15 @@
import { Component, Inject } from "@angular/core";
import { MatCheckboxChange } from "@angular/material/checkbox";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { ISearchTvResultV2 } from "../../interfaces/ISearchTvResultV2";
import { MessageService } from "../../services";
import { ISeasonsViewModel, IEpisodesRequests, INewSeasonRequests, ITvRequestViewModelV2 } from "../../interfaces";
import { ISeasonsViewModel, IEpisodesRequests, INewSeasonRequests, ITvRequestViewModelV2, IRequestEngineResult, RequestType } from "../../interfaces";
import { RequestServiceV2 } from "../../services/requestV2.service";
import { AdminRequestDialogComponent } from "../admin-request-dialog/admin-request-dialog.component";
export interface EpisodeRequestData {
series: ISearchTvResultV2;
isAdmin: boolean;
requestOnBehalf: string | undefined;
}
@Component({
@ -21,7 +23,7 @@ export class EpisodeRequestComponent {
}
constructor(public dialogRef: MatDialogRef<EpisodeRequestComponent>, @Inject(MAT_DIALOG_DATA) public data: EpisodeRequestData,
private requestService: RequestServiceV2, private notificationService: MessageService) { }
private requestService: RequestServiceV2, private notificationService: MessageService, private dialog: MatDialog) { }
public async submitRequests() {
@ -57,21 +59,23 @@ export class EpisodeRequestComponent {
viewModel.seasons.push(seasonsViewModel);
});
const requestResult = await this.requestService.requestTv(viewModel).toPromise();
if (requestResult.result) {
this.notificationService.send(
`Request for ${this.data.series.title} has been added successfully`);
this.data.series.seasonRequests.forEach((season) => {
season.episodes.forEach((ep) => {
ep.selected = false;
});
if (this.data.isAdmin) {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.tvShow, id: this.data.series.id }, panelClass: 'modal-panel' });
dialog.afterClosed().subscribe(async (result) => {
if (result) {
viewModel.requestOnBehalf = result.username?.id;
viewModel.qualityPathOverride = result?.sonarrPathId;
viewModel.rootFolderOverride = result?.sonarrFolderId;
const requestResult = await this.requestService.requestTv(viewModel).toPromise();
this.postRequest(requestResult);
}
});
} else {
this.notificationService.send(requestResult.errorMessage ? requestResult.errorMessage : requestResult.message);
const requestResult = await this.requestService.requestTv(viewModel).toPromise();
this.postRequest(requestResult);
}
this.dialogRef.close();
}
@ -114,4 +118,20 @@ export class EpisodeRequestComponent {
this.data.series.latestSeason = true;
await this.submitRequests();
}
private postRequest(requestResult: IRequestEngineResult) {
if (requestResult.result) {
this.notificationService.send(
`Request for ${this.data.series.title} has been added successfully`);
this.data.series.seasonRequests.forEach((season) => {
season.episodes.forEach((ep) => {
ep.selected = false;
});
});
} else {
this.notificationService.send(requestResult.errorMessage ? requestResult.errorMessage : requestResult.message);
}
}
}

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

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

@ -78,6 +78,7 @@ export class UserManagementUserComponent implements OnInit {
episodeRequestQuota: null,
movieRequestQuota: null,
language: null,
userAlias: "",
streamingCountry: "US",
userQualityProfiles: {
radarrQualityProfile: 0,

@ -142,8 +142,8 @@
background-color: $ombi-active;
}
hr{
border-top: 1px solid $ombi-background-primary;
hr {
border-top: 1px solid $accent-dark;
}
.form-control{
@ -157,3 +157,20 @@
color:#FFF;
border: 1px solid $ombi-active;
}
.alert .glyphicon{
display: table-cell;
vertical-align: middle;
padding-right: 1%;
}
.alert div,
.alert span{
padding-left: 1%;
display:table-cell;
}
.right-buttons {
float:right;
}

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

@ -252,6 +252,8 @@
"ViewCollection":"View Collection",
"NotEnoughInfo": "Unfortunately there is not enough information about this show yet!",
"AdvancedOptions":"Advanced Options",
"AutoApproveOptions":"You can configure the request here, once requested it will be send to your DVR application and will be auto approved!",
"AutoApproveOptionsTv":"You can configure the request here, once requested it will be send to your DVR application and will be auto approved! If the request is already in Sonarr, we will not change the root folder or quality profile if you set it!",
"QualityProfilesSelect":"Select A Quality Profile",
"RootFolderSelect":"Select A Root Folder",
"Status":"Status",

@ -1,10 +1,15 @@
import { BasePage } from "../../base.page";
import { AdminRequestDialog } from "../../shared/AdminRequestDialog";
class MovieInformationPanel {
get denyReason(): Cypress.Chainable<any> {
return cy.get('#deniedReasonInfo');
}
get requestedBy(): Cypress.Chainable<any> {
return cy.get('#requestedByInfo');
}
}
class DenyModal {
@ -74,6 +79,7 @@ class MovieDetailsPage extends BasePage {
denyModal = new DenyModal();
informationPanel = new MovieInformationPanel();
adminOptionsDialog = new AdminRequestDialog();
constructor() {
super();

@ -1,4 +1,5 @@
import { BasePage } from "../../base.page";
import { AdminRequestDialog } from "../../shared/AdminRequestDialog";
class TvRequestPanel {
@ -82,6 +83,7 @@ class TvDetailsPage extends BasePage {
informationPanel = new TvDetailsInformationPanel();
requestFabButton = new RequestFabButton();
requestPanel = new TvRequestPanel();
adminOptionsDialog = new AdminRequestDialog();
constructor() {
super();

@ -1,5 +1,6 @@
import { BasePage } from "../base.page";
import { DiscoverCard } from "../shared/DiscoverCard";
import { AdminRequestDialog } from "../shared/AdminRequestDialog";
import { DiscoverCard, DiscoverType } from "../shared/DiscoverCard";
class CarouselComponent {
private type: string;
@ -16,8 +17,8 @@ class CarouselComponent {
return cy.get(`#${this.type}Tv-button`);
}
getCard(id: string, movie: boolean): DiscoverCard {
return new DiscoverCard(id, movie);
getCard(id: string, movie: boolean, type?: DiscoverType): DiscoverCard {
return new DiscoverCard(id, movie, type);
}
constructor(id: string) {
@ -27,6 +28,7 @@ class CarouselComponent {
class DiscoverPage extends BasePage {
popularCarousel = new CarouselComponent("popular");
adminOptionsDialog = new AdminRequestDialog();
constructor() {
super();

@ -0,0 +1,58 @@
export class AdminRequestDialog {
isOpen(): Cypress.Chainable<any> {
return cy.waitUntil(x => {
return this.title.should('exist');
});
}
get title(): Cypress.Chainable<any> {
return cy.get(`#advancedOptionsTitle`);
}
get requestOnBehalfUserInput(): Cypress.Chainable<any> {
return cy.get(`#requestOnBehalfUserInput`);
}
get sonarrQualitySelect(): Cypress.Chainable<any> {
return cy.get(`#sonarrQualitySelect`);
}
selectSonarrQuality(id: number): Cypress.Chainable<any> {
return cy.get(`#sonarrQualitySelect${id}`);
}
get sonarrFolderSelect(): Cypress.Chainable<any> {
return cy.get(`#sonarrFolderSelect`);
}
selectSonarrFolder(id: number): Cypress.Chainable<any> {
return cy.get(`#sonarrFolderSelect${id}`);
}
get radarrQualitySelect(): Cypress.Chainable<any> {
return cy.get(`#radarrQualitySelect`);
}
selectradarrQuality(id: number): Cypress.Chainable<any> {
return cy.get(`#radarrQualitySelect${id}`);
}
get radarrFolderSelect(): Cypress.Chainable<any> {
return cy.get(`#radarrFolderSelect`);
}
selectradarrFolder(id: number): Cypress.Chainable<any> {
return cy.get(`#radarrFolderSelect${id}`);
}
get cancelButton(): Cypress.Chainable<any> {
return cy.get(`#cancelButton`);
}
get requestButton(): Cypress.Chainable<any> {
return cy.get(`#requestButton`);
}
}

@ -1,13 +1,22 @@
import { EpisodeRequestModal } from "./EpisodeRequestModal";
export enum DiscoverType {
Upcoming,
Trending,
Popular,
RecentlyRequested,
}
export class DiscoverCard {
private id: string;
private movie: boolean;
private type: DiscoverType;
episodeRequestModal = new EpisodeRequestModal();
constructor(id: string, movie: boolean) {
constructor(id: string, movie: boolean, type?: DiscoverType) {
this.id = id;
this.movie = movie;
this.type = type;
}
get topLevelCard(): Cypress.Chainable<any> {
@ -35,6 +44,10 @@ export class DiscoverCard {
}
get requestButton(): Cypress.Chainable<any> {
if (this.type) {
return cy.get(`#requestButton${this.id}${this.movie ? '1' : '0'}${this.type}`);
}
return cy.get(`#requestButton${this.id}${this.movie ? '1' : '0'}`);
}

@ -6,6 +6,10 @@ describe("Movie Details Buttons", () => {
Page.visit("587807");
Page.requestButton.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification("Tom & Jerry (2021) has been successfully added");
Page.requestedButton.should("be.visible");
@ -80,11 +84,13 @@ describe("Movie Details Buttons", () => {
it("Movie Requested, mark as available", () => {
cy.login();
Page.visit("399566");
Page.visit("12444");
Page.requestButton.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification(
"Godzilla vs. Kong (2021) has been successfully added"
"Harry Potter and the Deathly Hallows: Part 1 (2010) has been successfully added"
);
cy.reload();
@ -102,6 +108,8 @@ describe("Movie Details Buttons", () => {
Page.visit("671");
Page.requestButton.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification(
"Harry Potter and the Philosopher's Stone (2001) has been successfully added"
);
@ -112,6 +120,7 @@ describe("Movie Details Buttons", () => {
Page.denyButton.click();
Page.denyModal.denyReason.type("Automation Tests");
cy.wait(500);
Page.denyModal.denyButton.click();
Page.deniedButton.should('exist');

@ -137,6 +137,9 @@ describe("TV Requests Grid", function () {
Page.requestFabButton.fab.click();
Page.requestFabButton.requestSelected.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification('Request for Game of Thrones has been added successfully');
Page.requestPanel.getEpisodeStatus(2,1)
@ -157,6 +160,9 @@ describe("TV Requests Grid", function () {
Page.requestFabButton.fab.click();
Page.requestFabButton.requestFirst.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification('Request for Game of Thrones has been added successfully');
Page.requestPanel.getEpisodeStatus(1)
@ -170,6 +176,9 @@ describe("TV Requests Grid", function () {
Page.requestFabButton.fab.click();
Page.requestFabButton.requestLatest.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification('Request for Game of Thrones has been added successfully');
Page.requestPanel.seasonTab(8)

@ -1,11 +1,12 @@
import { discoverPage as Page } from "@/integration/page-objects";
import { DiscoverType } from "@/integration/page-objects/shared/DiscoverCard";
describe("Discover Cards Requests Tests", () => {
beforeEach(() => {
cy.login();
});
it("Not requested movie allows us to request", () => {
it("Not requested movie allows admin to request", () => {
window.localStorage.setItem("DiscoverOptions2", "2");
cy.intercept("GET", "**/search/Movie/Popular/**", (req) => {
req.reply((res) => {
@ -27,22 +28,76 @@ describe("Discover Cards Requests Tests", () => {
var expectedId = body[0].id;
var title = body[0].title;
const card = Page.popularCarousel.getCard(expectedId, true);
const card = Page.popularCarousel.getCard(expectedId, true, DiscoverType.Popular);
card.verifyTitle(title);
card.requestButton.should("exist");
// Not visible until hover
card.requestButton.should("not.be.visible");
cy.wait(500)
cy.wait(500);
card.topLevelCard.realHover();
card.requestButton.should("be.visible");
card.requestButton.click();
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification("has been successfully added!");
card.requestButton.should("not.be.visible");
card.availabilityText.should('have.text','Pending');
card.statusClass.should('have.class','requested');
card.requestButton.should("not.exist");
card.availabilityText.should("have.text", "Pending");
card.statusClass.should("have.class", "requested");
});
});
it("Not requested movie allows non-admin to request", () => {
cy.generateUniqueId().then((id) => {
cy.login();
const roles = [];
roles.push({ value: "RequestMovie", enabled: true });
cy.createUser(id, "a", roles).then(() => {
cy.removeLogin();
cy.loginWithCreds(id, "a");
window.localStorage.setItem("DiscoverOptions2", "2");
cy.intercept("GET", "**/search/Movie/Popular/**", (req) => {
req.reply((res) => {
const body = res.body;
const movie = body[6];
movie.available = false;
movie.approved = false;
movie.requested = false;
body[6] = movie;
res.send(body);
});
}).as("cardsResponse");
Page.visit();
cy.wait("@cardsResponse").then((res) => {
const body = JSON.parse(res.response.body);
var expectedId = body[6].id;
var title = body[6].title;
const card = Page.popularCarousel.getCard(expectedId, true, DiscoverType.Popular);
card.verifyTitle(title);
card.requestButton.should("exist");
// Not visible until hover
card.requestButton.should("not.be.visible");
cy.wait(500);
card.topLevelCard.realHover();
card.requestButton.should("be.visible");
card.requestButton.click();
cy.verifyNotification("has been successfully added!");
card.requestButton.should("not.exist");
card.availabilityText.should("have.text", "Pending");
card.statusClass.should("have.class", "requested");
});
});
});
});
@ -68,13 +123,13 @@ describe("Discover Cards Requests Tests", () => {
var expectedId = body[1].id;
var title = body[1].title;
const card = Page.popularCarousel.getCard(expectedId, true);
const card = Page.popularCarousel.getCard(expectedId, true, DiscoverType.Popular);
card.verifyTitle(title);
card.topLevelCard.realHover();
card.requestButton.should("not.exist");
card.availabilityText.should('have.text','Available');
card.statusClass.should('have.class','available');
card.availabilityText.should("have.text", "Available");
card.statusClass.should("have.class", "available");
});
});
@ -100,14 +155,13 @@ describe("Discover Cards Requests Tests", () => {
var expectedId = body[1].id;
var title = body[1].title;
const card = Page.popularCarousel.getCard(expectedId, true);
card.verifyTitle(title);
card.topLevelCard.realHover();
const card = Page.popularCarousel.getCard(expectedId, true, DiscoverType.Popular);
card.title.realHover();
card.verifyTitle(title);
card.requestButton.should("not.exist");
card.availabilityText.should('have.text','Pending');
card.statusClass.should('have.class','requested');
card.availabilityText.should("have.text", "Pending");
card.statusClass.should("have.class", "requested");
});
});
@ -133,13 +187,13 @@ describe("Discover Cards Requests Tests", () => {
var expectedId = body[1].id;
var title = body[1].title;
const card = Page.popularCarousel.getCard(expectedId, true);
card.verifyTitle(title);
card.topLevelCard.realHover();
const card = Page.popularCarousel.getCard(expectedId, true, DiscoverType.Popular);
card.title.realHover();
card.verifyTitle(title);
card.requestButton.should("not.exist");
card.availabilityText.should('have.text','Approved');
card.statusClass.should('have.class','approved');
card.availabilityText.should("have.text", "Approved");
card.statusClass.should("have.class", "approved");
});
});
@ -163,17 +217,17 @@ describe("Discover Cards Requests Tests", () => {
var expectedId = body[1].id;
var title = body[1].title;
const card = Page.popularCarousel.getCard(expectedId, true);
card.verifyTitle(title);
card.topLevelCard.realHover();
const card = Page.popularCarousel.getCard(expectedId, true, DiscoverType.Popular);
card.title.realHover();
card.verifyTitle(title);
card.requestButton.should("not.exist");
card.availabilityText.should('have.text','Available');
card.statusClass.should('have.class','available');
card.availabilityText.should("have.text", "Available");
card.statusClass.should("have.class", "available");
});
});
it.only("Not available TV does not allow us to request", () => {
it("Not available TV allow admin to request", () => {
cy.intercept("GET", "**/search/Tv/popular/**", (req) => {
req.reply((res) => {
const body = res.body;
@ -184,25 +238,77 @@ describe("Discover Cards Requests Tests", () => {
res.send(body);
});
}).as("cardsResponse");
cy.intercept("GET", "**/search/Tv/**").as("otherResponses");
window.localStorage.setItem("DiscoverOptions2", "3");
Page.visit();
cy.wait("@otherResponses");
cy.wait("@cardsResponse").then((res) => {
const body = JSON.parse(res.response.body);
var expectedId = body[3].id;
var title = body[3].title;
const card = Page.popularCarousel.getCard(expectedId, false);
card.verifyTitle(title);
card.topLevelCard.realHover();
const card = Page.popularCarousel.getCard(expectedId, false, DiscoverType.Popular);
card.title.realHover();
card.verifyTitle(title);
card.requestButton.should("be.visible");
card.requestButton.click();
const modal = card.episodeRequestModal;
modal.latestSeasonButton.click();
cy.verifyNotification("has been added successfully")
Page.adminOptionsDialog.isOpen();
Page.adminOptionsDialog.requestButton.click();
cy.verifyNotification("has been added successfully");
});
});
it("Not available TV allow non-admin to request", () => {
cy.generateUniqueId().then((id) => {
cy.login();
const roles = [];
roles.push({ value: "RequestTv", enabled: true });
cy.createUser(id, "a", roles).then(() => {
cy.removeLogin();
cy.loginWithCreds(id, "a");
cy.intercept("GET", "**/search/Tv/popular/**", (req) => {
req.reply((res) => {
const body = res.body;
const tv = body[5];
tv.fullyAvailable = false;
body[5] = tv;
res.send(body);
});
}).as("cardsResponse");
cy.intercept("GET", "**/search/Tv/**").as("otherResponses");
window.localStorage.setItem("DiscoverOptions2", "3");
Page.visit();
cy.wait("@otherResponses");
cy.wait("@cardsResponse").then((res) => {
const body = JSON.parse(res.response.body);
var expectedId = body[5].id;
var title = body[5].title;
const card = Page.popularCarousel.getCard(expectedId, false, DiscoverType.Popular);
card.title.realHover();
card.verifyTitle(title);
card.requestButton.should("be.visible");
card.requestButton.click();
const modal = card.episodeRequestModal;
modal.latestSeasonButton.click();
cy.verifyNotification("has been added successfully");
});
});
});
});
});

Loading…
Cancel
Save