Merge branch 'feature/v4' into jellyfin-redux

pull/3954/head
Jamie 4 years ago committed by GitHub
commit 436c7a73fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -67,6 +67,22 @@ namespace Ombi.Core.Engine
$"{movieInfo.Title}{(!string.IsNullOrEmpty(movieInfo.ReleaseDate) ? $" ({DateTime.Parse(movieInfo.ReleaseDate).Year})" : string.Empty)}"; $"{movieInfo.Title}{(!string.IsNullOrEmpty(movieInfo.ReleaseDate) ? $" ({DateTime.Parse(movieInfo.ReleaseDate).Year})" : string.Empty)}";
var userDetails = await GetUser(); var userDetails = await GetUser();
var canRequestOnBehalf = false;
if (model.RequestOnBehalf.HasValue())
{
canRequestOnBehalf = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin);
if (!canRequestOnBehalf)
{
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!"
};
}
}
var requestModel = new MovieRequests var requestModel = new MovieRequests
{ {
@ -82,7 +98,7 @@ namespace Ombi.Core.Engine
Status = movieInfo.Status, Status = movieInfo.Status,
RequestedDate = DateTime.UtcNow, RequestedDate = DateTime.UtcNow,
Approved = false, Approved = false,
RequestedUserId = userDetails.Id, RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
Background = movieInfo.BackdropPath, Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode, LangCode = model.LanguageCode,
RequestedByAlias = model.RequestedByAlias RequestedByAlias = model.RequestedByAlias
@ -103,7 +119,7 @@ namespace Ombi.Core.Engine
if (requestModel.Approved) // The rules have auto approved this if (requestModel.Approved) // The rules have auto approved this
{ {
var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName); var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf);
if (requestEngineResult.Result) if (requestEngineResult.Result)
{ {
var result = await ApproveMovie(requestModel); var result = await ApproveMovie(requestModel);
@ -124,7 +140,7 @@ namespace Ombi.Core.Engine
// If there are no providers then it's successful but movie has not been sent // If there are no providers then it's successful but movie has not been sent
} }
return await AddMovieRequest(requestModel, fullMovieName); return await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf);
} }
@ -429,7 +445,7 @@ namespace Ombi.Core.Engine
public async Task<MovieRequests> GetRequest(int requestId) public async Task<MovieRequests> GetRequest(int requestId)
{ {
var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync(); var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync();
await CheckForSubscription(new HideResult(), new List<MovieRequests>{request }); await CheckForSubscription(new HideResult(), new List<MovieRequests> { request });
return request; return request;
} }
@ -654,7 +670,7 @@ namespace Ombi.Core.Engine
}; };
} }
private async Task<RequestEngineResult> AddMovieRequest(MovieRequests model, string movieName) private async Task<RequestEngineResult> AddMovieRequest(MovieRequests model, string movieName, string requestOnBehalf)
{ {
await MovieRepository.Add(model); await MovieRepository.Add(model);
@ -666,7 +682,7 @@ namespace Ombi.Core.Engine
await _requestLog.Add(new RequestLog await _requestLog.Add(new RequestLog
{ {
UserId = (await GetUser()).Id, UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id,
RequestDate = DateTime.UtcNow, RequestDate = DateTime.UtcNow,
RequestId = model.Id, RequestId = model.Id,
RequestType = RequestType.Movie, RequestType = RequestType.Movie,

@ -51,12 +51,28 @@ namespace Ombi.Core.Engine
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv) public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
{ {
var user = await GetUser(); var user = await GetUser();
var canRequestOnBehalf = false;
if (tv.RequestOnBehalf.HasValue())
{
canRequestOnBehalf = await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, OmbiRoles.Admin);
if (!canRequestOnBehalf)
{
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!"
};
}
}
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi); var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi);
(await tvBuilder (await tvBuilder
.GetShowInfo(tv.TvDbId)) .GetShowInfo(tv.TvDbId))
.CreateTvList(tv) .CreateTvList(tv)
.CreateChild(tv, user.Id); .CreateChild(tv, canRequestOnBehalf ? tv.RequestOnBehalf : user.Id);
await tvBuilder.BuildEpisodes(tv); await tvBuilder.BuildEpisodes(tv);
@ -124,12 +140,12 @@ namespace Ombi.Core.Engine
ErrorMessage = "This has already been requested" ErrorMessage = "This has already been requested"
}; };
} }
return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest); return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf);
} }
// This is a new request // This is a new request
var newRequest = tvBuilder.CreateNewRequest(tv); var newRequest = tvBuilder.CreateNewRequest(tv);
return await AddRequest(newRequest.NewRequest); return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf);
} }
public async Task<RequestsViewModel<TvRequests>> GetRequests(int count, int position, OrderFilterModel type) public async Task<RequestsViewModel<TvRequests>> GetRequests(int count, int position, OrderFilterModel type)
@ -736,21 +752,21 @@ namespace Ombi.Core.Engine
} }
} }
private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest) private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf)
{ {
// Add the child // Add the child
existingRequest.ChildRequests.Add(newRequest); existingRequest.ChildRequests.Add(newRequest);
await TvRepository.Update(existingRequest); await TvRepository.Update(existingRequest);
return await AfterRequest(newRequest); return await AfterRequest(newRequest, requestOnBehalf);
} }
private async Task<RequestEngineResult> AddRequest(TvRequests model) private async Task<RequestEngineResult> AddRequest(TvRequests model, string requestOnBehalf)
{ {
await TvRepository.Add(model); await TvRepository.Add(model);
// This is a new request so we should only have 1 child // This is a new request so we should only have 1 child
return await AfterRequest(model.ChildRequests.FirstOrDefault()); return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf);
} }
private static List<ChildRequests> SortEpisodes(List<ChildRequests> items) private static List<ChildRequests> SortEpisodes(List<ChildRequests> items)
@ -766,7 +782,7 @@ namespace Ombi.Core.Engine
} }
private async Task<RequestEngineResult> AfterRequest(ChildRequests model) private async Task<RequestEngineResult> AfterRequest(ChildRequests model, string requestOnBehalf)
{ {
var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification); var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification);
if (sendRuleResult.Success) if (sendRuleResult.Success)
@ -776,7 +792,7 @@ namespace Ombi.Core.Engine
await _requestLog.Add(new RequestLog await _requestLog.Add(new RequestLog
{ {
UserId = (await GetUser()).Id, UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id,
RequestDate = DateTime.UtcNow, RequestDate = DateTime.UtcNow,
RequestId = model.Id, RequestId = model.Id,
RequestType = RequestType.TvShow, RequestType = RequestType.TvShow,

@ -33,6 +33,7 @@ namespace Ombi.Core.Models.Requests
{ {
public int TheMovieDbId { get; set; } public int TheMovieDbId { get; set; }
public string LanguageCode { get; set; } = "en"; public string LanguageCode { get; set; } = "en";
public string RequestOnBehalf { get; set; }
/// <summary> /// <summary>
/// This is only set from a HTTP Header /// This is only set from a HTTP Header

@ -12,6 +12,8 @@ namespace Ombi.Core.Models.Requests
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>(); public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
[JsonIgnore] [JsonIgnore]
public string RequestedByAlias { get; set; } public string RequestedByAlias { get; set; }
public string RequestOnBehalf { get; set; }
} }
public class SeasonsViewModel public class SeasonsViewModel

@ -30,4 +30,10 @@ namespace Ombi.Core.Models.UI
public string Value { get; set; } public string Value { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
} }
public class UserViewModelDropdown
{
public string Id { get; set; }
public string Username { get; set; }
}
} }

@ -67,7 +67,7 @@ export class DiscoverCardDetailsComponent implements OnInit {
public async request() { public async request() {
this.loading = true; this.loading = true;
if (this.data.type === RequestType.movie) { if (this.data.type === RequestType.movie) {
const result = await this.requestService.requestMovie({ theMovieDbId: this.data.id, languageCode: "" }).toPromise(); const result = await this.requestService.requestMovie({ theMovieDbId: this.data.id, languageCode: "", requestOnBehalf: null }).toPromise();
this.loading = false; this.loading = false;
if (result.result) { if (result.result) {

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

@ -5,7 +5,7 @@ import { ImageService, RequestService, SearchV2Service } from "../../../services
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; import { EpisodeRequestComponent, EpisodeRequestData } from "../../../shared/episode-request/episode-request.component";
import { MatSnackBar } from "@angular/material/snack-bar"; import { MatSnackBar } from "@angular/material/snack-bar";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
@ -139,7 +139,7 @@ export class DiscoverGridComponent implements OnInit {
public async request() { public async request() {
this.requesting = true; this.requesting = true;
if (this.result.type === RequestType.movie) { if (this.result.type === RequestType.movie) {
const result = await this.requestService.requestMovie({ theMovieDbId: this.result.id, languageCode: "" }).toPromise(); const result = await this.requestService.requestMovie({ theMovieDbId: this.result.id, languageCode: "", requestOnBehalf: null }).toPromise();
if (result.result) { if (result.result) {
this.result.requested = true; this.result.requested = true;
@ -148,7 +148,7 @@ export class DiscoverGridComponent implements OnInit {
this.notification.open(result.errorMessage, "Ok"); this.notification.open(result.errorMessage, "Ok");
} }
} else if (this.result.type === RequestType.tvShow) { } else if (this.result.type === RequestType.tvShow) {
this.dialog.open(EpisodeRequestComponent, { width: "700px", data: this.tv, panelClass: 'modal-panel' }) this.dialog.open(EpisodeRequestComponent, { width: "700px", data: <EpisodeRequestData> { series: this.tv, requestOnBehalf: null }, panelClass: 'modal-panel' })
} }
this.requesting = false; this.requesting = false;
} }

@ -1,4 +1,7 @@
export interface IRadarrRootFolder { import { IChildRequests, IMovieRequests } from ".";
import { ITvRequests } from "./IRequestModel";
export interface IRadarrRootFolder {
id: number; id: number;
path: string; path: string;
} }
@ -24,4 +27,6 @@ export interface IAdvancedData {
rootFolder: IRadarrRootFolder; rootFolder: IRadarrRootFolder;
rootFolders: IRadarrRootFolder[]; rootFolders: IRadarrRootFolder[];
rootFolderId: number; rootFolderId: number;
movieRequest: IMovieRequests;
tvRequest: ITvRequests;
} }

@ -167,6 +167,7 @@ export interface IEpisodesRequests {
export interface IMovieRequestModel { export interface IMovieRequestModel {
theMovieDbId: number; theMovieDbId: number;
languageCode: string | undefined; languageCode: string | undefined;
requestOnBehalf: string | undefined;
} }
export interface IFilter { export interface IFilter {

@ -47,6 +47,7 @@ export interface ITvRequestViewModel {
latestSeason: boolean; latestSeason: boolean;
tvDbId: number; tvDbId: number;
seasons: ISeasonsViewModel[]; seasons: ISeasonsViewModel[];
requestOnBehalf: string | undefined;
} }
export interface ISeasonsViewModel { export interface ISeasonsViewModel {

@ -24,6 +24,11 @@ export interface IUser {
musicRequestQuota: IRemainingRequests | null; musicRequestQuota: IRemainingRequests | null;
} }
export interface IUserDropdown {
username: string;
id: string;
}
export interface IUserQualityProfiles { export interface IUserQualityProfiles {
sonarrQualityProfileAnime: number; sonarrQualityProfileAnime: number;
sonarrRootPathAnime: number; sonarrRootPathAnime: number;

@ -9,7 +9,6 @@ import { MediaPosterComponent } from "./shared/media-poster/media-poster.compone
import { CastCarouselComponent } from "./shared/cast-carousel/cast-carousel.component"; import { CastCarouselComponent } from "./shared/cast-carousel/cast-carousel.component";
import { DenyDialogComponent } from "./shared/deny-dialog/deny-dialog.component"; import { DenyDialogComponent } from "./shared/deny-dialog/deny-dialog.component";
import { TvRequestsPanelComponent } from "./tv/panels/tv-requests/tv-requests-panel.component"; import { TvRequestsPanelComponent } from "./tv/panels/tv-requests/tv-requests-panel.component";
import { MovieAdminPanelComponent } from "./movie/panels/movie-admin-panel/movie-admin-panel.component";
import { MovieAdvancedOptionsComponent } from "./movie/panels/movie-advanced-options/movie-advanced-options.component"; import { MovieAdvancedOptionsComponent } from "./movie/panels/movie-advanced-options/movie-advanced-options.component";
import { SearchService, RequestService, RadarrService, IssuesService, SonarrService } from "../../services"; import { SearchService, RequestService, RadarrService, IssuesService, SonarrService } from "../../services";
import { RequestServiceV2 } from "../../services/requestV2.service"; import { RequestServiceV2 } from "../../services/requestV2.service";
@ -18,7 +17,8 @@ import { ArtistDetailsComponent } from "./artist/artist-details.component";
import { ArtistInformationPanel } from "./artist/panels/artist-information-panel/artist-information-panel.component"; import { ArtistInformationPanel } from "./artist/panels/artist-information-panel/artist-information-panel.component";
import { ArtistReleasePanel } from "./artist/panels/artist-release-panel/artist-release-panel.component"; import { ArtistReleasePanel } from "./artist/panels/artist-release-panel/artist-release-panel.component";
import { IssuesPanelComponent } from "./shared/issues-panel/issues-panel.component"; import { IssuesPanelComponent } from "./shared/issues-panel/issues-panel.component";
import { TvAdminPanelComponent } from "./tv/panels/tv-admin-panel/tv-admin-panel.component"; import { TvAdvancedOptionsComponent } from "./tv/panels/tv-advanced-options/tv-advanced-options.component";
import { RequestBehalfComponent } from "./shared/request-behalf/request-behalf.component";
export const components: any[] = [ export const components: any[] = [
MovieDetailsComponent, MovieDetailsComponent,
@ -32,21 +32,23 @@ export const components: any[] = [
CastCarouselComponent, CastCarouselComponent,
DenyDialogComponent, DenyDialogComponent,
TvRequestsPanelComponent, TvRequestsPanelComponent,
MovieAdminPanelComponent,
MovieAdvancedOptionsComponent, MovieAdvancedOptionsComponent,
TvAdvancedOptionsComponent,
NewIssueComponent, NewIssueComponent,
ArtistDetailsComponent, ArtistDetailsComponent,
ArtistInformationPanel, ArtistInformationPanel,
ArtistReleasePanel, ArtistReleasePanel,
RequestBehalfComponent,
IssuesPanelComponent, IssuesPanelComponent,
TvAdminPanelComponent,
]; ];
export const entryComponents: any[] = [ export const entryComponents: any[] = [
YoutubeTrailerComponent, YoutubeTrailerComponent,
DenyDialogComponent, DenyDialogComponent,
MovieAdvancedOptionsComponent, MovieAdvancedOptionsComponent,
TvAdvancedOptionsComponent,
NewIssueComponent, NewIssueComponent,
RequestBehalfComponent,
]; ];
export const providers: any[] = [ export const providers: any[] = [

@ -21,7 +21,7 @@
</div> </div>
<div class="col-12 col-lg-6 col-xl-6 media-row"> <div class="col-12 col-lg-5 col-xl-5 media-row">
<button mat-raised-button class="btn-green btn-spacing" *ngIf="movie.available"> {{ <button mat-raised-button class="btn-green btn-spacing" *ngIf="movie.available"> {{
'Common.Available' | translate }}</button> 'Common.Available' | translate }}</button>
@ -64,23 +64,28 @@
<button mat-raised-button class="btn-spacing" color="danger" (click)="issue()"> <button mat-raised-button class="btn-spacing" color="danger" (click)="issue()">
<i class="fa fa-exclamation"></i> {{ <i class="fa fa-exclamation"></i> {{
'Requests.ReportIssue' | translate }}</button> 'Requests.ReportIssue' | translate }}</button>
</div>
<!-- Setting/Configuration admin area -->
<div class="col-12 col-lg-1 col-xl-1 media-row content-end">
<button *ngIf="isAdmin" mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>settings</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="openRequestOnBehalf()" [disabled]="hasRequest || movie.available">
<mat-icon>supervised_user_circle</mat-icon>
<span>{{'MediaDetails.RequestOnBehalf' | translate}}</span>
</button>
<button mat-menu-item [disabled]="!showAdvanced || !movieRequest" (click)="openAdvancedOptions()">
<mat-icon>movie_filter</mat-icon>
<span>{{'MediaDetails.RadarrConfiguration' | translate}}</span>
</button>
</mat-menu>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-md-2"> <div class="col-12 col-md-2">
<mat-card class="mat-elevation-z8 spacing-below" *ngIf="isAdmin && movieRequest" [ngStyle]="{'display': showAdvanced ? '' : 'none' }">
<mat-card-content class="medium-font">
<movie-admin-panel [movie]="movieRequest" (radarrEnabledChange)="showAdvanced = $event" (advancedOptionsChanged)="setAdvancedOptions($event)">
</movie-admin-panel>
</mat-card-content>
</mat-card>
<mat-card class="mat-elevation-z8"> <mat-card class="mat-elevation-z8">
<mat-card-content class="medium-font"> <mat-card-content class="medium-font">
<movie-information-panel [movie]="movie" [request]="movieRequest" [advancedOptions]="showAdvanced"></movie-information-panel> <movie-information-panel [movie]="movie" [request]="movieRequest" [advancedOptions]="showAdvanced"></movie-information-panel>

@ -1,5 +1,5 @@
import { Component, ViewEncapsulation } from "@angular/core"; import { Component, ViewEncapsulation } from "@angular/core";
import { ImageService, SearchV2Service, RequestService, MessageService } from "../../../services"; import { ImageService, SearchV2Service, RequestService, MessageService, RadarrService } from "../../../services";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
@ -10,6 +10,9 @@ import { IMovieRequests, RequestType, IAdvancedData } from "../../../interfaces"
import { DenyDialogComponent } from "../shared/deny-dialog/deny-dialog.component"; import { DenyDialogComponent } from "../shared/deny-dialog/deny-dialog.component";
import { NewIssueComponent } from "../shared/new-issue/new-issue.component"; import { NewIssueComponent } from "../shared/new-issue/new-issue.component";
import { StorageService } from "../../../shared/storage/storage-service"; import { StorageService } from "../../../shared/storage/storage-service";
import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component";
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
@Component({ @Component({
templateUrl: "./movie-details.component.html", templateUrl: "./movie-details.component.html",
@ -30,6 +33,7 @@ export class MovieDetailsComponent {
constructor(private searchService: SearchV2Service, private route: ActivatedRoute, constructor(private searchService: SearchV2Service, private route: ActivatedRoute,
private sanitizer: DomSanitizer, private imageService: ImageService, private sanitizer: DomSanitizer, private imageService: ImageService,
public dialog: MatDialog, private requestService: RequestService, public dialog: MatDialog, private requestService: RequestService,
private requestService2: RequestServiceV2, private radarrService: RadarrService,
public messageService: MessageService, private auth: AuthService, public messageService: MessageService, private auth: AuthService,
private storage: StorageService) { private storage: StorageService) {
this.route.params.subscribe((params: any) => { this.route.params.subscribe((params: any) => {
@ -47,6 +51,10 @@ export class MovieDetailsComponent {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
if (this.isAdmin) {
this.showAdvanced = await this.radarrService.isRadarrEnabled();
}
if (this.imdbId) { if (this.imdbId) {
this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => { this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => {
this.movie = x; this.movie = x;
@ -76,8 +84,8 @@ export class MovieDetailsComponent {
} }
} }
public async request() { public async request(userId?: string) {
const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null }).toPromise(); const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null, requestOnBehalf: userId }).toPromise();
if (result.result) { if (result.result) {
this.movie.requested = true; this.movie.requested = true;
this.messageService.send(result.message, "Ok"); this.messageService.send(result.message, "Ok");
@ -144,4 +152,25 @@ export class MovieDetailsComponent {
this.movieRequest.rootPathOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name; this.movieRequest.rootPathOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name;
} }
} }
public async openAdvancedOptions() {
const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: <IAdvancedData>{ movieRequest: this.movieRequest }, panelClass: 'modal-panel' })
await dialog.afterClosed().subscribe(async result => {
if (result) {
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0];
result.profile = result.profiles.filter(f => f.id === +result.profileId)[0];
await this.requestService2.updateMovieAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.movieRequest.id }).toPromise();
this.setAdvancedOptions(result);
}
});
}
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);
}
});
}
} }

@ -1,3 +0,0 @@
<div *ngIf="movie && radarrEnabled" class="text-center">
<button mat-raised-button color="warn" class="text-center" (click)="openAdvancedOptions();">{{'MediaDetails.AdvancedOptions' | translate }}</button>
</div>

@ -1,76 +0,0 @@
import { Component, Input, OnInit, EventEmitter, Output } from "@angular/core";
import { RadarrService } from "../../../../../services";
import { IRadarrProfile, IRadarrRootFolder, IMovieRequests, IAdvancedData } from "../../../../../interfaces";
import { MatDialog } from "@angular/material/dialog";
import { MovieAdvancedOptionsComponent } from "../movie-advanced-options/movie-advanced-options.component";
import { RequestServiceV2 } from "../../../../../services/requestV2.service";
@Component({
templateUrl: "./movie-admin-panel.component.html",
selector: "movie-admin-panel",
})
export class MovieAdminPanelComponent implements OnInit {
@Input() public movie: IMovieRequests;
@Output() public advancedOptionsChanged = new EventEmitter<IAdvancedData>();
@Output() public radarrEnabledChange = new EventEmitter<boolean>();
public radarrEnabled: boolean;
public radarrProfiles: IRadarrProfile[];
public selectedRadarrProfile: IRadarrProfile;
public radarrRootFolders: IRadarrRootFolder[];
public selectRadarrRootFolders: IRadarrRootFolder;
constructor(private radarrService: RadarrService, private requestService: RequestServiceV2, private dialog: MatDialog) { }
public async ngOnInit() {
this.radarrEnabled = await this.radarrService.isRadarrEnabled();
if (this.radarrEnabled) {
this.radarrService.getQualityProfilesFromSettings().subscribe(c => {
this.radarrProfiles = c;
this.setQualityOverrides();
});
this.radarrService.getRootFoldersFromSettings().subscribe(c => {
this.radarrRootFolders = c;
this.setRootFolderOverrides();
});
}
this.radarrEnabledChange.emit(this.radarrEnabled);
}
public async openAdvancedOptions() {
const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: <IAdvancedData>{ profiles: this.radarrProfiles, rootFolders: this.radarrRootFolders }, panelClass: 'modal-panel' })
await dialog.afterClosed().subscribe(async result => {
if(result) {
// get the name and ids
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0];
result.profile = result.profiles.filter(f => f.id === +result.profileId)[0];
await this.requestService.updateMovieAdvancedOptions({qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.movie.id}).toPromise();
this.advancedOptionsChanged.emit(result);
}
});
}
private setQualityOverrides(): void {
if (this.radarrProfiles) {
const profile = this.radarrProfiles.filter((p) => {
return p.id === this.movie.qualityOverride;
});
if (profile.length > 0) {
this.movie.qualityOverrideTitle = profile[0].name;
}
}
}
private setRootFolderOverrides(): void {
if (this.radarrRootFolders) {
const path = this.radarrRootFolders.filter((folder) => {
return folder.id === this.movie.rootPathOverride;
});
if (path.length > 0) {
this.movie.rootPathOverrideTitle = path[0].path;
}
}
}
}

@ -5,7 +5,7 @@
<mat-form-field> <mat-form-field>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label> <mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select [(value)]="data.profileId"> <mat-select [(value)]="data.profileId">
<mat-option *ngFor="let profile of data.profiles" value="{{profile.id}}">{{profile.name}}</mat-option> <mat-option *ngFor="let profile of radarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -13,7 +13,7 @@
<mat-form-field> <mat-form-field>
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label> <mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
<mat-select [(value)]="data.rootFolderId"> <mat-select [(value)]="data.rootFolderId">
<mat-option *ngFor="let profile of data.rootFolders" value="{{profile.id}}">{{profile.path}}</mat-option> <mat-option *ngFor="let profile of radarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

@ -1,14 +1,55 @@
import { Component, Inject } from "@angular/core"; import { Component, Inject, OnInit } from "@angular/core";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { IAdvancedData } from "../../../../../interfaces"; import { IAdvancedData, IRadarrProfile, IRadarrRootFolder } from "../../../../../interfaces";
import { RadarrService } from "../../../../../services";
@Component({ @Component({
templateUrl: "./movie-advanced-options.component.html", templateUrl: "./movie-advanced-options.component.html",
selector: "movie-advanced-options", selector: "movie-advanced-options",
}) })
export class MovieAdvancedOptionsComponent { export class MovieAdvancedOptionsComponent implements OnInit {
public radarrProfiles: IRadarrProfile[];
public radarrRootFolders: IRadarrRootFolder[];
constructor(public dialogRef: MatDialogRef<MovieAdvancedOptionsComponent>, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData, constructor(public dialogRef: MatDialogRef<MovieAdvancedOptionsComponent>, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData,
private radarrService: RadarrService
) { ) {
} }
public async ngOnInit() {
this.radarrService.getQualityProfilesFromSettings().subscribe(c => {
this.radarrProfiles = c;
this.data.profiles = c;
this.setQualityOverrides();
});
this.radarrService.getRootFoldersFromSettings().subscribe(c => {
this.radarrRootFolders = c;
this.data.rootFolders = c;
this.setRootFolderOverrides();
});
}
private setQualityOverrides(): void {
if (this.radarrProfiles) {
const profile = this.radarrProfiles.filter((p) => {
return p.id === this.data.movieRequest.qualityOverride;
});
if (profile.length > 0) {
this.data.movieRequest.qualityOverrideTitle = profile[0].name;
}
}
}
private setRootFolderOverrides(): void {
if (this.radarrRootFolders) {
const path = this.radarrRootFolders.filter((folder) => {
return folder.id === this.data.movieRequest.rootPathOverride;
});
if (path.length > 0) {
this.data.movieRequest.rootPathOverrideTitle = path[0].path;
}
}
}
} }

@ -9,7 +9,7 @@
<div *ngIf="!movie.available">{{'Common.NotAvailable' | translate}}</div> <div *ngIf="!movie.available">{{'Common.NotAvailable' | translate}}</div>
</div> </div>
<br>
<div> <div>
<strong>{{'MediaDetails.RequestStatus' | translate }}</strong> <strong>{{'MediaDetails.RequestStatus' | translate }}</strong>
<div *ngIf="movie.approved && !movie.available">{{'Common.ProcessingRequest' | translate}}</div> <div *ngIf="movie.approved && !movie.available">{{'Common.ProcessingRequest' | translate}}</div>
@ -18,16 +18,28 @@
<div *ngIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}} <div *ngIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}}
</div> </div>
</div> </div>
<div *ngIf="request">
<strong>{{'Requests.RequestedBy' | translate }}</strong>
<div>{{request.requestedUser.userAlias}}</div>
</div>
<div *ngIf="request">
<strong>{{'Requests.RequestDate' | translate }}</strong>
<div>{{request.requestedDate | date}}</div>
</div>
<div *ngIf="movie.quality"> <div *ngIf="movie.quality">
<strong>{{'MediaDetails.Quality' | translate }}:</strong> <strong>{{'MediaDetails.Quality' | translate }}:</strong>
<div>{{movie.quality | quality}}</div> <div>{{movie.quality | quality}}</div>
</div> </div>
<div *ngIf="advancedOptions"> <div *ngIf="advancedOptions && request && request.rootPathOverrideTitle">
<strong>{{'MediaDetails.RootFolderOverride' | translate }}</strong> <strong>{{'MediaDetails.RootFolderOverride' | translate }}</strong>
<div>{{request.rootPathOverrideTitle}}</div> <div>{{request.rootPathOverrideTitle}}</div>
</div> </div>
<div *ngIf="advancedOptions"> <div *ngIf="advancedOptions && request && request.qualityOverrideTitle">
<strong>{{'MediaDetails.QualityOverride' | translate }}</strong> <strong>{{'MediaDetails.QualityOverride' | translate }}</strong>
<div>{{request.qualityOverrideTitle}}</div> <div>{{request.qualityOverrideTitle}}</div>
</div> </div>
@ -81,7 +93,7 @@
<br /> <br />
<div> <div *ngIf="movie?.keywords?.keywordsValue?.length > 0">
<strong>{{'MediaDetails.Keywords' | translate }}:</strong> <strong>{{'MediaDetails.Keywords' | translate }}:</strong>
<mat-chip-list> <mat-chip-list>
<mat-chip color="accent" selected *ngFor="let keyword of movie.keywords.keywordsValue"> <mat-chip color="accent" selected *ngFor="let keyword of movie.keywords.keywordsValue">

@ -0,0 +1,22 @@
<h1 mat-dialog-title>{{ 'MediaDetails.RequestOnBehalf' | translate}}</h1>
<div mat-dialog-content>
<form class="example-form">
<mat-form-field class="example-full-width">
<input type="text"
placeholder="{{ 'MediaDetails.PleaseSelectUser' | translate}}"
aria-label="Number"
matInput
[formControl]="myControl"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{option.username}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>
<button mat-raised-button (click)="onNoClick()"> Cancel</button>
<button mat-raised-button (click)="request()" color="accent" [mat-dialog-close]="userId" cdkFocusInitial>{{'Common.Request' | translate}}</button>
</div>

@ -0,0 +1,52 @@
import { Component, Inject, OnInit } from "@angular/core";
import { IDenyDialogData } from "../interfaces/interfaces";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { RequestService, MessageService, IdentityService } from "../../../../services";
import { RequestType, IRequestEngineResult, IUserDropdown } from "../../../../interfaces";
import { FormControl } from "@angular/forms";
import { Observable } from "rxjs";
import { filter, map, startWith } from "rxjs/operators";
@Component({
selector: "request-behalf",
templateUrl: "./request-behalf.component.html",
})
export class RequestBehalfComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<RequestBehalfComponent>,
public identity: IdentityService) { }
public myControl = new FormControl();
public options: IUserDropdown[];
public filteredOptions: Observable<IUserDropdown[]>;
public userId: string;
public async ngOnInit() {
this.options = await this.identity.getUsersDropdown().toPromise();
this.filteredOptions = this.myControl.valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
}
public request() {
this.dialogRef.close(this.myControl.value);
}
public onNoClick(): void {
this.dialogRef.close();
}
public displayFn(user: IUserDropdown): string {
return user?.username ? user.username : '';
}
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));
}
}

@ -1,3 +0,0 @@
<div *ngIf="tv && sonarrEnabled" class="text-center">
<button mat-raised-button color="warn" class="text-center" (click)="openAdvancedOptions();">{{'MediaDetails.AdvancedOptions' | translate }}</button>
</div>

@ -1,83 +0,0 @@
import { Component, Input, OnInit, EventEmitter, Output } from "@angular/core";
import { RadarrService, SonarrService } from "../../../../../services";
import { IRadarrProfile, IRadarrRootFolder, IAdvancedData, ITvRequests, ISonarrProfile, ISonarrRootFolder } from "../../../../../interfaces";
import { MatDialog } from "@angular/material/dialog";
import { RequestServiceV2 } from "../../../../../services/requestV2.service";
import { MovieAdvancedOptionsComponent } from "../../../movie/panels/movie-advanced-options/movie-advanced-options.component";
@Component({
templateUrl: "./tv-admin-panel.component.html",
selector: "tv-admin-panel",
})
export class TvAdminPanelComponent implements OnInit {
@Input() public tv: ITvRequests;
@Output() public advancedOptionsChanged = new EventEmitter<IAdvancedData>();
@Output() public sonarrEnabledChange = new EventEmitter<boolean>();
public sonarrEnabled: boolean;
public radarrProfiles: IRadarrProfile[];
public selectedRadarrProfile: IRadarrProfile;
public radarrRootFolders: IRadarrRootFolder[];
public selectRadarrRootFolders: IRadarrRootFolder;
public sonarrProfiles: ISonarrProfile[];
public sonarrRootFolders: ISonarrRootFolder[];
constructor(private sonarrService: SonarrService, private requestService: RequestServiceV2, private dialog: MatDialog) { }
public async ngOnInit() {
this.sonarrEnabled = await this.sonarrService.isEnabled();
if (this.sonarrEnabled) {
this.sonarrService.getQualityProfilesWithoutSettings()
.subscribe(x => {
this.sonarrProfiles = x;
this.setQualityOverrides();
});
this.sonarrService.getRootFoldersWithoutSettings()
.subscribe(x => {
this.sonarrRootFolders = x;
this.setRootFolderOverrides();
});
}
this.sonarrEnabledChange.emit(this.sonarrEnabled);
}
public async openAdvancedOptions() {
const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: <IAdvancedData>{ profiles: this.sonarrProfiles, rootFolders: this.sonarrRootFolders }, panelClass: 'modal-panel' })
await dialog.afterClosed().subscribe(async result => {
if (result) {
// get the name and ids
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0];
result.profile = result.profiles.filter(f => f.id === +result.profileId)[0];
await this.requestService.updateTvAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.tv.id }).toPromise();
this.advancedOptionsChanged.emit(result);
}
});
}
private setQualityOverrides(): void {
if (this.sonarrProfiles) {
const profile = this.sonarrProfiles.filter((p) => {
return p.id === this.tv.qualityOverride;
});
if (profile.length > 0) {
this.tv.qualityOverrideTitle = profile[0].name;
}
}
}
private setRootFolderOverrides(): void {
if (this.sonarrRootFolders) {
const path = this.sonarrRootFolders.filter((folder) => {
return folder.id === this.tv.rootFolder;
});
if (path.length > 0) {
this.tv.rootPathOverrideTitle = path[0].path;
}
}
}
}

