Got most of the requesting stuff done! !wip #2313

pull/2478/head
Jamie 6 years ago
parent cda7c0fe4c
commit b72905ab4a

@ -16,5 +16,10 @@ namespace Ombi.Api.Lidarr
Task<AlbumLookup> GetAlbumByForeignId(string foreignArtistId, string apiKey, string baseUrl);
Task<List<ArtistResult>> GetArtists(string apiKey, string baseUrl);
Task<List<AlbumResponse>> GetAllAlbums(string apiKey, string baseUrl);
Task<ArtistResult> AddArtist(ArtistAdd artist, string apiKey, string baseUrl);
Task<AlbumResponse> MontiorAlbum(int albumId, string apiKey, string baseUrl);
Task<List<AlbumResponse>> GetAllAlbumsByArtistId(int artistId, string apiKey, string baseUrl);
Task<List<MetadataProfile>> GetMetadataProfile(string apiKey, string baseUrl);
Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl);
}
}

@ -63,13 +63,13 @@ namespace Ombi.Api.Lidarr
return Api.Request<ArtistResult>(request);
}
public Task<ArtistResult> GetArtistByForeignId(string foreignArtistId, string apiKey, string baseUrl)
public async Task<ArtistResult> GetArtistByForeignId(string foreignArtistId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist/lookup", baseUrl, HttpMethod.Get);
request.AddQueryString("term", $"lidarr:{foreignArtistId}");
AddHeaders(request, apiKey);
return Api.Request<ArtistResult>(request);
return (await Api.Request<List<ArtistResult>>(request)).FirstOrDefault();
}
public async Task<AlbumLookup> GetAlbumByForeignId(string foreignArtistId, string apiKey, string baseUrl)
@ -105,6 +105,48 @@ namespace Ombi.Api.Lidarr
return Api.Request<List<AlbumResponse>>(request);
}
public Task<ArtistResult> AddArtist(ArtistAdd artist, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist", baseUrl, HttpMethod.Post);
request.AddJsonBody(artist);
AddHeaders(request, apiKey);
return Api.Request<ArtistResult>(request);
}
public Task<AlbumResponse> MontiorAlbum(int albumId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album/monitor", baseUrl, HttpMethod.Put);
request.AddJsonBody(new
{
albumIds = new[] { albumId },
monitored = true
});
AddHeaders(request, apiKey);
return Api.Request<AlbumResponse>(request);
}
public Task<List<AlbumResponse>> GetAllAlbumsByArtistId(int artistId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album", baseUrl, HttpMethod.Get);
request.AddQueryString("artistId", artistId.ToString());
AddHeaders(request, apiKey);
return Api.Request<List<AlbumResponse>>(request);
}
public Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/languageprofile", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<LanguageProfiles>>(request);
}
public Task<List<MetadataProfile>> GetMetadataProfile(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/metadataprofile", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<MetadataProfile>>(request);
}
private void AddHeaders(Request request, string key)
{
request.AddHeader("X-Api-Key", key);

@ -0,0 +1,48 @@
using System;
using System.Net.Mime;
namespace Ombi.Api.Lidarr.Models
{
public class ArtistAdd
{
public string status { get; set; }
public bool ended { get; set; }
public string artistName { get; set; }
public string foreignArtistId { get; set; }
public int tadbId { get; set; }
public int discogsId { get; set; }
public string overview { get; set; }
public string disambiguation { get; set; }
public Link[] links { get; set; }
public Image[] images { get; set; }
public string remotePoster { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }
public string cleanName { get; set; }
public string sortName { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public Statistics statistics { get; set; }
public Addoptions addOptions { get; set; }
public string rootFolderPath { get; set; }
}
public class Addoptions
{
/// <summary>
/// Future = 1
/// Missing = 2
/// Existing = 3
/// First = 5
/// Latest = 4
/// None = 6
/// </summary>
public int selectedOption { get; set; }
public bool monitored { get; set; }
public bool searchForMissingAlbums { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class LanguageProfiles
{
public string name { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class MetadataProfile
{
public string name { get; set; }
public int id { get; set; }
}
}

@ -15,6 +15,7 @@ using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Senders;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
@ -29,11 +30,11 @@ namespace Ombi.Core.Engine
INotificationHelper helper, IRuleEvaluator r, ILogger<MusicRequestEngine> log,
OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache,
ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub, ILidarrApi lidarr,
ISettingsService<LidarrSettings> lidarrSettings)
ISettingsService<LidarrSettings> lidarrSettings, IMusicSender sender)
: base(user, requestService, r, manager, cache, ombiSettings, sub)
{
NotificationHelper = helper;
//Sender = sender;
_musicSender = sender;
Logger = log;
_requestLog = rl;
_lidarrApi = lidarr;
@ -46,6 +47,7 @@ namespace Ombi.Core.Engine
private readonly IRepository<RequestLog> _requestLog;
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
private readonly IMusicSender _musicSender;
/// <summary>
/// Requests the Album.
@ -79,7 +81,8 @@ namespace Ombi.Core.Engine
RequestedUserId = userDetails.Id,
Title = album.title,
Disk = album.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url,
Cover = album.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url
Cover = album.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url,
ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty
};
if (requestModel.Cover.IsNullOrEmpty())
{
@ -341,26 +344,25 @@ namespace Ombi.Core.Engine
if (request.Approved)
{
//TODO
//var result = await Sender.Send(request);
//if (result.Success && result.Sent)
//{
// return new RequestEngineResult
// {
// Result = true
// };
//}
//if (!result.Success)
//{
// Logger.LogWarning("Tried auto sending movie but failed. Message: {0}", result.Message);
// return new RequestEngineResult
// {
// Message = result.Message,
// ErrorMessage = result.Message,
// Result = false
// };
//}
var result = await _musicSender.Send(request);
if (result.Success && result.Sent)
{
return new RequestEngineResult
{
Result = true
};
}
if (!result.Success)
{
Logger.LogWarning("Tried auto sending album but failed. Message: {0}", result.Message);
return new RequestEngineResult
{
Message = result.Message,
ErrorMessage = result.Message,
Result = false
};
}
// If there are no providers then it's successful but movie has not been sent
}

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Senders
{
public interface IMusicSender
{
Task<SenderResult> Send(AlbumRequest model);
}
}

@ -0,0 +1,110 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
using Serilog;
namespace Ombi.Core.Senders
{
public class MusicSender : IMusicSender
{
public MusicSender(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi)
{
_lidarrSettings = lidarr;
_lidarrApi = lidarrApi;
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
public async Task<SenderResult> Send(AlbumRequest model)
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SendToLidarr(model, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
}
private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)
{
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
//if (model.QualityOverride > 0)
//{
// qualityToUse = model.QualityOverride;
//}
var rootFolderPath = /*model.RootPathOverride <= 0 ?*/ settings.DefaultRootPath /*: await RadarrRootPath(model.RootPathOverride, settings)*/;
// Need to get the artist
var artist = await _lidarrApi.GetArtistByForeignId(model.ForeignArtistId, settings.ApiKey, settings.FullUri);
if (artist == null || artist.id <= 0)
{
// Create artist
var newArtist = new ArtistAdd
{
foreignArtistId = model.ForeignArtistId,
addOptions = new Addoptions
{
monitored = true,
searchForMissingAlbums = false,
selectedOption = 6 // None
},
added = DateTime.Now,
monitored = true,
albumFolder = settings.AlbumFolder,
artistName = model.ArtistName,
cleanName = model.ArtistName.ToLowerInvariant().RemoveSpaces(),
images = new Image[] { },
languageProfileId = settings.LanguageProfileId,
links = new Link[] {},
metadataProfileId = settings.MetadataProfileId,
qualityProfileId = qualityToUse,
rootFolderPath = rootFolderPath,
};
var result = await _lidarrApi.AddArtist(newArtist, settings.ApiKey, settings.FullUri);
if (result != null && result.id > 0)
{
// Setup the albums
await SetupAlbum(model, result, settings);
}
}
else
{
await SetupAlbum(model, artist, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Album is already monitored" };
}
private async Task<SenderResult> SetupAlbum(AlbumRequest model, ArtistResult artist, LidarrSettings settings)
{
// Get the album id
var albums = await _lidarrApi.GetAllAlbumsByArtistId(artist.id, settings.ApiKey, settings.FullUri);
// Get the album we want.
var album = albums.FirstOrDefault(x =>
x.foreignAlbumId.Equals(model.ForeignAlbumId, StringComparison.InvariantCultureIgnoreCase));
if (album == null)
{
return new SenderResult { Message = "Could not find album in Lidarr", Sent = false, Success = false };
}
var result = await _lidarrApi.MontiorAlbum(album.id, settings.ApiKey, settings.FullUri);
if (result.monitored)
{
return new SenderResult {Message = "Album has been requested!", Sent = true, Success = true};
}
return new SenderResult { Message = "Could not set album to monitored", Sent = false, Success = false };
}
}
}

@ -88,6 +88,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMusicSearchEngine, MusicSearchEngine>();
services.AddTransient<IMusicRequestEngine, MusicRequestEngine>();
services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMusicSender, MusicSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
}

@ -75,5 +75,10 @@ namespace Ombi.Helpers
return -1;
}
public static string RemoveSpaces(this string str)
{
return str.Replace(" ", "");
}
}
}

@ -8,5 +8,8 @@ namespace Ombi.Settings.Settings.Models.External
public string ApiKey { get; set; }
public string DefaultQualityProfile { get; set; }
public string DefaultRootPath { get; set; }
public bool AlbumFolder { get; set; }
public int LanguageProfileId { get; set; }
public int MetadataProfileId { get; set; }
}
}

@ -8,6 +8,11 @@ export interface IRadarrProfile {
id: number;
}
export interface IProfiles {
name: string;
id: number;
}
export interface IMinimumAvailability {
value: string;
name: string;

@ -23,13 +23,14 @@ export interface IMovieRequests extends IFullBaseRequest {
export interface IAlbumRequest extends IBaseRequest {
foreignAlbumId: string;
foreignArtistId: string;
Disk: string;
disk: string;
cover: string;
releaseDate: Date;
artistName: string;
subscribed: boolean;
showSubscribe: boolean;
background: any;
}
export interface IAlbumRequestModel {

@ -90,6 +90,9 @@ export interface ILidarrSettings extends IExternalSettings {
defaultQualityProfile: string;
defaultRootPath: string;
fullRootPath: string;
metadataProfileId: number;
languageProfileId: number;
albumFolder: boolean;
}
export interface ILandingPageSettings extends ISettings {

@ -0,0 +1,270 @@
<div class="form-group">
<div class="input-group">
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)">
<span class="input-group-btn">
<button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = !filterDisplay">
<i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}
</button>
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fa fa-sort"></i> {{ 'Requests.Sort' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a (click)="setOrder(OrderType.RequestedDateAsc, $event)">{{ 'Requests.SortRequestDateAsc' | translate }}
</a>
<a class="active" (click)="setOrder(OrderType.RequestedDateDesc, $event)">{{ 'Requests.SortRequestDateDesc' | translate }}
</a>
<a (click)="setOrder(OrderType.TitleAsc, $event)">{{ 'Requests.SortTitleAsc' | translate}}
</a>
<a (click)="setOrder(OrderType.TitleDesc, $event)">{{ 'Requests.SortTitleDesc' | translate}}
</a>
<a (click)="setOrder(OrderType.StatusAsc, $event)">{{ 'Requests.SortStatusAsc' | translate}}
</a>
<a (click)="setOrder(OrderType.StatusDesc, $event)">{{ 'Requests.SortStatusDesc' | translate}}
</a>
</li>
</ul>
</span>
</div>
</div>
<br />
<div>
<div *ngFor="let request of albumRequests" class="col-md-4">
<div class="row">
<div class="album-bg backdrop" [style.background-image]="request.background"></div>
<div class="album-tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-12 small-padding">
<img *ngIf="request.disk" class="img-responsive poster album-cover" src="{{request.disk}}" alt="poster">
</div>
<div class="col-sm-12 small-padding">
<div>
<h4>
<a href="" target="_blank">
{{request.title}}
</a>
</h4>
<h5>
<a href="">
{{request.artistName}}
</a>
</h5>
</div>
<div class="request-info">
<div class="request-by">
<span>{{ 'Requests.RequestedBy' | translate }} </span>
<span *ngIf="!isAdmin">{{request.requestedUser.userName}}</span>
<span *ngIf="isAdmin && request.requestedUser.alias">{{request.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !request.requestedUser.alias">{{request.requestedUser.userName}}</span>
</div>
<div class="request-status">
<span>{{ 'Requests.Status' | translate }} </span>
<span class="label label-success" id="requestedStatusLabel">{{request.status}}</span>
</div>
<div class="requested-status">
<span>{{ 'Requests.RequestStatus' | translate }} </span>
<span *ngIf="request.available" class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
<span *ngIf="request.approved && !request.available" id="processingRequestLabel" class="label label-info" [translate]="'Common.ProcessingRequest'"></span>
<span *ngIf="request.denied" class="label label-danger" id="requestDeclinedLabel" [translate]="'Common.RequestDenied'"></span>
<span *ngIf="request.deniedReason" title="{{request.deniedReason}}">
<i class="fa fa-info-circle"></i>
</span>
<span *ngIf="!request.approved && !request.availble && !request.denied" id="pendingApprovalLabel" class="label label-warning"
[translate]="'Common.PendingApproval'"></span>
</div>
<div *ngIf="request.denied" id="requestDenied">
{{ 'Requests.Denied' | translate }}
<i style="color:red;" class="fa fa-check"></i>
</div>
<div id="releaseDate">{{ 'Requests.TheatricalRelease' | translate: {date: request.releaseDate | date: 'mediumDate'} }}</div>
<div *ngIf="request.digitalReleaseDate" id="digitalReleaseDate">{{ 'Requests.DigitalRelease' | translate: {date: request.digitalReleaseDate | date: 'mediumDate'} }}</div>
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | date}}</div>
<br />
</div>
<div *ngIf="isAdmin">
<div *ngIf="request.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }}
<span>{{request.qualityOverrideTitle}} </span>
</div>
<div *ngIf="request.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' | translate }}
<span>{{request.rootPathOverrideTitle}} </span>
</div>
</div>
</div>
<div class="col-sm-12 small-padding">
<!-- <div class="row">
<div class="col-md-2 col-md-push-6">
<a *ngIf="request.showSubscribe && !request.subscribed" style="color:white" (click)="subscribe(request)" pTooltip="Subscribe for notifications">
<i class="fa fa-rss"></i>
</a>
<a *ngIf="request.showSubscribe && request.subscribed" style="color:red" (click)="unSubscribe(request)" pTooltip="Unsubscribe notification">
<i class="fa fa-rss"></i>
</a>
</div>
</div> -->
<div *ngIf="isAdmin">
<div *ngIf="!request.approved" id="approveBtn">
<form>
<button (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve" type="submit">
<i class="fa fa-plus"></i> {{ 'Common.Approve' | translate }}
</button>
</form>
<!--Radarr Root Folder-->
<!-- <div *ngIf="radarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let folder of radarrRootFolders">
<a href="#" (click)="selectRootFolder(request, folder, $event)">{{folder.path}}</a>
</li>
</ul>
</div> -->
<!--Radarr Quality Profiles -->
<!-- <div *ngIf="radarrProfiles" class="btn-group btn-split" id="changeQualityBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let profile of radarrProfiles">
<a href="#" (click)="selectQualityProfile(request, profile, $event)">{{profile.name}}</a>
</li>
</ul>
</div> -->
<div *ngIf="!request.denied" id="denyBtn">
<button type="button" (click)="deny(request)" class="btn btn-sm btn-danger-outline deny">
<i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}
</button>
</div>
</div>
<form id="removeBtn">
<button (click)="removeRequest(request)" style="text-align: right" class="btn btn-sm btn-danger-outline delete">
<i class="fa fa-minus"></i> {{ 'Requests.Remove' | translate }}
</button>
</form>
<form id="markBtnGroup">
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
value="false" class="btn btn-sm btn-info-outline change">
<i class="fa fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}
</button>
<button id="availableBtn" *ngIf="!request.available" (click)="changeAvailability(request, true)" style="text-align: right"
value="true" class="btn btn-sm btn-success-outline change">
<i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button>
</form>
</div>
<!-- <div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories">
<a [routerLink]="" (click)="reportIssue(cat, request)">{{cat.value}}</a>
</li>
</ul>
</div> -->
</div>
</div>
<br/>
<br/>
</div>
<p-paginator [rows]="10" [totalRecords]="totalMovies" (onPageChange)="paginate($event)"></p-paginator>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId"></issue-report>
<p-sidebar [(visible)]="filterDisplay" styleClass="ui-sidebar-md side-back side-small">
<h3>{{ 'Requests.Filter' | translate }}</h3>
<hr>
<div>
<h4>{{ 'Filter.FilterHeaderAvailability' | translate }}</h4>
<div class="form-group">
<div class="radio">
<input type="radio" id="Available" name="Availability" (click)="filterAvailability(filterType.Available, $event)">
<label for="Available">{{ 'Common.Available' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="notAvailable" name="Availability" (click)="filterAvailability(filterType.NotAvailable, $event)">
<label for="notAvailable">{{ 'Common.NotAvailable' | translate }}</label>
</div>
</div>
</div>
<div>
<h4>{{ 'Filter.FilterHeaderRequestStatus' | translate }}</h4>
<div class="form-group">
<div class="radio">
<input type="radio" id="approved" name="Status" (click)="filterStatus(filterType.Approved, $event)">
<label for="approved">{{ 'Filter.Approved' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="Processing" name="Status" (click)="filterStatus(filterType.Processing, $event)">
<label for="Processing">{{ 'Common.ProcessingRequest' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="pendingApproval" name="Status" (click)="filterStatus(filterType.PendingApproval, $event)">
<label for="pendingApproval">{{ 'Filter.PendingApproval' | translate }}</label>
</div>
</div>
</div>
<button class="btn btn-sm btn-primary-outline" (click)="clearFilter($event)">
<i class="fa fa-filter"></i> {{ 'Filter.ClearFilter' | translate }}</button>
</p-sidebar>

@ -0,0 +1,350 @@
import { PlatformLocation } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../../auth/auth.service";
import { FilterType, IAlbumRequest, IFilter, IIssueCategory, IPagenator, OrderType } from "../../interfaces";
import { NotificationService, RequestService } from "../../services";
@Component({
selector: "music-requests",
templateUrl: "./musicrequests.component.html",
})
export class MusicRequestsComponent implements OnInit {
public albumRequests: IAlbumRequest[];
public defaultPoster: string;
public searchChanged: Subject<string> = new Subject<string>();
public searchText: string;
public isAdmin: boolean; // Also PowerUser
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequest: IAlbumRequest;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
public filterDisplay: boolean;
public filter: IFilter;
public filterType = FilterType;
public orderType: OrderType = OrderType.RequestedDateDesc;
public OrderType = OrderType;
public totalAlbums: number = 100;
private currentlyLoaded: number;
private amountToLoad: number;
constructor(
private requestService: RequestService,
private auth: AuthService,
private notificationService: NotificationService,
private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) {
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.resetSearch();
return;
}
this.requestService.searchAlbumRequests(this.searchText)
.subscribe(m => {
this.setOverrides(m);
this.albumRequests = m;
});
});
this.defaultPoster = "../../../images/default-music-placeholder.png";
const base = this.platformLocation.getBaseHrefFromDOM();
if (base) {
this.defaultPoster = "../../.." + base + "/images/default-music-placeholder.png";
}
}
public ngOnInit() {
this.amountToLoad = 10;
this.currentlyLoaded = 10;
this.filter = {
availabilityFilter: FilterType.None,
statusFilter: FilterType.None,
};
this.loadInit();
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
}
public paginate(event: IPagenator) {
const skipAmount = event.first;
this.loadRequests(this.amountToLoad, skipAmount);
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public removeRequest(request: IAlbumRequest) {
this.requestService.removeAlbumRequest(request);
this.removeRequestFromUi(request);
this.loadRequests(this.amountToLoad, this.currentlyLoaded = 0);
}
public changeAvailability(request: IAlbumRequest, available: boolean) {
request.available = available;
if (available) {
this.requestService.markAlbumAvailable({ id: request.id }).subscribe(x => {
if (x.result) {
this.notificationService.success(
`${request.title} Is now available`);
} else {
this.notificationService.warning("Request Available", x.message ? x.message : x.errorMessage);
request.approved = false;
}
});
} else {
this.requestService.markAlbumUnavailable({ id: request.id }).subscribe(x => {
if (x.result) {
this.notificationService.success(
`${request.title} Is now unavailable`);
} else {
this.notificationService.warning("Request Available", x.message ? x.message : x.errorMessage);
request.approved = false;
}
});
}
}
public approve(request: IAlbumRequest) {
request.approved = true;
this.approveRequest(request);
}
public deny(request: IAlbumRequest) {
request.denied = true;
this.denyRequest(request);
}
// public selectRootFolder(searchResult: IAlbumRequest, rootFolderSelected: IRadarrRootFolder, event: any) {
// event.preventDefault();
// // searchResult.rootPathOverride = rootFolderSelected.id;
// this.setOverride(searchResult);
// this.updateRequest(searchResult);
// }
// public selectQualityProfile(searchResult: IMovieRequests, profileSelected: IRadarrProfile, event: any) {
// event.preventDefault();
// searchResult.qualityOverride = profileSelected.id;
// this.setOverride(searchResult);
// this.updateRequest(searchResult);
// }
public reportIssue(catId: IIssueCategory, req: IAlbumRequest) {
this.issueRequest = req;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.foreignAlbumId;
}
public ignore(event: any): void {
event.preventDefault();
}
public clearFilter(el: any) {
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
el = el.parentElement;
el = el.querySelectorAll("INPUT");
for (el of el) {
el.checked = false;
el.parentElement.classList.remove("active");
}
this.filterDisplay = false;
this.filter.availabilityFilter = FilterType.None;
this.filter.statusFilter = FilterType.None;
this.resetSearch();
}
public filterAvailability(filter: FilterType, el: any) {
this.filterActiveStyle(el);
this.filter.availabilityFilter = filter;
this.loadInit();
}
public filterStatus(filter: FilterType, el: any) {
this.filterActiveStyle(el);
this.filter.statusFilter = filter;
this.loadInit();
}
public setOrder(value: OrderType, el: any) {
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
const parent = el.parentElement;
const previousFilter = parent.querySelector(".active");
previousFilter.className = "";
el.className = "active";
this.orderType = value;
this.loadInit();
}
// public subscribe(request: IAlbumRequest) {
// request.subscribed = true;
// this.requestService.subscribeToMovie(request.id)
// .subscribe(x => {
// this.notificationService.success("Subscribed To Movie!");
// });
// }
// public unSubscribe(request: IMovieRequests) {
// request.subscribed = false;
// this.requestService.unSubscribeToMovie(request.id)
// .subscribe(x => {
// this.notificationService.success("Unsubscribed Movie!");
// });
// }
private filterActiveStyle(el: any) {
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
el = el.parentElement; //gets radio div
el = el.parentElement; //gets form group div
el = el.parentElement; //gets status filter div
el = el.querySelectorAll("INPUT");
for (el of el) {
if (el.checked) {
if (!el.parentElement.classList.contains("active")) {
el.parentElement.className += " active";
}
} else {
el.parentElement.classList.remove("active");
}
}
}
private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getAlbumRequests(amountToLoad, currentlyLoaded, this.orderType, this.filter)
.subscribe(x => {
this.setOverrides(x.collection);
if (!this.albumRequests) {
this.albumRequests = [];
}
this.albumRequests = x.collection;
this.totalAlbums = x.total;
this.currentlyLoaded = currentlyLoaded + amountToLoad;
});
}
private approveRequest(request: IAlbumRequest) {
this.requestService.approveAlbum({ id: request.id })
.subscribe(x => {
request.approved = true;
if (x.result) {
this.notificationService.success(
`Request for ${request.title} has been approved successfully`);
} else {
this.notificationService.warning("Request Approved", x.message ? x.message : x.errorMessage);
request.approved = false;
}
});
}
private denyRequest(request: IAlbumRequest) {
this.requestService.denyAlbum({ id: request.id })
.subscribe(x => {
if (x.result) {
this.notificationService.success(
`Request for ${request.title} has been denied successfully`);
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
request.denied = false;
}
});
}
private loadInit() {
this.requestService.getAlbumRequests(this.amountToLoad, 0, this.orderType, this.filter)
.subscribe(x => {
this.albumRequests = x.collection;
this.totalAlbums = x.total;
this.setOverrides(this.albumRequests);
if (this.isAdmin) {
// this.radarrService.getQualityProfilesFromSettings().subscribe(c => {
// this.radarrProfiles = c;
// this.albumRequests.forEach((req) => this.setQualityOverrides(req));
// });
// this.radarrService.getRootFoldersFromSettings().subscribe(c => {
// this.radarrRootFolders = c;
// this.albumRequests.forEach((req) => this.setRootFolderOverrides(req));
// });
}
});
}
private resetSearch() {
this.currentlyLoaded = 5;
this.loadInit();
}
private removeRequestFromUi(key: IAlbumRequest) {
const index = this.albumRequests.indexOf(key, 0);
if (index > -1) {
this.albumRequests.splice(index, 1);
}
}
private setOverrides(requests: IAlbumRequest[]): void {
requests.forEach((req) => {
this.setOverride(req);
});
}
// private setQualityOverrides(req: IMovieRequests): void {
// if (this.radarrProfiles) {
// const profile = this.radarrProfiles.filter((p) => {
// return p.id === req.qualityOverride;
// });
// if (profile.length > 0) {
// req.qualityOverrideTitle = profile[0].name;
// }
// }
// }
// private setRootFolderOverrides(req: IMovieRequests): void {
// if (this.radarrRootFolders) {
// const path = this.radarrRootFolders.filter((folder) => {
// return folder.id === req.rootPathOverride;
// });
// if (path.length > 0) {
// req.rootPathOverrideTitle = path[0].path;
// }
// }
// }
private setOverride(req: IAlbumRequest): void {
this.setAlbumBackground(req);
// this.setQualityOverrides(req);
// this.setRootFolderOverrides(req);
}
private setAlbumBackground(req: IAlbumRequest) {
if (req.disk === null) {
if(req.cover === null) {
req.disk = this.defaultPoster;
} else {
req.disk = req.cover;
}
}
req.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + req.cover + ")");
}
}

@ -9,6 +9,10 @@
<a id="tvTabButton" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectTvTab()"><i class="fa fa-television"></i> {{ 'Requests.TvTab' | translate }}</a>
</li>
<li role="presentation">
<a id="albumTabButton" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectMusicTab()"><i class="fa fa-music"></i> {{ 'Requests.MusicTab' | translate }}</a>
</li>
</ul>
<!-- Tab panes -->
@ -19,5 +23,8 @@
<div [hidden]="!showTv">
<tv-requests [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></tv-requests>
</div>
<div [hidden]="!showAlbums">
<music-requests [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></music-requests>
</div>
</div>

@ -11,6 +11,7 @@ export class RequestComponent implements OnInit {
public showMovie = true;
public showTv = false;
public showAlbums = false;
public issueCategories: IIssueCategory[];
public issuesEnabled = false;
@ -28,10 +29,18 @@ export class RequestComponent implements OnInit {
public selectMovieTab() {
this.showMovie = true;
this.showTv = false;
this.showAlbums = false;
}
public selectTvTab() {
this.showMovie = false;
this.showTv = true;
this.showAlbums = false;
}
public selectMusicTab() {
this.showMovie = false;
this.showTv = false;
this.showAlbums = true;
}
}

@ -8,6 +8,7 @@ import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { ButtonModule, DialogModule, PaginatorModule } from "primeng/primeng";
import { MovieRequestsComponent } from "./movierequests.component";
import { MusicRequestsComponent } from "./music/musicrequests.component";
// Request
import { RequestComponent } from "./request.component";
import { TvRequestChildrenComponent } from "./tvrequest-children.component";
@ -23,7 +24,6 @@ import { SharedModule } from "../shared/shared.module";
const routes: Routes = [
{ path: "", component: RequestComponent, canActivate: [AuthGuard] },
{ path: ":id", component: TvRequestChildrenComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
@ -44,6 +44,7 @@ const routes: Routes = [
MovieRequestsComponent,
TvRequestsComponent,
TvRequestChildrenComponent,
MusicRequestsComponent,
],
exports: [
RouterModule,

@ -3,7 +3,7 @@ import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { ILidarrProfile, ILidarrRootFolder } from "../../interfaces";
import { ILidarrProfile, ILidarrRootFolder, IProfiles } from "../../interfaces";
import { ILidarrSettings } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers";
@ -26,4 +26,11 @@ export class LidarrService extends ServiceHelpers {
public getQualityProfilesFromSettings(): Observable<ILidarrProfile[]> {
return this.http.get<ILidarrProfile[]>(`${this.url}/Profiles/`, {headers: this.headers});
}
public getMetadataProfiles(settings: ILidarrSettings): Observable<IProfiles[]> {
return this.http.post<IProfiles[]>(`${this.url}/Metadata/`, JSON.stringify(settings), {headers: this.headers});
}
public getLanguages(settings: ILidarrSettings): Observable<IProfiles[]> {
return this.http.post<IProfiles[]>(`${this.url}/Langauges/`,JSON.stringify(settings), {headers: this.headers});
}
}

@ -159,8 +159,8 @@ export class RequestService extends ServiceHelpers {
return this.http.post<IRequestEngineResult>(`${this.url}music/unavailable`, JSON.stringify(Album), {headers: this.headers});
}
public getAlbumRequests(count: number, position: number, order: OrderType, filter: IFilter): Observable<IRequestsViewModel<IMovieRequests>> {
return this.http.get<IRequestsViewModel<IMovieRequests>>(`${this.url}music/${count}/${position}/${order}/${filter.statusFilter}/${filter.availabilityFilter}`, {headers: this.headers});
public getAlbumRequests(count: number, position: number, order: OrderType, filter: IFilter): Observable<IRequestsViewModel<IAlbumRequest>> {
return this.http.get<IRequestsViewModel<IAlbumRequest>>(`${this.url}music/${count}/${position}/${order}/${filter.statusFilter}/${filter.availabilityFilter}`, {headers: this.headers});
}
public searchAlbumRequests(search: string): Observable<IAlbumRequest[]> {

@ -50,38 +50,59 @@
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div>
<button (click)="getProfiles(form)" type="button" class="btn btn-primary-outline">Get Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"> </span></button>
</div>
</div>
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom" id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom col-md-5 form-half" id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
<button (click)="getProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"> </span></button>
</div>
<small *ngIf="form.get('defaultQualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
<div class="form-group">
<div>
<button (click)="getRootFolders(form)" type="button" class="btn btn-primary-outline">Get Root Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
</div>
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<div id="rootFolders">
<select formControlName="defaultRootPath" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('defaultRootPath').hasError('required')}">
<select formControlName="defaultRootPath" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('defaultRootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.path}}" >{{folder.path}}</option>
</select>
<button (click)="getRootFolders(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Root Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
<small *ngIf="form.get('defaultRootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
</div>
<div class="form-group">
<label for="languageProfileId" class="control-label">Language Profile</label>
<div id="languageProfileId">
<select formControlName="languageProfileId" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('languageProfileId').hasError('required')}">
<option *ngFor="let folder of languageProfiles" value="{{folder.id}}" >{{folder.name}}</option>
</select>
<button (click)="getLanguageProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Languages <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
<small *ngIf="form.get('languageProfileId').hasError('required')" class="error-text">A Language profile is required</small>
</div>
<div class="form-group">
<label for="metadataProfileId" class="control-label">Metadata Profile</label>
<div id="metadataProfileId">
<select formControlName="metadataProfileId" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('metadataProfileId').hasError('required')}">
<option *ngFor="let folder of metadataProfiles" value="{{folder.id}}" >{{folder.name}}</option>
</select>
<button (click)="getMetadataProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Metadata <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
<small *ngIf="form.get('metadataProfileId').hasError('required')" class="error-text">A Metadata profile is required</small>
</div>
<div class="form-group">
<div>

@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ILidarrSettings, IMinimumAvailability, IRadarrProfile, IRadarrRootFolder } from "../../interfaces";
import { ILidarrSettings, IMinimumAvailability, IProfiles, IRadarrProfile, IRadarrRootFolder } from "../../interfaces";
import { IRadarrSettings } from "../../interfaces";
import { LidarrService, TesterService } from "../../services";
import { NotificationService } from "../../services";
@ -13,10 +13,14 @@ import { SettingsService } from "../../services";
export class LidarrComponent implements OnInit {
public qualities: IRadarrProfile[];
public languageProfiles: IProfiles[];
public metadataProfiles: IProfiles[];
public rootFolders: IRadarrRootFolder[];
public minimumAvailabilityOptions: IMinimumAvailability[];
public profilesRunning: boolean;
public rootFoldersRunning: boolean;
public metadataRunning: boolean;
public languageRunning: boolean;
public advanced = false;
public form: FormGroup;
@ -39,6 +43,9 @@ export class LidarrComponent implements OnInit {
subDir: [x.subDir],
ip: [x.ip, [Validators.required]],
port: [x.port, [Validators.required]],
albumFolder: [x.albumFolder],
languageProfileId: [x.languageProfileId, [Validators.required]],
metadataProfileId: [x.metadataProfileId, [Validators.required]],
});
if (x.defaultQualityProfile) {
@ -47,35 +54,69 @@ export class LidarrComponent implements OnInit {
if (x.defaultRootPath) {
this.getRootFolders(this.form);
}
if (x.languageProfileId) {
this.getLanguageProfiles(this.form);
}
if (x.metadataProfileId) {
this.getMetadataProfiles(this.form);
}
});
this.qualities = [];
this.qualities.push({ name: "Please Select", id: -1 });
this.rootFolders = [];
this.rootFolders.push({ path: "Please Select", id: -1 });
this.languageProfiles = [];
this.languageProfiles.push({ name: "Please Select", id: -1 });
this.metadataProfiles = [];
this.metadataProfiles.push({ name: "Please Select", id: -1 });
}
public getProfiles(form: FormGroup) {
this.profilesRunning = true;
this.lidarrService.getQualityProfiles(form.value).subscribe(x => {
this.qualities = x;
this.qualities.unshift({ name: "Please Select", id: -1 });
this.profilesRunning = false;
this.notificationService.success("Successfully retrieved the Quality Profiles");
});
this.profilesRunning = true;
this.lidarrService.getQualityProfiles(form.value).subscribe(x => {
this.qualities = x;
this.qualities.unshift({ name: "Please Select", id: -1 });
this.profilesRunning = false;
this.notificationService.success("Successfully retrieved the Quality Profiles");
});
}
public getRootFolders(form: FormGroup) {
this.rootFoldersRunning = true;
this.lidarrService.getRootFolders(form.value).subscribe(x => {
this.rootFolders = x;
this.rootFolders.unshift({ path: "Please Select", id: -1 });
this.rootFoldersRunning = false;
this.notificationService.success("Successfully retrieved the Root Folders");
});
this.rootFoldersRunning = true;
this.lidarrService.getRootFolders(form.value).subscribe(x => {
this.rootFolders = x;
this.rootFolders.unshift({ path: "Please Select", id: -1 });
this.rootFoldersRunning = false;
this.notificationService.success("Successfully retrieved the Root Folders");
});
}
public getMetadataProfiles(form: FormGroup) {
this.metadataRunning = true;
this.lidarrService.getMetadataProfiles(form.value).subscribe(x => {
this.metadataProfiles = x;
this.metadataProfiles.unshift({ name: "Please Select", id: -1 });
this.metadataRunning = false;
this.notificationService.success("Successfully retrieved the Metadata profiles");
});
}
public getLanguageProfiles(form: FormGroup) {
this.languageRunning = true;
this.lidarrService.getLanguages(form.value).subscribe(x => {
this.languageProfiles = x;
this.languageProfiles.unshift({ name: "Please Select", id: -1 });
this.languageRunning = false;
this.notificationService.success("Successfully retrieved the Language profiles");
});
}
public test(form: FormGroup) {
@ -93,12 +134,12 @@ export class LidarrComponent implements OnInit {
});
}
public onSubmit(form: FormGroup) {
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
if(form.controls.defaultQualityProfile.value === "-1" || form.controls.defaultRootPath.value === "Please Select") {
if (form.controls.defaultQualityProfile.value === "-1" || form.controls.defaultRootPath.value === "Please Select") {
this.notificationService.error("Please check your entered values");
return;
}

@ -1003,4 +1003,25 @@ a > h4:hover {
white-space: normal;
vertical-align: baseline;
border-radius: .25em;
}
.form-control-grid {
display: block;
width: inherit !important;
height: 39px;
color: #fefefe;
border-radius: 5px;
padding: 8px 16px;
font-size: 15px;
line-height: 1.42857143;
color: #2b3e50;
background-color: #ffffff;
background-image: none;
border: 1px solid transparent;
border-radius: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}

@ -50,6 +50,27 @@ namespace Ombi.Controllers.External
return await _lidarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Lidarr metadata profiles.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("Metadata")]
public async Task<IEnumerable<MetadataProfile>> GetMetadataProfiles([FromBody] LidarrSettings settings)
{
return await _lidarrApi.GetMetadataProfile(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Lidarr Langauge profiles.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("Langauges")]
public async Task<IEnumerable<LanguageProfiles>> GetLanguageProfiles([FromBody] LidarrSettings settings)
{
return await _lidarrApi.GetLanguageProfile(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Lidarr profiles using the saved settings
/// <remarks>The data is cached for an hour</remarks>

@ -119,6 +119,7 @@
"Below you can see yours and all other requests, as well as their download and approval status.",
"MoviesTab": "Movies",
"TvTab": "TV Shows",
"MusicTab":"Music",
"RequestedBy": "Requested By:",
"Status": "Status:",
"RequestStatus": "Request status:",

Loading…
Cancel
Save