mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-15 01:32:55 -07:00
* Added the ability to request on behalf of a user
* Moved the "advanced" section into a small cog icon on the media details page * Added some more information on the movie panel e.g. Requested By user
This commit is contained in:
parent
87233a7fd3
commit
8220f41e0b
37 changed files with 563 additions and 341 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -270,7 +286,7 @@ namespace Ombi.Core.Engine
|
||||||
allRequests = allRequests.Where(x => x.Available);
|
allRequests = allRequests.Where(x => x.Available);
|
||||||
break;
|
break;
|
||||||
case RequestStatus.Denied:
|
case RequestStatus.Denied:
|
||||||
allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available);
|
allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -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,19 +670,19 @@ 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);
|
||||||
|
|
||||||
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification);
|
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
await NotificationHelper.NewRequest(model);
|
await NotificationHelper.NewRequest(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
@ -46,6 +46,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,127 +2,145 @@
|
||||||
<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">
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<media-poster [posterPath]="tv.images?.medium"></media-poster>
|
|
||||||
|
|
||||||
<!--Next to poster-->
|
|
||||||
<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">
|
|
||||||
</social-icons>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-lg-6 col-xl-6 media-row">
|
|
||||||
|
|
||||||
<button *ngIf="!tv.fullyAvailable" mat-raised-button class="btn-spacing" color="primary" (click)="request()"><i class="fa fa-plus"></i>
|
|
||||||
{{ 'Common.Request' | translate }}</button>
|
|
||||||
|
|
||||||
<button *ngIf="tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent" [disabled]>
|
|
||||||
<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]>
|
|
||||||
<i class="fa fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>
|
|
||||||
|
|
||||||
<button mat-raised-button class="btn-spacing" color="danger" (click)="issue()">
|
|
||||||
<i class="fa fa-exclamation"></i> {{
|
|
||||||
'Requests.ReportIssue' | translate }}</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<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-content class="medium-font">
|
|
||||||
<tv-information-panel [tv]="tv" [request]="showRequest" [advancedOptions]="showAdvanced"></tv-information-panel>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="col-12 col-md-10">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
|
||||||
<mat-card class="mat-elevation-z8 spacing-below">
|
<media-poster [posterPath]="tv.images?.medium"></media-poster>
|
||||||
<mat-card-content>
|
|
||||||
{{tv.overview}}
|
<!--Next to poster-->
|
||||||
|
<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">
|
||||||
|
</social-icons>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
{{ 'Common.Request' | translate }}</button>
|
||||||
|
|
||||||
|
<button *ngIf="tv.fullyAvailable" mat-raised-button class="btn-spacing" color="accent"
|
||||||
|
[disabled]>
|
||||||
|
<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]>
|
||||||
|
<i class="fa fa-check"></i> {{'Common.PartiallyAvailable' | translate }}</button>
|
||||||
|
|
||||||
|
<button mat-raised-button class="btn-spacing" color="danger" (click)="issue()">
|
||||||
|
<i class="fa fa-exclamation"></i> {{
|
||||||
|
'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]="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 class="row">
|
||||||
|
<div class="col-12 col-md-2">
|
||||||
|
<mat-card class="mat-elevation-z8">
|
||||||
|
<mat-card-content class="medium-font">
|
||||||
|
<tv-information-panel [tv]="tv" [request]="showRequest"
|
||||||
|
[advancedOptions]="showAdvanced"></tv-information-panel>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<cast-carousel [cast]="tv.cast"></cast-carousel>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<mat-card class="mat-elevation-z8 spacing-below">
|
||||||
|
<mat-card-content>
|
||||||
|
{{tv.overview}}
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<cast-carousel [cast]="tv.cast"></cast-carousel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-2">
|
<div class="col-12 col-md-2">
|
||||||
|
|
||||||
<!--Just some space yo-->
|
<!--Just some space yo-->
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<div class="issuesPanel">
|
||||||
|
<issues-panel [providerId]="tv.theTvDbId" [isAdmin]="isAdmin"></issues-panel>
|
||||||
|
</div>
|
||||||
|
<mat-accordion>
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Requests
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<tv-requests-panel [tvRequest]="tvRequest" [isAdmin]="isAdmin"></tv-requests-panel>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
</mat-accordion>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-12 col-md-10">
|
|
||||||
<div class="issuesPanel">
|
|
||||||
<issues-panel [providerId]="tv.theTvDbId" [isAdmin]="isAdmin"></issues-panel>
|
|
||||||
</div>
|
</div>
|
||||||
<mat-accordion>
|
|
||||||
<mat-expansion-panel>
|
|
||||||
<mat-expansion-panel-header>
|
|
||||||
<mat-panel-title>
|
|
||||||
Requests
|
|
||||||
</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
<tv-requests-panel [tvRequest]="tvRequest" [isAdmin]="isAdmin"></tv-requests-panel>
|
|
||||||
</mat-expansion-panel>
|
|
||||||
|
|
||||||
</mat-accordion>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="bottom-page-gap">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="bottom-page-gap">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</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);
|
||||||
|
|
|
@ -222,4 +222,8 @@
|
||||||
.no-info {
|
.no-info {
|
||||||
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>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -271,7 +271,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…
Add table
Add a link
Reference in a new issue