@ -0,0 +1,23 @@
<h1 mat-dialog-title>
Advanced Options</h1>
<div mat-dialog-content>
<mat-form-field>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select [(value)]="data.profileId">
<mat-option *ngFor="let profile of sonarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div mat-dialog-content>
<mat-form-field>
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
<mat-select [(value)]="data.rootFolderId">
<mat-option *ngFor="let profile of sonarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
</mat-select>
</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>
</div>

@ -0,0 +1,55 @@
import { Component, Inject, OnInit } from "@angular/core";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { IAdvancedData, ISonarrProfile, ISonarrRootFolder } from "../../../../../interfaces";
import { SonarrService } from "../../../../../services";
@Component({
templateUrl: "./tv-advanced-options.component.html",
selector: "tv-advanced-options",
})
export class TvAdvancedOptionsComponent implements OnInit {
public sonarrProfiles: ISonarrProfile[];
public sonarrRootFolders: ISonarrRootFolder[];
constructor(public dialogRef: MatDialogRef<TvAdvancedOptionsComponent>, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData,
private sonarrService: SonarrService
) {
}
public async ngOnInit() {
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => {
this.sonarrProfiles = c;
this.data.profiles = c;
this.setQualityOverrides();
});
this.sonarrService.getRootFoldersWithoutSettings().subscribe(c => {
this.sonarrRootFolders = c;
this.data.rootFolders = c;
this.setRootFolderOverrides();
});
}
private setQualityOverrides(): void {
if (this.sonarrProfiles) {
const profile = this.sonarrProfiles.filter((p) => {
return p.id === this.data.tvRequest.qualityOverride;
});
if (profile.length > 0) {
this.data.movieRequest.qualityOverrideTitle = profile[0].name;
}
}
}
private setRootFolderOverrides(): void {
if (this.sonarrRootFolders) {
const path = this.sonarrRootFolders.filter((folder) => {
return folder.id === this.data.tvRequest.rootFolder;
});
if (path.length > 0) {
this.data.movieRequest.rootPathOverrideTitle = path[0].path;
}
}
}
}

@ -11,11 +11,11 @@
</div> </div>
</div> </div>
<div *ngIf="advancedOptions && request.rootPathOverrideTitle"> <div *ngIf="advancedOptions && request?.rootPathOverrideTitle">
<strong>{{'MediaDetails.RootFolderOverride' | translate }}</strong> <strong>{{'MediaDetails.RootFolderOverride' | translate }}</strong>
<div>{{request.rootPathOverrideTitle}}</div> <div>{{request.rootPathOverrideTitle}}</div>
</div> </div>
<div *ngIf="advancedOptions && request.qualityOverrideTitle"> <div *ngIf="advancedOptions && request?.qualityOverrideTitle">
<strong>{{'MediaDetails.QualityOverride' | translate }}</strong> <strong>{{'MediaDetails.QualityOverride' | translate }}</strong>
<div>{{request.qualityOverrideTitle}}</div> <div>{{request.qualityOverrideTitle}}</div>
</div> </div>

@ -2,18 +2,20 @@
<mat-spinner [color]="'accent'"></mat-spinner> <mat-spinner [color]="'accent'"></mat-spinner>
</div> </div>
<div *ngIf="tv"> <div *ngIf="tv">
<div *ngIf="tv.id === 0; else main"> <div *ngIf="tv.id === 0; else main">
<div class="small-middle-container no-info"> <div class="small-middle-container no-info">
<h1><i class="fa fa-frown-o" aria-hidden="true"></i></h1><h3> {{ 'MediaDetails.NotEnoughInfo' | translate }}</h3> <h1><i class="fa fa-frown-o" aria-hidden="true"></i></h1>
<h3> {{ 'MediaDetails.NotEnoughInfo' | translate }}</h3>
</div>
</div> </div>
</div>
<ng-template #main> <ng-template #main>
<div> <div>
<top-banner [background]="tv.background" [available]="tv.available" [title]="tv.title" [releaseDate]="tv.firstAired" [tagline]="tv.certification"></top-banner> <top-banner [background]="tv.background" [available]="tv.available" [title]="tv.title"
[releaseDate]="tv.firstAired" [tagline]="tv.certification"></top-banner>
<section id="info-wrapper"> <section id="info-wrapper">
<div class="small-middle-container"> <div class="small-middle-container">
@ -21,22 +23,28 @@
<media-poster [posterPath]="tv.images?.medium"></media-poster> <media-poster [posterPath]="tv.images?.medium"></media-poster>
<!--Next to poster--> <!--Next to poster-->
<div class="col-12 col-lg-3 col-xl-3 media-row"> <div class="col-12 col-lg-3 col-xl-3 media-row">
<social-icons [homepage]="tv.homepage" [tvdbId]="tv.id" [hasTrailer]="tv.trailer" (openTrailer)="openDialog()" [imdbId]="tv.imdbId" [available]="tv.available" [plexUrl]="tv.plexUrl" [embyUrl]="tv.embyUrl" [jellyfinUrl]="tv.jellyfinUrl"> <social-icons [homepage]="tv.homepage" [tvdbId]="tv.id" [hasTrailer]="tv.trailer"
(openTrailer)="openDialog()" [imdbId]="tv.imdbId" [available]="tv.available"
[plexUrl]="tv.plexUrl" [embyUrl]="tv.embyUrl" [jellyfinUrl]="tv.jellyfinUrl">
</social-icons> </social-icons>
</div> </div>
<div class="col-12 col-lg-6 col-xl-6 media-row"> <div class="col-12 col-lg-5 col-xl-5 media-row">
<button *ngIf="!tv.fullyAvailable" mat-raised-button class="btn-spacing" color="primary" (click)="request()"><i class="fa fa-plus"></i> <button *ngIf="!tv.fullyAvailable" mat-raised-button class="btn-spacing" color="primary"
(click)="request()"><i class="fa fa-plus"></i>
{{ 'Common.Request' | translate }}</button> {{ 'Common.Request' | translate }}</button>
<button *ngIf="tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent" [disabled]> <button *ngIf="tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent"
[disabled]>
<i class="fa fa-check"></i> {{'Common.Available' | translate }}</button> <i class="fa fa-check"></i> {{'Common.Available' | translate }}</button>
<button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent" [disabled]> <button *ngIf="tv.partlyAvailable && !tv.fullyAvailable" mat-raised-button
class="btn-spacing" color="accent" [disabled]>
<i class="fa fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button> <i class="fa fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>
<button mat-raised-button class="btn-spacing" color="danger" (click)="issue()"> <button mat-raised-button class="btn-spacing" color="danger" (click)="issue()">
@ -44,20 +52,31 @@
'Requests.ReportIssue' | translate }}</button> 'Requests.ReportIssue' | translate }}</button>
</div> </div>
<!-- Setting/Configuration admin area -->
<div class="col-12 col-lg-1 col-xl-1 media-row content-end">
<button *ngIf="isAdmin" mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>settings</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="openRequestOnBehalf()" [disabled]="fullyAvailable">
<mat-icon>supervised_user_circle</mat-icon>
<span>{{'MediaDetails.RequestOnBehalf' | translate}}</span>
</button>
<button mat-menu-item [disabled]="!showAdvanced || !showRequest"
(click)="openAdvancedOptions()">
<mat-icon>movie_filter</mat-icon>
<span>{{'MediaDetails.SonarrConfiguration' | translate}}</span>
</button>
</mat-menu>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-md-2"> <div class="col-12 col-md-2">
<mat-card class="mat-elevation-z8 spacing-below" *ngIf="isAdmin && showRequest" [ngStyle]="{'display': showAdvanced ? '' : 'none' }">
<mat-card-content class="medium-font">
<tv-admin-panel [tv]="showRequest" (sonarrEnabledChange)="showAdvanced = $event" (advancedOptionsChanged)="setAdvancedOptions($event)">
</tv-admin-panel>
</mat-card-content>
</mat-card>
<mat-card class="mat-elevation-z8"> <mat-card class="mat-elevation-z8">
<mat-card-content class="medium-font"> <mat-card-content class="medium-font">
<tv-information-panel [tv]="tv" [request]="showRequest" [advancedOptions]="showAdvanced"></tv-information-panel> <tv-information-panel [tv]="tv" [request]="showRequest"
[advancedOptions]="showAdvanced"></tv-information-panel>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -119,10 +138,11 @@
<div class="bottom-page-gap"> <div class="bottom-page-gap">
</div> </div>
</section> </section>
</div> </div>
</ng-template> </ng-template>
</div> </div>

@ -5,10 +5,13 @@ import { DomSanitizer } from "@angular/platform-browser";
import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component"; import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component";
import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; import { EpisodeRequestComponent, EpisodeRequestData } from "../../../shared/episode-request/episode-request.component";
import { IAdvancedData, IChildRequests, ISonarrProfile, ISonarrRootFolder, ITvRequests, RequestType } from "../../../interfaces"; import { IAdvancedData, IChildRequests, ITvRequests, RequestType } from "../../../interfaces";
import { AuthService } from "../../../auth/auth.service"; import { AuthService } from "../../../auth/auth.service";
import { NewIssueComponent } from "../shared/new-issue/new-issue.component"; import { NewIssueComponent } from "../shared/new-issue/new-issue.component";
import { TvAdvancedOptionsComponent } from "./panels/tv-advanced-options/tv-advanced-options.component";
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
@Component({ @Component({
templateUrl: "./tv-details.component.html", templateUrl: "./tv-details.component.html",
@ -30,6 +33,7 @@ export class TvDetailsComponent implements OnInit {
constructor(private searchService: SearchV2Service, private route: ActivatedRoute, constructor(private searchService: SearchV2Service, private route: ActivatedRoute,
private sanitizer: DomSanitizer, private imageService: ImageService, private sanitizer: DomSanitizer, private imageService: ImageService,
public dialog: MatDialog, public messageService: MessageService, private requestService: RequestService, public dialog: MatDialog, public messageService: MessageService, private requestService: RequestService,
private requestService2: RequestServiceV2,
private auth: AuthService, private sonarrService: SonarrService) { private auth: AuthService, private sonarrService: SonarrService) {
this.route.params.subscribe((params: any) => { this.route.params.subscribe((params: any) => {
this.tvdbId = params.tvdbId; this.tvdbId = params.tvdbId;
@ -44,6 +48,11 @@ export class TvDetailsComponent implements OnInit {
public async load() { public async load() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
if (this.isAdmin) {
this.showAdvanced = await this.sonarrService.isEnabled();
}
if (this.fromSearch) { if (this.fromSearch) {
this.tv = await this.searchService.getTvInfoWithMovieDbId(this.tvdbId); this.tv = await this.searchService.getTvInfoWithMovieDbId(this.tvdbId);
this.tvdbId = this.tv.id; this.tvdbId = this.tv.id;
@ -60,8 +69,8 @@ export class TvDetailsComponent implements OnInit {
this.tv.background = this.sanitizer.bypassSecurityTrustStyle("url(" + tvBanner + ")"); this.tv.background = this.sanitizer.bypassSecurityTrustStyle("url(" + tvBanner + ")");
} }
public async request() { public async request(userId: string) {
this.dialog.open(EpisodeRequestComponent, { width: "800px", data: this.tv, panelClass: 'modal-panel' }) this.dialog.open(EpisodeRequestComponent, { width: "800px", data: <EpisodeRequestData> { series: this.tv, requestOnBehalf: userId }, panelClass: 'modal-panel' })
} }
public async issue() { public async issue() {
@ -81,6 +90,28 @@ export class TvDetailsComponent implements OnInit {
}); });
} }
public async openAdvancedOptions() {
const dialog = this.dialog.open(TvAdvancedOptionsComponent, { width: "700px", data: <IAdvancedData>{ tvRequest: this.showRequest }, panelClass: 'modal-panel' })
await dialog.afterClosed().subscribe(async result => {
if (result) {
// get the name and ids
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0];
result.profile = result.profiles.filter(f => f.id === +result.profileId)[0];
await this.requestService2.updateTvAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.tv.id }).toPromise();
this.setAdvancedOptions(result);
}
});
}
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) { public setAdvancedOptions(data: IAdvancedData) {
this.advancedOptions = data; this.advancedOptions = data;
console.log(this.advancedOptions); console.log(this.advancedOptions);

@ -223,3 +223,7 @@
text-align: center; text-align: center;
padding-top: 15%; padding-top: 15%;
} }
.content-end {
text-align: end;
}

@ -1,8 +1,6 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { SearchService, RequestService, RadarrService } from "../services";
import {CarouselModule} from 'primeng/carousel'; import {CarouselModule} from 'primeng/carousel';
import { SharedModule } from "../shared/shared.module"; import { SharedModule } from "../shared/shared.module";
@ -13,6 +11,7 @@ import { PipeModule } from "../pipes/pipe.module";
import * as fromComponents from './components'; import * as fromComponents from './components';
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";
import { ArtistDetailsComponent } from "./components/artist/artist-details.component"; import { ArtistDetailsComponent } from "./components/artist/artist-details.component";
import { ReactiveFormsModule } from "@angular/forms";
const routes: Routes = [ const routes: Routes = [
@ -25,6 +24,7 @@ const routes: Routes = [
imports: [ imports: [
RouterModule.forChild(routes), RouterModule.forChild(routes),
SharedModule, SharedModule,
ReactiveFormsModule,
PipeModule, PipeModule,
CarouselModule, CarouselModule,
], ],

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { ICheckbox, ICreateWizardUser, IIdentityResult, INotificationPreferences, IResetPasswordToken, IUpdateLocalUser, IUser, IWizardUserResult } from "../interfaces"; import { ICheckbox, ICreateWizardUser, IIdentityResult, INotificationPreferences, IResetPasswordToken, IUpdateLocalUser, IUser, IUserDropdown, IWizardUserResult } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@Injectable() @Injectable()
@ -32,6 +32,10 @@ export class IdentityService extends ServiceHelpers {
return this.http.get<IUser[]>(`${this.url}Users`, {headers: this.headers}); return this.http.get<IUser[]>(`${this.url}Users`, {headers: this.headers});
} }
public getUsersDropdown(): Observable<IUserDropdown[]> {
return this.http.get<IUserDropdown[]>(`${this.url}dropdown/Users`, {headers: this.headers});
}
public getAllAvailableClaims(): Observable<ICheckbox[]> { public getAllAvailableClaims(): Observable<ICheckbox[]> {
return this.http.get<ICheckbox[]>(`${this.url}Claims`, {headers: this.headers}); return this.http.get<ICheckbox[]>(`${this.url}Claims`, {headers: this.headers});
} }

@ -16,7 +16,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12" *ngFor="let season of series.seasonRequests"> <div class="col-12" *ngFor="let season of data.series.seasonRequests">
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
@ -24,9 +24,9 @@
Season {{season.seasonNumber}}</mat-checkbox> Season {{season.seasonNumber}}</mat-checkbox>
<span *ngIf="season.seasonAvailable">Season {{season.seasonNumber}}</span> <span *ngIf="season.seasonAvailable">Season {{season.seasonNumber}}</span>
</mat-panel-title> </mat-panel-title>
<!-- <mat-panel-description> <mat-panel-description>
Description <!-- Description -->
</mat-panel-description> --> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="row" *ngFor="let ep of season.episodes"> <div class="row" *ngFor="let ep of season.episodes">

@ -4,8 +4,12 @@ import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { ISearchTvResultV2 } from "../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../interfaces/ISearchTvResultV2";
import { RequestService, MessageService } from "../../services"; import { RequestService, MessageService } from "../../services";
import { ITvRequestViewModel, ISeasonsViewModel, IEpisodesRequests, INewSeasonRequests } from "../../interfaces"; import { ITvRequestViewModel, ISeasonsViewModel, IEpisodesRequests, INewSeasonRequests } from "../../interfaces";
import { ThousandShortPipe } from "../../pipes/ThousandShortPipe";
export interface EpisodeRequestData {
series: ISearchTvResultV2;
requestOnBehalf: string | undefined;
}
@Component({ @Component({
selector: "episode-request", selector: "episode-request",
templateUrl: "episode-request.component.html", templateUrl: "episode-request.component.html",
@ -14,7 +18,7 @@ export class EpisodeRequestComponent implements OnInit {
public loading: boolean; public loading: boolean;
constructor(public dialogRef: MatDialogRef<EpisodeRequestComponent>, @Inject(MAT_DIALOG_DATA) public series: ISearchTvResultV2, constructor(public dialogRef: MatDialogRef<EpisodeRequestComponent>, @Inject(MAT_DIALOG_DATA) public data: EpisodeRequestData,
private requestService: RequestService, private notificationService: MessageService) { } private requestService: RequestService, private notificationService: MessageService) { }
public ngOnInit() { public ngOnInit() {
@ -25,24 +29,27 @@ export class EpisodeRequestComponent implements OnInit {
public async submitRequests() { public async submitRequests() {
// Make sure something has been selected // Make sure something has been selected
const selected = this.series.seasonRequests.some((season) => { const selected = this.data.series.seasonRequests.some((season) => {
return season.episodes.some((ep) => { return season.episodes.some((ep) => {
return ep.selected; return ep.selected;
}); });
}); });
debugger; debugger;
if (!selected && !this.series.requestAll && !this.series.firstSeason && !this.series.latestSeason) { if (!selected && !this.data.series.requestAll && !this.data.series.firstSeason && !this.data.series.latestSeason) {
this.notificationService.send("You need to select some episodes!", "OK"); this.notificationService.send("You need to select some episodes!", "OK");
return; return;
} }
this.series.requested = true; this.data.series.requested = true;
const viewModel = <ITvRequestViewModel>{ firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id }; const viewModel = <ITvRequestViewModel>{
firstSeason: this.data.series.firstSeason, latestSeason: this.data.series.latestSeason, requestAll: this.data.series.requestAll, tvDbId: this.data.series.id,
requestOnBehalf: this.data.requestOnBehalf
};
viewModel.seasons = []; viewModel.seasons = [];
this.series.seasonRequests.forEach((season) => { this.data.series.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel>{ seasonNumber: season.seasonNumber, episodes: [] }; const seasonsViewModel = <ISeasonsViewModel>{ seasonNumber: season.seasonNumber, episodes: [] };
if (!this.series.latestSeason && !this.series.requestAll && !this.series.firstSeason) { if (!this.data.series.latestSeason && !this.data.series.requestAll && !this.data.series.firstSeason) {
season.episodes.forEach(ep => { season.episodes.forEach(ep => {
if (ep.selected) { if (ep.selected) {
ep.requested = true; ep.requested = true;
@ -57,9 +64,9 @@ export class EpisodeRequestComponent implements OnInit {
if (requestResult.result) { if (requestResult.result) {
this.notificationService.send( this.notificationService.send(
`Request for ${this.series.title} has been added successfully`); `Request for ${this.data.series.title} has been added successfully`);
this.series.seasonRequests.forEach((season) => { this.data.series.seasonRequests.forEach((season) => {
season.episodes.forEach((ep) => { season.episodes.forEach((ep) => {
ep.selected = false; ep.selected = false;
}); });
@ -90,17 +97,17 @@ export class EpisodeRequestComponent implements OnInit {
} }
public async requestAllSeasons() { public async requestAllSeasons() {
this.series.requestAll = true; this.data.series.requestAll = true;
await this.submitRequests(); await this.submitRequests();
} }
public async requestFirstSeason() { public async requestFirstSeason() {
this.series.firstSeason = true; this.data.series.firstSeason = true;
await this.submitRequests(); await this.submitRequests();
} }
public async requestLatestSeason() { public async requestLatestSeason() {
this.series.latestSeason = true; this.data.series.latestSeason = true;
await this.submitRequests(); await this.submitRequests();
} }
} }

@ -128,9 +128,9 @@ table {
border: 1px solid rgba(0, 0, 0, 0.18); border: 1px solid rgba(0, 0, 0, 0.18);
} }
::ng-deep .mat-toolbar.mat-primary { // ::ng-deep .mat-toolbar.mat-primary {
margin-bottom: 0.5em; // margin-bottom: 0.5em;
} // }
::ng-deep .dark .mat-form-field.mat-focused .mat-form-field-label { ::ng-deep .dark .mat-form-field.mat-focused .mat-form-field-label {
color: $accent-dark; color: $accent-dark;

@ -275,6 +275,31 @@ namespace Ombi.Controllers.V1
return model.OrderBy(x => x.UserName); return model.OrderBy(x => x.UserName);
} }
/// <summary>
/// Gets all users for dropdown purposes.
/// </summary>
/// <returns>Basic Information about all users</returns>
[HttpGet("dropdown/Users")]
[PowerUser]
public async Task<IEnumerable<UserViewModelDropdown>> GetAllUsersDropdown()
{
var users = await UserManager.Users.Where(x => x.UserType != UserType.SystemUser)
.ToListAsync();
var model = new List<UserViewModelDropdown>();
foreach (var user in users)
{
model.Add(new UserViewModelDropdown
{
Id = user.Id,
Username = user.UserName
});
}
return model.OrderBy(x => x.Username);
}
/// <summary> /// <summary>
/// Gets the current logged in user. /// Gets the current logged in user.
/// </summary> /// </summary>

@ -272,7 +272,11 @@
"AllSeasonsTooltip": "This will request every season for this show", "AllSeasonsTooltip": "This will request every season for this show",
"FirstSeasonTooltip": "This will only request the First Season for this show", "FirstSeasonTooltip": "This will only request the First Season for this show",
"LatestSeasonTooltip": "This will only request the Latest Season for this show" "LatestSeasonTooltip": "This will only request the Latest Season for this show"
} },
"SonarrConfiguration": "Sonarr Configuration",
"RadarrConfiguration": "Radarr Configuration",
"RequestOnBehalf": "Request on behalf of",
"PleaseSelectUser": "Please select a user"
}, },
"Discovery": { "Discovery": {
"PopularTab": "Popular", "PopularTab": "Popular",

Loading…
Cancel
Save