New: Use ASP.NET Core instead of Nancy

pull/2397/head
ta264 3 years ago committed by Qstick
parent fe956f340c
commit c247d07e84

@ -78,7 +78,9 @@ export default {
const promise = createAjaxRequest({ const promise = createAjaxRequest({
method: 'PUT', method: 'PUT',
url: '/qualityDefinition/update', url: '/qualityDefinition/update',
data: JSON.stringify(upatedDefinitions) data: JSON.stringify(upatedDefinitions),
contentType: 'application/json',
dataType: 'json'
}).request; }).request;
promise.done((data) => { promise.done((data) => {

@ -88,6 +88,7 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: '/history/failed',
method: 'POST', method: 'POST',
dataType: 'json',
data: { data: {
id: historyId id: historyId
} }
@ -109,4 +110,3 @@ export const reducers = createHandleActions({
} }
}, defaultState, section); }, defaultState, section);

@ -80,6 +80,7 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: '/history/failed',
method: 'POST', method: 'POST',
dataType: 'json',
data: { data: {
id: historyId id: historyId
} }
@ -101,4 +102,3 @@ export const reducers = createHandleActions({
} }
}, defaultState, section); }, defaultState, section);

@ -146,6 +146,7 @@ export const actionHandlers = handleThunks({
url: '/blacklist/bulk', url: '/blacklist/bulk',
method: 'DELETE', method: 'DELETE',
dataType: 'json', dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({ ids }) data: JSON.stringify({ ids })
}).request; }).request;

@ -139,7 +139,8 @@ export function executeCommandHelper( payload, dispatch) {
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/command', url: '/command',
method: 'POST', method: 'POST',
data: JSON.stringify(payload) data: JSON.stringify(payload),
dataType: 'json'
}).request; }).request;
return promise.then((data) => { return promise.then((data) => {

@ -264,6 +264,7 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: '/history/failed',
method: 'POST', method: 'POST',
dataType: 'json',
data: { data: {
id id
} }

@ -396,6 +396,7 @@ export const actionHandlers = handleThunks({
url: `/queue/bulk?removeFromClient=${remove}&blacklist=${blacklist}&skipredownload=${skipredownload}`, url: `/queue/bulk?removeFromClient=${remove}&blacklist=${blacklist}&skipredownload=${skipredownload}`,
method: 'DELETE', method: 'DELETE',
dataType: 'json', dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({ ids }) data: JSON.stringify({ ids })
}).request; }).request;
@ -453,4 +454,3 @@ export const reducers = createHandleActions({
}) })
}, defaultState, section); }, defaultState, section);

@ -271,6 +271,7 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/release', url: '/release',
method: 'POST', method: 'POST',
dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(payload) data: JSON.stringify(payload)
}).request; }).request;

@ -115,6 +115,7 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/artist', url: '/artist',
method: 'POST', method: 'POST',
dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(newArtist) data: JSON.stringify(newArtist)
}).request; }).request;

@ -53,7 +53,8 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/tag', url: '/tag',
method: 'POST', method: 'POST',
data: JSON.stringify(payload.tag) data: JSON.stringify(payload.tag),
dataType: 'json'
}).request; }).request;
promise.done((data) => { promise.done((data) => {

@ -7,18 +7,6 @@ function isRelative(ajaxOptions) {
return !absUrlRegex.test(ajaxOptions.url); return !absUrlRegex.test(ajaxOptions.url);
} }
function moveBodyToQuery(ajaxOptions) {
if (ajaxOptions.data && ajaxOptions.type === 'DELETE') {
if (ajaxOptions.url.contains('?')) {
ajaxOptions.url += '&';
} else {
ajaxOptions.url += '?';
}
ajaxOptions.url += $.param(ajaxOptions.data);
delete ajaxOptions.data;
}
}
function addRootUrl(ajaxOptions) { function addRootUrl(ajaxOptions) {
ajaxOptions.url = apiRoot + ajaxOptions.url; ajaxOptions.url = apiRoot + ajaxOptions.url;
} }
@ -32,7 +20,7 @@ function addContentType(ajaxOptions) {
if ( if (
ajaxOptions.contentType == null && ajaxOptions.contentType == null &&
ajaxOptions.dataType === 'json' && ajaxOptions.dataType === 'json' &&
(ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST')) { (ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST' || ajaxOptions.method === 'DELETE')) {
ajaxOptions.contentType = 'application/json'; ajaxOptions.contentType = 'application/json';
} }
} }
@ -52,7 +40,6 @@ export default function createAjaxRequest(originalAjaxOptions) {
const ajaxOptions = { ...originalAjaxOptions }; const ajaxOptions = { ...originalAjaxOptions };
if (isRelative(ajaxOptions)) { if (isRelative(ajaxOptions)) {
moveBodyToQuery(ajaxOptions);
addRootUrl(ajaxOptions); addRootUrl(ajaxOptions);
addApiKey(ajaxOptions); addApiKey(ajaxOptions);
addContentType(ajaxOptions); addContentType(ajaxOptions);

@ -1,27 +1,25 @@
using System.Linq; using System.Linq;
using Lidarr.Http.Extensions; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
namespace Lidarr.Api.V1.AlbumStudio namespace Lidarr.Api.V1.AlbumStudio
{ {
public class AlbumStudioModule : LidarrV1Module [V1ApiController]
public class AlbumStudioController : Controller
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IAlbumMonitoredService _albumMonitoredService; private readonly IAlbumMonitoredService _albumMonitoredService;
public AlbumStudioModule(IArtistService artistService, IAlbumMonitoredService albumMonitoredService) public AlbumStudioController(IArtistService artistService, IAlbumMonitoredService albumMonitoredService)
: base("/albumstudio")
{ {
_artistService = artistService; _artistService = artistService;
_albumMonitoredService = albumMonitoredService; _albumMonitoredService = albumMonitoredService;
Post("/", artist => UpdateAll());
} }
private object UpdateAll() [HttpPost]
public IActionResult UpdateAll([FromBody] AlbumStudioResource request)
{ {
//Read from request
var request = Request.Body.FromJson<AlbumStudioResource>();
var artistToUpdate = _artistService.GetArtists(request.Artist.Select(s => s.Id)); var artistToUpdate = _artistService.GetArtists(request.Artist.Select(s => s.Id));
foreach (var s in request.Artist) foreach (var s in request.Artist)
@ -41,7 +39,7 @@ namespace Lidarr.Api.V1.AlbumStudio
_albumMonitoredService.SetAlbumMonitoredStatus(artist, request.MonitoringOptions); _albumMonitoredService.SetAlbumMonitoredStatus(artist, request.MonitoringOptions);
} }
return ResponseWithCode("ok", HttpStatusCode.Accepted); return Accepted();
} }
} }
} }

@ -1,9 +1,10 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Nancy; using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
@ -21,7 +22,8 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Albums namespace Lidarr.Api.V1.Albums
{ {
public class AlbumModule : AlbumModuleWithSignalR, [V1ApiController]
public class AlbumController : AlbumControllerWithSignalR,
IHandle<AlbumGrabbedEvent>, IHandle<AlbumGrabbedEvent>,
IHandle<AlbumEditedEvent>, IHandle<AlbumEditedEvent>,
IHandle<AlbumUpdatedEvent>, IHandle<AlbumUpdatedEvent>,
@ -33,7 +35,7 @@ namespace Lidarr.Api.V1.Albums
protected readonly IReleaseService _releaseService; protected readonly IReleaseService _releaseService;
protected readonly IAddAlbumService _addAlbumService; protected readonly IAddAlbumService _addAlbumService;
public AlbumModule(IArtistService artistService, public AlbumController(IArtistService artistService,
IAlbumService albumService, IAlbumService albumService,
IAddAlbumService addAlbumService, IAddAlbumService addAlbumService,
IReleaseService releaseService, IReleaseService releaseService,
@ -50,12 +52,6 @@ namespace Lidarr.Api.V1.Albums
_releaseService = releaseService; _releaseService = releaseService;
_addAlbumService = addAlbumService; _addAlbumService = addAlbumService;
GetResourceAll = GetAlbums;
CreateResource = AddAlbum;
UpdateResource = UpdateAlbum;
DeleteResource = DeleteAlbum;
Put("/monitor", x => SetAlbumsMonitored());
PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty(); PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty();
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator); PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator);
PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator); PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
@ -63,14 +59,13 @@ namespace Lidarr.Api.V1.Albums
PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty(); PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty();
} }
private List<AlbumResource> GetAlbums() [HttpGet]
public List<AlbumResource> GetAlbums([FromQuery]int? artistId,
[FromQuery] List<int> albumIds,
[FromQuery]string foreignAlbumId,
[FromQuery]bool includeAllArtistAlbums = false)
{ {
var artistIdQuery = Request.Query.ArtistId; if (!artistId.HasValue && !albumIds.Any() && foreignAlbumId.IsNullOrWhiteSpace())
var albumIdsQuery = Request.Query.AlbumIds;
var foreignIdQuery = Request.Query.ForeignAlbumId;
var includeAllArtistAlbumsQuery = Request.Query.IncludeAllArtistAlbums;
if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue && !foreignIdQuery.HasValue)
{ {
var albums = _albumService.GetAllAlbums(); var albums = _albumService.GetAllAlbums();
@ -93,17 +88,13 @@ namespace Lidarr.Api.V1.Albums
return MapToResource(albums, false); return MapToResource(albums, false);
} }
if (artistIdQuery.HasValue) if (artistId.HasValue)
{ {
int artistId = Convert.ToInt32(artistIdQuery.Value); return MapToResource(_albumService.GetAlbumsByArtist(artistId.Value), false);
return MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
} }
if (foreignIdQuery.HasValue) if (foreignAlbumId.IsNotNullOrWhiteSpace())
{ {
string foreignAlbumId = foreignIdQuery.Value.ToString();
var album = _albumService.FindById(foreignAlbumId); var album = _albumService.FindById(foreignAlbumId);
if (album == null) if (album == null)
@ -111,7 +102,7 @@ namespace Lidarr.Api.V1.Albums
return MapToResource(new List<Album>(), false); return MapToResource(new List<Album>(), false);
} }
if (includeAllArtistAlbumsQuery.HasValue && Convert.ToBoolean(includeAllArtistAlbumsQuery.Value)) if (includeAllArtistAlbums)
{ {
return MapToResource(_albumService.GetAlbumsByArtist(album.ArtistId), false); return MapToResource(_albumService.GetAlbumsByArtist(album.ArtistId), false);
} }
@ -121,23 +112,19 @@ namespace Lidarr.Api.V1.Albums
} }
} }
string albumIdsValue = albumIdsQuery.Value.ToString();
var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
return MapToResource(_albumService.GetAlbums(albumIds), false); return MapToResource(_albumService.GetAlbums(albumIds), false);
} }
private int AddAlbum(AlbumResource albumResource) [RestPostById]
public ActionResult<AlbumResource> AddAlbum(AlbumResource albumResource)
{ {
var album = _addAlbumService.AddAlbum(albumResource.ToModel()); var album = _addAlbumService.AddAlbum(albumResource.ToModel());
return album.Id; return Created(album.Id);
} }
private void UpdateAlbum(AlbumResource albumResource) [RestPutById]
public ActionResult<AlbumResource> UpdateAlbum(AlbumResource albumResource)
{ {
var album = _albumService.GetAlbum(albumResource.Id); var album = _albumService.GetAlbum(albumResource.Id);
@ -147,9 +134,12 @@ namespace Lidarr.Api.V1.Albums
_releaseService.UpdateMany(model.AlbumReleases.Value); _releaseService.UpdateMany(model.AlbumReleases.Value);
BroadcastResourceChange(ModelAction.Updated, model.Id); BroadcastResourceChange(ModelAction.Updated, model.Id);
return Accepted(model.Id);
} }
private void DeleteAlbum(int id) [RestDeleteById]
public void DeleteAlbum(int id)
{ {
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
@ -157,15 +147,15 @@ namespace Lidarr.Api.V1.Albums
_albumService.DeleteAlbum(id, deleteFiles, addImportListExclusion); _albumService.DeleteAlbum(id, deleteFiles, addImportListExclusion);
} }
private object SetAlbumsMonitored() [HttpPut("monitor")]
public IActionResult SetAlbumsMonitored([FromBody]AlbumsMonitoredResource resource)
{ {
var resource = Request.Body.FromJson<AlbumsMonitoredResource>();
_albumService.SetMonitored(resource.AlbumIds, resource.Monitored); _albumService.SetMonitored(resource.AlbumIds, resource.Monitored);
return ResponseWithCode(MapToResource(_albumService.GetAlbums(resource.AlbumIds), false), HttpStatusCode.Accepted); return Accepted(MapToResource(_albumService.GetAlbums(resource.AlbumIds), false));
} }
[NonAction]
public void Handle(AlbumGrabbedEvent message) public void Handle(AlbumGrabbedEvent message)
{ {
foreach (var album in message.Album.Albums) foreach (var album in message.Album.Albums)
@ -177,31 +167,37 @@ namespace Lidarr.Api.V1.Albums
} }
} }
[NonAction]
public void Handle(AlbumEditedEvent message) public void Handle(AlbumEditedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true)); BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
} }
[NonAction]
public void Handle(AlbumUpdatedEvent message) public void Handle(AlbumUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true)); BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
} }
[NonAction]
public void Handle(AlbumDeletedEvent message) public void Handle(AlbumDeletedEvent message)
{ {
BroadcastResourceChange(ModelAction.Deleted, message.Album.ToResource()); BroadcastResourceChange(ModelAction.Deleted, message.Album.ToResource());
} }
[NonAction]
public void Handle(AlbumImportedEvent message) public void Handle(AlbumImportedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true)); BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
} }
[NonAction]
public void Handle(TrackImportedEvent message) public void Handle(TrackImportedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, message.TrackInfo.Album.ToResource()); BroadcastResourceChange(ModelAction.Updated, message.TrackInfo.Album.ToResource());
} }
[NonAction]
public void Handle(TrackFileDeletedEvent message) public void Handle(TrackFileDeletedEvent message)
{ {
if (message.Reason == DeleteMediaFileReason.Upgrade) if (message.Reason == DeleteMediaFileReason.Upgrade)

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using Lidarr.Http; using Lidarr.Http.REST;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
@ -11,14 +11,14 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Albums namespace Lidarr.Api.V1.Albums
{ {
public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album> public abstract class AlbumControllerWithSignalR : RestControllerWithSignalR<AlbumResource, Album>
{ {
protected readonly IAlbumService _albumService; protected readonly IAlbumService _albumService;
protected readonly IArtistStatisticsService _artistStatisticsService; protected readonly IArtistStatisticsService _artistStatisticsService;
protected readonly IUpgradableSpecification _qualityUpgradableSpecification; protected readonly IUpgradableSpecification _qualityUpgradableSpecification;
protected readonly IMapCoversToLocal _coverMapper; protected readonly IMapCoversToLocal _coverMapper;
protected AlbumModuleWithSignalR(IAlbumService albumService, protected AlbumControllerWithSignalR(IAlbumService albumService,
IArtistStatisticsService artistStatisticsService, IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
IUpgradableSpecification qualityUpgradableSpecification, IUpgradableSpecification qualityUpgradableSpecification,
@ -29,27 +29,9 @@ namespace Lidarr.Api.V1.Albums
_artistStatisticsService = artistStatisticsService; _artistStatisticsService = artistStatisticsService;
_coverMapper = coverMapper; _coverMapper = coverMapper;
_qualityUpgradableSpecification = qualityUpgradableSpecification; _qualityUpgradableSpecification = qualityUpgradableSpecification;
GetResourceById = GetAlbum;
}
protected AlbumModuleWithSignalR(IAlbumService albumService,
IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster,
string resource)
: base(signalRBroadcaster, resource)
{
_albumService = albumService;
_artistStatisticsService = artistStatisticsService;
_coverMapper = coverMapper;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
GetResourceById = GetAlbum;
} }
protected AlbumResource GetAlbum(int id) public override AlbumResource GetResourceById(int id)
{ {
var album = _albumService.GetAlbum(id); var album = _albumService.GetAlbum(id);
var resource = MapToResource(album, true); var resource = MapToResource(album, true);

@ -1,26 +1,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
namespace Lidarr.Api.V1.Albums namespace Lidarr.Api.V1.Albums
{ {
public class AlbumLookupModule : LidarrRestModule<AlbumResource> [V1ApiController("album/lookup")]
public class AlbumLookupController : Controller
{ {
private readonly ISearchForNewAlbum _searchProxy; private readonly ISearchForNewAlbum _searchProxy;
public AlbumLookupModule(ISearchForNewAlbum searchProxy) public AlbumLookupController(ISearchForNewAlbum searchProxy)
: base("/album/lookup")
{ {
_searchProxy = searchProxy; _searchProxy = searchProxy;
Get("/", x => Search());
} }
private object Search() [HttpGet]
public object Search(string term)
{ {
var searchResults = _searchProxy.SearchForNewAlbum((string)Request.Query.term, null); var searchResults = _searchProxy.SearchForNewAlbum(term, null);
return MapToResource(searchResults).ToList(); return MapToResource(searchResults).ToList();
} }

@ -4,6 +4,9 @@ using System.Linq;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
@ -22,7 +25,8 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Artist namespace Lidarr.Api.V1.Artist
{ {
public class ArtistModule : LidarrRestModuleWithSignalR<ArtistResource, NzbDrone.Core.Music.Artist>, [V1ApiController]
public class ArtistController : RestControllerWithSignalR<ArtistResource, NzbDrone.Core.Music.Artist>,
IHandle<AlbumImportedEvent>, IHandle<AlbumImportedEvent>,
IHandle<AlbumEditedEvent>, IHandle<AlbumEditedEvent>,
IHandle<AlbumDeletedEvent>, IHandle<AlbumDeletedEvent>,
@ -41,7 +45,7 @@ namespace Lidarr.Api.V1.Artist
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, public ArtistController(IBroadcastSignalRMessage signalRBroadcaster,
IArtistService artistService, IArtistService artistService,
IAlbumService albumService, IAlbumService albumService,
IAddArtistService addArtistService, IAddArtistService addArtistService,
@ -68,12 +72,6 @@ namespace Lidarr.Api.V1.Artist
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
GetResourceAll = AllArtists;
GetResourceById = GetArtist;
CreateResource = AddArtist;
UpdateResource = UpdateArtist;
DeleteResource = DeleteArtist;
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId));
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId)); Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId));
@ -98,7 +96,7 @@ namespace Lidarr.Api.V1.Artist
PutValidator.RuleFor(s => s.Path).IsValidPath(); PutValidator.RuleFor(s => s.Path).IsValidPath();
} }
private ArtistResource GetArtist(int id) public override ArtistResource GetResourceById(int id)
{ {
var artist = _artistService.GetArtist(id); var artist = _artistService.GetArtist(id);
return GetArtistResource(artist); return GetArtistResource(artist);
@ -122,15 +120,15 @@ namespace Lidarr.Api.V1.Artist
return resource; return resource;
} }
private List<ArtistResource> AllArtists() [HttpGet]
public List<ArtistResource> AllArtists(Guid? mbId)
{ {
var mbId = Request.GetGuidQueryParameter("mbId");
var artistStats = _artistStatisticsService.ArtistStatistics(); var artistStats = _artistStatisticsService.ArtistStatistics();
var artistsResources = new List<ArtistResource>(); var artistsResources = new List<ArtistResource>();
if (mbId != Guid.Empty) if (mbId.HasValue)
{ {
artistsResources.AddIfNotNull(_artistService.FindById(mbId.ToString()).ToResource()); artistsResources.AddIfNotNull(_artistService.FindById(mbId.Value.ToString()).ToResource());
} }
else else
{ {
@ -146,14 +144,16 @@ namespace Lidarr.Api.V1.Artist
return artistsResources; return artistsResources;
} }
private int AddArtist(ArtistResource artistResource) [RestPostById]
public ActionResult<ArtistResource> AddArtist(ArtistResource artistResource)
{ {
var artist = _addArtistService.AddArtist(artistResource.ToModel()); var artist = _addArtistService.AddArtist(artistResource.ToModel());
return artist.Id; return Created(artist.Id);
} }
private void UpdateArtist(ArtistResource artistResource) [RestPutById]
public ActionResult<ArtistResource> UpdateArtist(ArtistResource artistResource)
{ {
var moveFiles = Request.GetBooleanQueryParameter("moveFiles"); var moveFiles = Request.GetBooleanQueryParameter("moveFiles");
var artist = _artistService.GetArtist(artistResource.Id); var artist = _artistService.GetArtist(artistResource.Id);
@ -175,9 +175,12 @@ namespace Lidarr.Api.V1.Artist
_artistService.UpdateArtist(model); _artistService.UpdateArtist(model);
BroadcastResourceChange(ModelAction.Updated, artistResource); BroadcastResourceChange(ModelAction.Updated, artistResource);
return Accepted(artistResource.Id);
} }
private void DeleteArtist(int id) [RestDeleteById]
public void DeleteArtist(int id)
{ {
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
@ -250,21 +253,25 @@ namespace Lidarr.Api.V1.Artist
resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path);
} }
[NonAction]
public void Handle(AlbumImportedEvent message) public void Handle(AlbumImportedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist)); BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
} }
[NonAction]
public void Handle(AlbumEditedEvent message) public void Handle(AlbumEditedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value)); BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value));
} }
[NonAction]
public void Handle(AlbumDeletedEvent message) public void Handle(AlbumDeletedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value)); BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value));
} }
[NonAction]
public void Handle(TrackFileDeletedEvent message) public void Handle(TrackFileDeletedEvent message)
{ {
if (message.Reason == DeleteMediaFileReason.Upgrade) if (message.Reason == DeleteMediaFileReason.Upgrade)
@ -275,16 +282,19 @@ namespace Lidarr.Api.V1.Artist
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.TrackFile.Artist.Value)); BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.TrackFile.Artist.Value));
} }
[NonAction]
public void Handle(ArtistUpdatedEvent message) public void Handle(ArtistUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist)); BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
} }
[NonAction]
public void Handle(ArtistEditedEvent message) public void Handle(ArtistEditedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist)); BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
} }
[NonAction]
public void Handle(ArtistsDeletedEvent message) public void Handle(ArtistsDeletedEvent message)
{ {
foreach (var artist in message.Artists) foreach (var artist in message.Artists)
@ -293,11 +303,13 @@ namespace Lidarr.Api.V1.Artist
} }
} }
[NonAction]
public void Handle(ArtistRenamedEvent message) public void Handle(ArtistRenamedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
} }
[NonAction]
public void Handle(MediaCoversUpdatedEvent message) public void Handle(MediaCoversUpdatedEvent message)
{ {
if (message.Updated) if (message.Updated)

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http.Extensions; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
@ -9,23 +9,21 @@ using NzbDrone.Core.Music.Commands;
namespace Lidarr.Api.V1.Artist namespace Lidarr.Api.V1.Artist
{ {
public class ArtistEditorModule : LidarrV1Module [V1ApiController("artist/editor")]
public class ArtistEditorController : Controller
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
public ArtistEditorModule(IArtistService artistService, IManageCommandQueue commandQueueManager) public ArtistEditorController(IArtistService artistService, IManageCommandQueue commandQueueManager)
: base("/artist/editor")
{ {
_artistService = artistService; _artistService = artistService;
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
Put("/", artist => SaveAll());
Delete("/", artist => DeleteArtist());
} }
private object SaveAll() [HttpPut]
public IActionResult SaveAll([FromBody] ArtistEditorResource resource)
{ {
var resource = Request.Body.FromJson<ArtistEditorResource>();
var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); var artistToUpdate = _artistService.GetArtists(resource.ArtistIds);
var artistToMove = new List<BulkMoveArtist>(); var artistToMove = new List<BulkMoveArtist>();
@ -86,15 +84,12 @@ namespace Lidarr.Api.V1.Artist
}); });
} }
return ResponseWithCode(_artistService.UpdateArtists(artistToUpdate, !resource.MoveFiles) return Accepted(_artistService.UpdateArtists(artistToUpdate, !resource.MoveFiles).ToResource());
.ToResource(),
HttpStatusCode.Accepted);
} }
private object DeleteArtist() [HttpDelete]
public object DeleteArtist([FromBody] ArtistEditorResource resource)
{ {
var resource = Request.Body.FromJson<ArtistEditorResource>();
_artistService.DeleteArtists(resource.ArtistIds, resource.DeleteFiles); _artistService.DeleteArtists(resource.ArtistIds, resource.DeleteFiles);
return new object(); return new object();

@ -1,28 +0,0 @@
using System.Collections.Generic;
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Nancy;
using NzbDrone.Core.Music;
namespace Lidarr.Api.V1.Artist
{
public class ArtistImportModule : LidarrRestModule<ArtistResource>
{
private readonly IAddArtistService _addArtistService;
public ArtistImportModule(IAddArtistService addArtistService)
: base("/artist/import")
{
_addArtistService = addArtistService;
Post("/", x => Import());
}
private object Import()
{
var resource = Request.Body.FromJson<List<ArtistResource>>();
var newArtists = resource.ToModel();
return _addArtistService.AddArtists(newArtists).ToResource();
}
}
}

@ -1,26 +1,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
namespace Lidarr.Api.V1.Artist namespace Lidarr.Api.V1.Artist
{ {
public class ArtistLookupModule : LidarrRestModule<ArtistResource> [V1ApiController("artist/lookup")]
public class ArtistLookupController : Controller
{ {
private readonly ISearchForNewArtist _searchProxy; private readonly ISearchForNewArtist _searchProxy;
public ArtistLookupModule(ISearchForNewArtist searchProxy) public ArtistLookupController(ISearchForNewArtist searchProxy)
: base("/artist/lookup")
{ {
_searchProxy = searchProxy; _searchProxy = searchProxy;
Get("/", x => Search());
} }
private object Search() [HttpGet]
public object Search([FromQuery] string term)
{ {
var searchResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); var searchResults = _searchProxy.SearchForNewArtist(term);
return MapToResource(searchResults).ToList(); return MapToResource(searchResults).ToList();
} }

@ -0,0 +1,43 @@
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Datastore;
namespace Lidarr.Api.V1.Blacklist
{
[V1ApiController]
public class BlacklistController : Controller
{
private readonly IBlacklistService _blacklistService;
public BlacklistController(IBlacklistService blacklistService)
{
_blacklistService = blacklistService;
}
[HttpGet]
public PagingResource<BlacklistResource> GetBlacklist()
{
var pagingResource = Request.ReadPagingResourceFromRequest<BlacklistResource>();
var pagingSpec = pagingResource.MapToPagingSpec<BlacklistResource, NzbDrone.Core.Blacklisting.Blacklist>("date", SortDirection.Descending);
return pagingSpec.ApplyToPage(_blacklistService.Paged, BlacklistResourceMapper.MapToResource);
}
[RestDeleteById]
public void DeleteBlacklist(int id)
{
_blacklistService.Delete(id);
}
[HttpDelete("bulk")]
public object Remove([FromBody] BlacklistBulkResource resource)
{
_blacklistService.Delete(resource.Ids);
return new object();
}
}
}

@ -1,42 +0,0 @@
using Lidarr.Http;
using Lidarr.Http.Extensions;
using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Datastore;
namespace Lidarr.Api.V1.Blacklist
{
public class BlacklistModule : LidarrRestModule<BlacklistResource>
{
private readonly IBlacklistService _blacklistService;
public BlacklistModule(IBlacklistService blacklistService)
{
_blacklistService = blacklistService;
GetResourcePaged = GetBlacklist;
DeleteResource = DeleteBlacklist;
Delete("/bulk", x => Remove());
}
private PagingResource<BlacklistResource> GetBlacklist(PagingResource<BlacklistResource> pagingResource)
{
var pagingSpec = pagingResource.MapToPagingSpec<BlacklistResource, NzbDrone.Core.Blacklisting.Blacklist>("date", SortDirection.Descending);
return ApplyToPage(_blacklistService.Paged, pagingSpec, BlacklistResourceMapper.MapToResource);
}
private void DeleteBlacklist(int id)
{
_blacklistService.Delete(id);
}
private object Remove()
{
var resource = Request.Body.FromJson<BlacklistBulkResource>();
_blacklistService.Delete(resource.Ids);
return new object();
}
}
}

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lidarr.Api.V1.Albums;
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Music;
using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Calendar
{
[V1ApiController]
public class CalendarController : AlbumControllerWithSignalR
{
public CalendarController(IAlbumService albumService,
IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{
}
[HttpGet]
public List<AlbumResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeArtist = false)
{
//TODO: Add Album Image support to AlbumControllerWithSignalR
var includeAlbumImages = Request.GetBooleanQueryParameter("includeAlbumImages");
var startUse = start ?? DateTime.Today;
var endUse = end ?? DateTime.Today.AddDays(2);
var resources = MapToResource(_albumService.AlbumsBetweenDates(startUse, endUse, unmonitored), includeArtist);
return resources.OrderBy(e => e.ReleaseDate).ToList();
}
}
}

@ -5,60 +5,38 @@ using Ical.Net;
using Ical.Net.CalendarComponents; using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes; using Ical.Net.DataTypes;
using Ical.Net.Serialization; using Ical.Net.Serialization;
using Lidarr.Http.Extensions; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using Nancy.Responses;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Tags; using NzbDrone.Core.Tags;
namespace Lidarr.Api.V1.Calendar namespace Lidarr.Api.V1.Calendar
{ {
public class CalendarFeedModule : LidarrV1FeedModule [V1FeedController("calendar")]
public class CalendarFeedController : Controller
{ {
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly ITagService _tagService; private readonly ITagService _tagService;
public CalendarFeedModule(IAlbumService albumService, IArtistService artistService, ITagService tagService) public CalendarFeedController(IAlbumService albumService, IArtistService artistService, ITagService tagService)
: base("calendar")
{ {
_albumService = albumService; _albumService = albumService;
_artistService = artistService; _artistService = artistService;
_tagService = tagService; _tagService = tagService;
Get("/Lidarr.ics", options => GetCalendarFeed());
} }
private object GetCalendarFeed() [HttpGet("Lidarr.ics")]
public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tagList = "", bool unmonitored = false)
{ {
var pastDays = 7;
var futureDays = 28;
var start = DateTime.Today.AddDays(-pastDays); var start = DateTime.Today.AddDays(-pastDays);
var end = DateTime.Today.AddDays(futureDays); var end = DateTime.Today.AddDays(futureDays);
var unmonitored = Request.GetBooleanQueryParameter("unmonitored");
var tags = new List<int>(); var tags = new List<int>();
var queryPastDays = Request.Query.PastDays; if (tagList.IsNotNullOrWhiteSpace())
var queryFutureDays = Request.Query.FutureDays;
var queryTags = Request.Query.Tags;
if (queryPastDays.HasValue)
{
pastDays = int.Parse(queryPastDays.Value);
start = DateTime.Today.AddDays(-pastDays);
}
if (queryFutureDays.HasValue)
{
futureDays = int.Parse(queryFutureDays.Value);
end = DateTime.Today.AddDays(futureDays);
}
if (queryTags.HasValue)
{ {
var tagInput = (string)queryTags.Value.ToString(); tags.AddRange(tagList.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
} }
var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored); var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored);
@ -95,7 +73,7 @@ namespace Lidarr.Api.V1.Calendar
var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext());
var icalendar = serializer.SerializeToString(calendar); var icalendar = serializer.SerializeToString(calendar);
return new TextResponse(icalendar, "text/calendar"); return Content(icalendar, "text/calendar");
} }
} }
} }

@ -1,54 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lidarr.Api.V1.Albums;
using Lidarr.Http.Extensions;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Music;
using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Calendar
{
public class CalendarModule : AlbumModuleWithSignalR
{
public CalendarModule(IAlbumService albumService,
IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "calendar")
{
GetResourceAll = GetCalendar;
}
private List<AlbumResource> GetCalendar()
{
var start = DateTime.Today;
var end = DateTime.Today.AddDays(2);
var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored");
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
//TODO: Add Album Image support to AlbumModuleWithSignalR
var includeAlbumImages = Request.GetBooleanQueryParameter("includeAlbumImages");
var queryStart = Request.Query.Start;
var queryEnd = Request.Query.End;
if (queryStart.HasValue)
{
start = DateTime.Parse(queryStart.Value);
}
if (queryEnd.HasValue)
{
end = DateTime.Parse(queryEnd.Value);
}
var resources = MapToResource(_albumService.AlbumsBetweenDates(start, end, includeUnmonitored), includeArtist);
return resources.OrderBy(e => e.ReleaseDate).ToList();
}
}
}

@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Lidarr.Http.Validation; using Lidarr.Http.Validation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
@ -14,14 +18,15 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Commands namespace Lidarr.Api.V1.Commands
{ {
public class CommandModule : LidarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent> [V1ApiController]
public class CommandController : RestControllerWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
{ {
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly IServiceFactory _serviceFactory; private readonly IServiceFactory _serviceFactory;
private readonly Debouncer _debouncer; private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates; private readonly Dictionary<int, CommandResource> _pendingUpdates;
public CommandModule(IManageCommandQueue commandQueueManager, public CommandController(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster, IBroadcastSignalRMessage signalRBroadcaster,
IServiceFactory serviceFactory) IServiceFactory serviceFactory)
: base(signalRBroadcaster) : base(signalRBroadcaster)
@ -29,50 +34,55 @@ namespace Lidarr.Api.V1.Commands
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
_serviceFactory = serviceFactory; _serviceFactory = serviceFactory;
GetResourceById = GetCommand;
CreateResource = StartCommand;
GetResourceAll = GetStartedCommands;
DeleteResource = CancelCommand;
PostValidator.RuleFor(c => c.Name).NotBlank(); PostValidator.RuleFor(c => c.Name).NotBlank();
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1)); _debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
_pendingUpdates = new Dictionary<int, CommandResource>(); _pendingUpdates = new Dictionary<int, CommandResource>();
} }
private CommandResource GetCommand(int id) public override CommandResource GetResourceById(int id)
{ {
return _commandQueueManager.Get(id).ToResource(); return _commandQueueManager.Get(id).ToResource();
} }
private int StartCommand(CommandResource commandResource) [RestPostById]
public ActionResult<CommandResource> StartCommand(CommandResource commandResource)
{ {
var commandType = var commandType =
_serviceFactory.GetImplementations(typeof(Command)) _serviceFactory.GetImplementations(typeof(Command))
.Single(c => c.Name.Replace("Command", "") .Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
dynamic command = Request.Body.FromJson(commandType); Request.Body.Seek(0, SeekOrigin.Begin);
command.Trigger = CommandTrigger.Manual; using (var reader = new StreamReader(Request.Body))
command.SuppressMessages = !command.SendUpdatesToClient; {
command.SendUpdatesToClient = true; var body = reader.ReadToEnd();
dynamic command = STJson.Deserialize(body, commandType);
command.ClientUserAgent = Request.Headers.UserAgent; command.Trigger = CommandTrigger.Manual;
command.SuppressMessages = !command.SendUpdatesToClient;
command.SendUpdatesToClient = true;
command.ClientUserAgent = Request.Headers["UserAgent"];
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
return trackedCommand.Id; return Created(trackedCommand.Id);
}
} }
private List<CommandResource> GetStartedCommands() [HttpGet]
public List<CommandResource> GetStartedCommands()
{ {
return _commandQueueManager.All().ToResource(); return _commandQueueManager.All().ToResource();
} }
private void CancelCommand(int id) [RestDeleteById]
public void CancelCommand(int id)
{ {
_commandQueueManager.Cancel(id); _commandQueueManager.Cancel(id);
} }
[NonAction]
public void Handle(CommandUpdatedEvent message) public void Handle(CommandUpdatedEvent message)
{ {
if (message.Command.Body.SendUpdatesToClient) if (message.Command.Body.SendUpdatesToClient)

@ -0,0 +1,48 @@
using System.Linq;
using System.Reflection;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Config
{
public abstract class ConfigController<TResource> : RestController<TResource>
where TResource : RestResource, new()
{
private readonly IConfigService _configService;
protected ConfigController(IConfigService configService)
{
_configService = configService;
}
public override TResource GetResourceById(int id)
{
return GetConfig();
}
[HttpGet]
public TResource GetConfig()
{
var resource = ToResource(_configService);
resource.Id = 1;
return resource;
}
[RestPutById]
public ActionResult<TResource> SaveConfig(TResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configService.SaveConfigDictionary(dictionary);
return Accepted(resource.Id);
}
protected abstract TResource ToResource(IConfigService model);
}
}

@ -1,10 +1,12 @@
using Lidarr.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class DownloadClientConfigModule : LidarrConfigModule<DownloadClientConfigResource> [V1ApiController("config/downloadclient")]
public class DownloadClientConfigController : ConfigController<DownloadClientConfigResource>
{ {
public DownloadClientConfigModule(IConfigService configService) public DownloadClientConfigController(IConfigService configService)
: base(configService) : base(configService)
{ {
} }

@ -4,6 +4,9 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -13,26 +16,22 @@ using NzbDrone.Core.Validation.Paths;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class HostConfigModule : LidarrRestModule<HostConfigResource> [V1ApiController("config/host")]
public class HostConfigController : RestController<HostConfigResource>
{ {
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IUserService _userService; private readonly IUserService _userService;
public HostConfigModule(IConfigFileProvider configFileProvider, public HostConfigController(IConfigFileProvider configFileProvider,
IConfigService configService, IConfigService configService,
IUserService userService, IUserService userService,
FileExistsValidator fileExistsValidator) FileExistsValidator fileExistsValidator)
: base("/config/host")
{ {
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_configService = configService; _configService = configService;
_userService = userService; _userService = userService;
GetResourceSingle = GetHostConfig;
GetResourceById = GetHostConfig;
UpdateResource = SaveHostConfig;
SharedValidator.RuleFor(c => c.BindAddress) SharedValidator.RuleFor(c => c.BindAddress)
.ValidIp4Address() .ValidIp4Address()
.NotListenAllIp4Address() .NotListenAllIp4Address()
@ -79,7 +78,13 @@ namespace Lidarr.Api.V1.Config
return cert != null; return cert != null;
} }
private HostConfigResource GetHostConfig() public override HostConfigResource GetResourceById(int id)
{
return GetHostConfig();
}
[HttpGet]
public HostConfigResource GetHostConfig()
{ {
var resource = _configFileProvider.ToResource(_configService); var resource = _configFileProvider.ToResource(_configService);
resource.Id = 1; resource.Id = 1;
@ -94,12 +99,8 @@ namespace Lidarr.Api.V1.Config
return resource; return resource;
} }
private HostConfigResource GetHostConfig(int id) [RestPutById]
{ public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource)
return GetHostConfig();
}
private void SaveHostConfig(HostConfigResource resource)
{ {
var dictionary = resource.GetType() var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public) .GetProperties(BindingFlags.Instance | BindingFlags.Public)
@ -112,6 +113,8 @@ namespace Lidarr.Api.V1.Config
{ {
_userService.Upsert(resource.Username, resource.Password); _userService.Upsert(resource.Username, resource.Password);
} }
return Accepted(resource.Id);
} }
} }
} }

@ -1,12 +1,14 @@
using FluentValidation; using FluentValidation;
using Lidarr.Http;
using Lidarr.Http.Validation; using Lidarr.Http.Validation;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class IndexerConfigModule : LidarrConfigModule<IndexerConfigResource> [V1ApiController("config/indexer")]
public class IndexerConfigController : ConfigController<IndexerConfigResource>
{ {
public IndexerConfigModule(IConfigService configService) public IndexerConfigController(IConfigService configService)
: base(configService) : base(configService)
{ {
SharedValidator.RuleFor(c => c.MinimumAge) SharedValidator.RuleFor(c => c.MinimumAge)

@ -1,53 +0,0 @@
using System.Linq;
using System.Reflection;
using Lidarr.Http;
using Lidarr.Http.REST;
using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Config
{
public abstract class LidarrConfigModule<TResource> : LidarrRestModule<TResource>
where TResource : RestResource, new()
{
private readonly IConfigService _configService;
protected LidarrConfigModule(IConfigService configService)
: this(new TResource().ResourceName.Replace("config", ""), configService)
{
}
protected LidarrConfigModule(string resource, IConfigService configService)
: base("config/" + resource.Trim('/'))
{
_configService = configService;
GetResourceSingle = GetConfig;
GetResourceById = GetConfig;
UpdateResource = SaveConfig;
}
private TResource GetConfig()
{
var resource = ToResource(_configService);
resource.Id = 1;
return resource;
}
protected abstract TResource ToResource(IConfigService model);
private TResource GetConfig(int id)
{
return GetConfig();
}
private void SaveConfig(TResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configService.SaveConfigDictionary(dictionary);
}
}
}

@ -1,4 +1,5 @@
using FluentValidation; using FluentValidation;
using Lidarr.Http;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -6,9 +7,10 @@ using NzbDrone.Core.Validation.Paths;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class MediaManagementConfigModule : LidarrConfigModule<MediaManagementConfigResource> [V1ApiController("config/mediamanagement")]
public class MediaManagementConfigController : ConfigController<MediaManagementConfigResource>
{ {
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator) public MediaManagementConfigController(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
: base(configService) : base(configService)
{ {
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0); SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);

@ -1,13 +1,15 @@
using FluentValidation; using FluentValidation;
using Lidarr.Http;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class MetadataProviderConfigModule : LidarrConfigModule<MetadataProviderConfigResource> [V1ApiController("config/metadataprovider")]
public class MetadataProviderConfigController : ConfigController<MetadataProviderConfigResource>
{ {
public MetadataProviderConfigModule(IConfigService configService) public MetadataProviderConfigController(IConfigService configService)
: base(configService) : base(configService)
{ {
SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace()); SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace());

@ -3,49 +3,44 @@ using System.Linq;
using FluentValidation; using FluentValidation;
using FluentValidation.Results; using FluentValidation.Results;
using Lidarr.Http; using Lidarr.Http;
using Nancy.ModelBinding; using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class NamingConfigModule : LidarrRestModule<NamingConfigResource> [V1ApiController("config/naming")]
public class NamingConfigController : RestController<NamingConfigResource>
{ {
private readonly INamingConfigService _namingConfigService; private readonly INamingConfigService _namingConfigService;
private readonly IFilenameSampleService _filenameSampleService; private readonly IFilenameSampleService _filenameSampleService;
private readonly IFilenameValidationService _filenameValidationService; private readonly IFilenameValidationService _filenameValidationService;
private readonly IBuildFileNames _filenameBuilder; private readonly IBuildFileNames _filenameBuilder;
public NamingConfigModule(INamingConfigService namingConfigService, public NamingConfigController(INamingConfigService namingConfigService,
IFilenameSampleService filenameSampleService, IFilenameSampleService filenameSampleService,
IFilenameValidationService filenameValidationService, IFilenameValidationService filenameValidationService,
IBuildFileNames filenameBuilder) IBuildFileNames filenameBuilder)
: base("config/naming")
{ {
_namingConfigService = namingConfigService; _namingConfigService = namingConfigService;
_filenameSampleService = filenameSampleService; _filenameSampleService = filenameSampleService;
_filenameValidationService = filenameValidationService; _filenameValidationService = filenameValidationService;
_filenameBuilder = filenameBuilder; _filenameBuilder = filenameBuilder;
GetResourceSingle = GetNamingConfig;
GetResourceById = GetNamingConfig;
UpdateResource = UpdateNamingConfig;
Get("/examples", x => GetExamples(this.Bind<NamingConfigResource>()));
SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat();
SharedValidator.RuleFor(c => c.MultiDiscTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.MultiDiscTrackFormat).ValidTrackFormat();
SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat();
} }
private void UpdateNamingConfig(NamingConfigResource resource) public override NamingConfigResource GetResourceById(int id)
{ {
var nameSpec = resource.ToModel(); return GetNamingConfig();
ValidateFormatResult(nameSpec);
_namingConfigService.Save(nameSpec);
} }
private NamingConfigResource GetNamingConfig() [HttpGet]
public NamingConfigResource GetNamingConfig()
{ {
var nameSpec = _namingConfigService.GetConfig(); var nameSpec = _namingConfigService.GetConfig();
var resource = nameSpec.ToResource(); var resource = nameSpec.ToResource();
@ -65,12 +60,19 @@ namespace Lidarr.Api.V1.Config
return resource; return resource;
} }
private NamingConfigResource GetNamingConfig(int id) [RestPutById]
public ActionResult<NamingConfigResource> UpdateNamingConfig(NamingConfigResource resource)
{ {
return GetNamingConfig(); var nameSpec = resource.ToModel();
ValidateFormatResult(nameSpec);
_namingConfigService.Save(nameSpec);
return Accepted(resource.Id);
} }
private object GetExamples(NamingConfigResource config) [HttpGet("examples")]
public object GetExamples([FromQuery]NamingConfigResource config)
{ {
if (config.Id == 0) if (config.Id == 0)
{ {

@ -1,10 +1,12 @@
using NzbDrone.Core.Configuration; using Lidarr.Http;
using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Config namespace Lidarr.Api.V1.Config
{ {
public class UiConfigModule : LidarrConfigModule<UiConfigResource> [V1ApiController("config/ui")]
public class UiConfigController : ConfigController<UiConfigResource>
{ {
public UiConfigModule(IConfigService configService) public UiConfigController(IConfigService configService)
: base(configService) : base(configService)
{ {
} }

@ -0,0 +1,52 @@
using System.Collections.Generic;
using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.CustomFilters;
namespace Lidarr.Api.V1.CustomFilters
{
[V1ApiController]
public class CustomFilterController : RestController<CustomFilterResource>
{
private readonly ICustomFilterService _customFilterService;
public CustomFilterController(ICustomFilterService customFilterService)
{
_customFilterService = customFilterService;
}
public override CustomFilterResource GetResourceById(int id)
{
return _customFilterService.Get(id).ToResource();
}
[HttpGet]
public List<CustomFilterResource> GetCustomFilters()
{
return _customFilterService.All().ToResource();
}
[RestPostById]
public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource)
{
var customFilter = _customFilterService.Add(resource.ToModel());
return Created(customFilter.Id);
}
[RestPutById]
public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource)
{
_customFilterService.Update(resource.ToModel());
return Accepted(resource.Id);
}
[RestDeleteById]
public void DeleteCustomResource(int id)
{
_customFilterService.Delete(id);
}
}
}

@ -1,49 +0,0 @@
using System.Collections.Generic;
using Lidarr.Http;
using NzbDrone.Core.CustomFilters;
namespace Lidarr.Api.V1.CustomFilters
{
public class CustomFilterModule : LidarrRestModule<CustomFilterResource>
{
private readonly ICustomFilterService _customFilterService;
public CustomFilterModule(ICustomFilterService customFilterService)
{
_customFilterService = customFilterService;
GetResourceById = GetCustomFilter;
GetResourceAll = GetCustomFilters;
CreateResource = AddCustomFilter;
UpdateResource = UpdateCustomFilter;
DeleteResource = DeleteCustomResource;
}
private CustomFilterResource GetCustomFilter(int id)
{
return _customFilterService.Get(id).ToResource();
}
private List<CustomFilterResource> GetCustomFilters()
{
return _customFilterService.All().ToResource();
}
private int AddCustomFilter(CustomFilterResource resource)
{
var customFilter = _customFilterService.Add(resource.ToModel());
return customFilter.Id;
}
private void UpdateCustomFilter(CustomFilterResource resource)
{
_customFilterService.Update(resource.ToModel());
}
private void DeleteCustomResource(int id)
{
_customFilterService.Delete(id);
}
}
}

@ -1,20 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using Lidarr.Http; using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.DiskSpace; using NzbDrone.Core.DiskSpace;
namespace Lidarr.Api.V1.DiskSpace namespace Lidarr.Api.V1.DiskSpace
{ {
public class DiskSpaceModule : LidarrRestModule<DiskSpaceResource> [V1ApiController("diskspace")]
public class DiskSpaceController : Controller
{ {
private readonly IDiskSpaceService _diskSpaceService; private readonly IDiskSpaceService _diskSpaceService;
public DiskSpaceModule(IDiskSpaceService diskSpaceService) public DiskSpaceController(IDiskSpaceService diskSpaceService)
: base("diskspace")
{ {
_diskSpaceService = diskSpaceService; _diskSpaceService = diskSpaceService;
GetResourceAll = GetFreeSpace;
} }
[HttpGet]
public List<DiskSpaceResource> GetFreeSpace() public List<DiskSpaceResource> GetFreeSpace()
{ {
return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource);

@ -1,12 +1,14 @@
using NzbDrone.Core.Download; using Lidarr.Http;
using NzbDrone.Core.Download;
namespace Lidarr.Api.V1.DownloadClient namespace Lidarr.Api.V1.DownloadClient
{ {
public class DownloadClientModule : ProviderModuleBase<DownloadClientResource, IDownloadClient, DownloadClientDefinition> [V1ApiController]
public class DownloadClientController : ProviderControllerBase<DownloadClientResource, IDownloadClient, DownloadClientDefinition>
{ {
public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper();
public DownloadClientModule(IDownloadClientFactory downloadClientFactory) public DownloadClientController(IDownloadClientFactory downloadClientFactory)
: base(downloadClientFactory, "downloadclient", ResourceMapper) : base(downloadClientFactory, "downloadclient", ResourceMapper)
{ {
} }

@ -1,44 +1,36 @@
using System.Linq; using System.Linq;
using Lidarr.Http.Extensions; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
namespace Lidarr.Api.V1.FileSystem namespace Lidarr.Api.V1.FileSystem
{ {
public class FileSystemModule : LidarrV1Module [V1ApiController]
public class FileSystemController : Controller
{ {
private readonly IFileSystemLookupService _fileSystemLookupService; private readonly IFileSystemLookupService _fileSystemLookupService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
public FileSystemModule(IFileSystemLookupService fileSystemLookupService, public FileSystemController(IFileSystemLookupService fileSystemLookupService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IDiskScanService diskScanService) IDiskScanService diskScanService)
: base("/filesystem")
{ {
_fileSystemLookupService = fileSystemLookupService; _fileSystemLookupService = fileSystemLookupService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_diskScanService = diskScanService; _diskScanService = diskScanService;
Get("/", x => GetContents());
Get("/type", x => GetEntityType());
Get("/mediafiles", x => GetMediaFiles());
} }
private object GetContents() [HttpGet]
public IActionResult GetContents(string path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false)
{ {
var pathQuery = Request.Query.path; return Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes));
var includeFiles = Request.GetBooleanQueryParameter("includeFiles");
var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes");
return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes);
} }
private object GetEntityType() [HttpGet("type")]
public object GetEntityType(string path)
{ {
var pathQuery = Request.Query.path;
var path = (string)pathQuery.Value;
if (_diskProvider.FileExists(path)) if (_diskProvider.FileExists(path))
{ {
return new { type = "file" }; return new { type = "file" };
@ -48,11 +40,9 @@ namespace Lidarr.Api.V1.FileSystem
return new { type = "folder" }; return new { type = "folder" };
} }
private object GetMediaFiles() [HttpGet("mediafiles")]
public object GetMediaFiles(string path)
{ {
var pathQuery = Request.Query.path;
var path = (string)pathQuery.Value;
if (!_diskProvider.FolderExists(path)) if (!_diskProvider.FolderExists(path))
{ {
return new string[0]; return new string[0];

@ -1,5 +1,8 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.HealthCheck; using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -7,23 +10,30 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Health namespace Lidarr.Api.V1.Health
{ {
public class HealthModule : LidarrRestModuleWithSignalR<HealthResource, HealthCheck>, [V1ApiController]
public class HealthController : RestControllerWithSignalR<HealthResource, HealthCheck>,
IHandle<HealthCheckCompleteEvent> IHandle<HealthCheckCompleteEvent>
{ {
private readonly IHealthCheckService _healthCheckService; private readonly IHealthCheckService _healthCheckService;
public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_healthCheckService = healthCheckService; _healthCheckService = healthCheckService;
GetResourceAll = GetHealth;
} }
private List<HealthResource> GetHealth() public override HealthResource GetResourceById(int id)
{
throw new NotImplementedException();
}
[HttpGet]
public List<HealthResource> GetHealth()
{ {
return _healthCheckService.Results().ToResource(); return _healthCheckService.Results().ToResource();
} }
[NonAction]
public void Handle(HealthCheckCompleteEvent message) public void Handle(HealthCheckCompleteEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

@ -6,8 +6,7 @@ using Lidarr.Api.V1.Artist;
using Lidarr.Api.V1.Tracks; using Lidarr.Api.V1.Tracks;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Lidarr.Http.REST; using Microsoft.AspNetCore.Mvc;
using Nancy;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
@ -15,24 +14,20 @@ using NzbDrone.Core.History;
namespace Lidarr.Api.V1.History namespace Lidarr.Api.V1.History
{ {
public class HistoryModule : LidarrRestModule<HistoryResource> [V1ApiController]
public class HistoryController : Controller
{ {
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IFailedDownloadService _failedDownloadService; private readonly IFailedDownloadService _failedDownloadService;
public HistoryModule(IHistoryService historyService, public HistoryController(IHistoryService historyService,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IFailedDownloadService failedDownloadService) IFailedDownloadService failedDownloadService)
{ {
_historyService = historyService; _historyService = historyService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
_failedDownloadService = failedDownloadService; _failedDownloadService = failedDownloadService;
GetResourcePaged = GetHistory;
Get("/since", x => GetHistorySince());
Get("/artist", x => GetArtistHistory());
Post("/failed", x => MarkAsFailed());
} }
protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeArtist, bool includeAlbum, bool includeTrack) protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeArtist, bool includeAlbum, bool includeTrack)
@ -62,12 +57,11 @@ namespace Lidarr.Api.V1.History
return resource; return resource;
} }
private PagingResource<HistoryResource> GetHistory(PagingResource<HistoryResource> pagingResource) [HttpGet]
public PagingResource<HistoryResource> GetHistory(bool includeArtist = false, bool includeAlbum = false, bool includeTrack = false)
{ {
var pagingResource = Request.ReadPagingResourceFromRequest<HistoryResource>();
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending); var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending);
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
var includeTrack = Request.GetBooleanQueryParameter("includeTrack");
var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType"); var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType");
var albumIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "albumId"); var albumIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "albumId");
@ -91,68 +85,29 @@ namespace Lidarr.Api.V1.History
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId); pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
} }
return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack)); return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
} }
private List<HistoryResource> GetHistorySince() [HttpGet("since")]
public List<HistoryResource> GetHistorySince(DateTime date, HistoryEventType? eventType = null, bool includeArtist = false, bool includeAlbum = false, bool includeTrack = false)
{ {
var queryDate = Request.Query.Date;
var queryEventType = Request.Query.EventType;
if (!queryDate.HasValue)
{
throw new BadRequestException("date is missing");
}
DateTime date = DateTime.Parse(queryDate.Value);
HistoryEventType? eventType = null;
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
var includeTrack = Request.GetBooleanQueryParameter("includeTrack");
if (queryEventType.HasValue)
{
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
}
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
} }
private List<HistoryResource> GetArtistHistory() [HttpGet("artist")]
public List<HistoryResource> GetArtistHistory(int artistId, int? albumId = null, HistoryEventType? eventType = null, bool includeArtist = false, bool includeAlbum = false, bool includeTrack = false)
{ {
var queryArtistId = Request.Query.ArtistId; if (albumId.HasValue)
var queryAlbumId = Request.Query.AlbumId;
var queryEventType = Request.Query.EventType;
if (!queryArtistId.HasValue)
{
throw new BadRequestException("artistId is missing");
}
int artistId = Convert.ToInt32(queryArtistId.Value);
HistoryEventType? eventType = null;
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
var includeTrack = Request.GetBooleanQueryParameter("includeTrack");
if (queryEventType.HasValue)
{ {
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); return _historyService.GetByAlbum(albumId.Value, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
}
if (queryAlbumId.HasValue)
{
int albumId = Convert.ToInt32(queryAlbumId.Value);
return _historyService.GetByAlbum(albumId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
} }
return _historyService.GetByArtist(artistId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); return _historyService.GetByArtist(artistId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
} }
private object MarkAsFailed() [HttpPost("failed")]
public object MarkAsFailed([FromBody] int id)
{ {
var id = (int)Request.Form.Id;
_failedDownloadService.MarkAsFailed(id); _failedDownloadService.MarkAsFailed(id);
return new object(); return new object();
} }

@ -1,14 +1,16 @@
using Lidarr.Http;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
namespace Lidarr.Api.V1.ImportLists namespace Lidarr.Api.V1.ImportLists
{ {
public class ImportListModule : ProviderModuleBase<ImportListResource, IImportList, ImportListDefinition> [V1ApiController]
public class ImportListController : ProviderControllerBase<ImportListResource, IImportList, ImportListDefinition>
{ {
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper(); public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
public ImportListModule(ImportListFactory importListFactory, public ImportListController(ImportListFactory importListFactory,
QualityProfileExistsValidator qualityProfileExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator) MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(importListFactory, "importlist", ResourceMapper) : base(importListFactory, "importlist", ResourceMapper)

@ -1,54 +1,57 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace Lidarr.Api.V1.ImportLists namespace Lidarr.Api.V1.ImportLists
{ {
public class ImportListExclusionModule : LidarrRestModule<ImportListExclusionResource> [V1ApiController]
public class ImportListExclusionController : RestController<ImportListExclusionResource>
{ {
private readonly IImportListExclusionService _importListExclusionService; private readonly IImportListExclusionService _importListExclusionService;
public ImportListExclusionModule(IImportListExclusionService importListExclusionService, public ImportListExclusionController(IImportListExclusionService importListExclusionService,
ImportListExclusionExistsValidator importListExclusionExistsValidator, ImportListExclusionExistsValidator importListExclusionExistsValidator,
GuidValidator guidValidator) GuidValidator guidValidator)
{ {
_importListExclusionService = importListExclusionService; _importListExclusionService = importListExclusionService;
GetResourceById = GetImportListExclusion;
GetResourceAll = GetImportListExclusions;
CreateResource = AddImportListExclusion;
UpdateResource = UpdateImportListExclusion;
DeleteResource = DeleteImportListExclusionResource;
SharedValidator.RuleFor(c => c.ForeignId).NotEmpty().SetValidator(guidValidator).SetValidator(importListExclusionExistsValidator); SharedValidator.RuleFor(c => c.ForeignId).NotEmpty().SetValidator(guidValidator).SetValidator(importListExclusionExistsValidator);
SharedValidator.RuleFor(c => c.ArtistName).NotEmpty(); SharedValidator.RuleFor(c => c.ArtistName).NotEmpty();
} }
private ImportListExclusionResource GetImportListExclusion(int id) public override ImportListExclusionResource GetResourceById(int id)
{ {
return _importListExclusionService.Get(id).ToResource(); return _importListExclusionService.Get(id).ToResource();
} }
private List<ImportListExclusionResource> GetImportListExclusions() [HttpGet]
public List<ImportListExclusionResource> GetImportListExclusions()
{ {
return _importListExclusionService.All().ToResource(); return _importListExclusionService.All().ToResource();
} }
private int AddImportListExclusion(ImportListExclusionResource resource) [RestPostById]
public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportListExclusionResource resource)
{ {
var customFilter = _importListExclusionService.Add(resource.ToModel()); var customFilter = _importListExclusionService.Add(resource.ToModel());
return customFilter.Id; return Created(customFilter.Id);
} }
private void UpdateImportListExclusion(ImportListExclusionResource resource) [RestPutById]
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion(ImportListExclusionResource resource)
{ {
_importListExclusionService.Update(resource.ToModel()); _importListExclusionService.Update(resource.ToModel());
return Accepted(resource.Id);
} }
private void DeleteImportListExclusionResource(int id) [RestDeleteById]
public void DeleteImportListExclusionResource(int id)
{ {
_importListExclusionService.Delete(id); _importListExclusionService.Delete(id);
} }

@ -1,12 +1,14 @@
using NzbDrone.Core.Indexers; using Lidarr.Http;
using NzbDrone.Core.Indexers;
namespace Lidarr.Api.V1.Indexers namespace Lidarr.Api.V1.Indexers
{ {
public class IndexerModule : ProviderModuleBase<IndexerResource, IIndexer, IndexerDefinition> [V1ApiController]
public class IndexerController : ProviderControllerBase<IndexerResource, IIndexer, IndexerDefinition>
{ {
public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper();
public IndexerModule(IndexerFactory indexerFactory) public IndexerController(IndexerFactory indexerFactory)
: base(indexerFactory, "indexer", ResourceMapper) : base(indexerFactory, "indexer", ResourceMapper)
{ {
} }

@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Nancy; using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -18,7 +19,8 @@ using HttpStatusCode = System.Net.HttpStatusCode;
namespace Lidarr.Api.V1.Indexers namespace Lidarr.Api.V1.Indexers
{ {
public class ReleaseModule : ReleaseModuleBase [V1ApiController]
public class ReleaseController : ReleaseControllerBase
{ {
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
@ -32,7 +34,7 @@ namespace Lidarr.Api.V1.Indexers
private readonly ICached<RemoteAlbum> _remoteAlbumCache; private readonly ICached<RemoteAlbum> _remoteAlbumCache;
public ReleaseModule(IAlbumService albumService, public ReleaseController(IAlbumService albumService,
IArtistService artistService, IArtistService artistService,
IFetchAndParseRss rssFetcherAndParser, IFetchAndParseRss rssFetcherAndParser,
ISearchForNzb nzbSearchService, ISearchForNzb nzbSearchService,
@ -53,17 +55,17 @@ namespace Lidarr.Api.V1.Indexers
_downloadService = downloadService; _downloadService = downloadService;
_logger = logger; _logger = logger;
GetResourceAll = GetReleases;
Post("/", x => DownloadRelease(ReadResourceFromRequest()));
PostValidator.RuleFor(s => s.IndexerId).ValidId(); PostValidator.RuleFor(s => s.IndexerId).ValidId();
PostValidator.RuleFor(s => s.Guid).NotEmpty(); PostValidator.RuleFor(s => s.Guid).NotEmpty();
_remoteAlbumCache = cacheManager.GetCache<RemoteAlbum>(GetType(), "remoteAlbums"); _remoteAlbumCache = cacheManager.GetCache<RemoteAlbum>(GetType(), "remoteAlbums");
} }
private object DownloadRelease(ReleaseResource release) [HttpPost]
public ActionResult<ReleaseResource> Create(ReleaseResource release)
{ {
ValidateResource(release);
var remoteAlbum = _remoteAlbumCache.Find(GetCacheKey(release)); var remoteAlbum = _remoteAlbumCache.Find(GetCacheKey(release));
if (remoteAlbum == null) if (remoteAlbum == null)
@ -129,19 +131,20 @@ namespace Lidarr.Api.V1.Indexers
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed");
} }
return release; return Ok(release);
} }
private List<ReleaseResource> GetReleases() [HttpGet]
public List<ReleaseResource> GetReleases(int? albumId, int? artistId)
{ {
if (Request.Query.albumId.HasValue) if (albumId.HasValue)
{ {
return GetAlbumReleases(Request.Query.albumId); return GetAlbumReleases(int.Parse(Request.Query["albumId"]));
} }
if (Request.Query.artistId.HasValue) if (artistId.HasValue)
{ {
return GetArtistReleases(Request.Query.artistId); return GetArtistReleases(int.Parse(Request.Query["artistId"]));
} }
return GetRss(); return GetRss();

@ -1,11 +1,17 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Lidarr.Http; using Lidarr.Http.REST;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
namespace Lidarr.Api.V1.Indexers namespace Lidarr.Api.V1.Indexers
{ {
public abstract class ReleaseModuleBase : LidarrRestModule<ReleaseResource> public abstract class ReleaseControllerBase : RestController<ReleaseResource>
{ {
public override ReleaseResource GetResourceById(int id)
{
throw new NotImplementedException();
}
protected virtual List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions) protected virtual List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions)
{ {
var result = new List<ReleaseResource>(); var result = new List<ReleaseResource>();

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using FluentValidation.Results; using FluentValidation.Results;
using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
@ -12,14 +14,15 @@ using NzbDrone.Core.Parser.Model;
namespace Lidarr.Api.V1.Indexers namespace Lidarr.Api.V1.Indexers
{ {
internal class ReleasePushModule : ReleaseModuleBase [V1ApiController("release/push")]
public class ReleasePushController : ReleaseControllerBase
{ {
private readonly IMakeDownloadDecision _downloadDecisionMaker; private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IProcessDownloadDecisions _downloadDecisionProcessor; private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
private readonly IIndexerFactory _indexerFactory; private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger; private readonly Logger _logger;
public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker, public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker,
IProcessDownloadDecisions downloadDecisionProcessor, IProcessDownloadDecisions downloadDecisionProcessor,
IIndexerFactory indexerFactory, IIndexerFactory indexerFactory,
Logger logger) Logger logger)
@ -29,18 +32,19 @@ namespace Lidarr.Api.V1.Indexers
_indexerFactory = indexerFactory; _indexerFactory = indexerFactory;
_logger = logger; _logger = logger;
Post("/push", x => ProcessRelease(ReadResourceFromRequest()));
PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty();
PostValidator.RuleFor(s => s.Protocol).NotEmpty(); PostValidator.RuleFor(s => s.Protocol).NotEmpty();
PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); PostValidator.RuleFor(s => s.PublishDate).NotEmpty();
} }
private object ProcessRelease(ReleaseResource release) [HttpPost]
public ActionResult<ReleaseResource> Create(ReleaseResource release)
{ {
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl); _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
ValidateResource(release);
var info = release.ToModel(); var info = release.ToModel();
info.Guid = "PUSH-" + info.DownloadUrl; info.Guid = "PUSH-" + info.DownloadUrl;

@ -9,11 +9,9 @@
<ProjectReference Include="..\NzbDrone.SignalR\Lidarr.SignalR.csproj" /> <ProjectReference Include="..\NzbDrone.SignalR\Lidarr.SignalR.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageReference Include="FluentValidation" Version="8.6.2" /> <PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Ical.Net" Version="4.2.0" /> <PackageReference Include="Ical.Net" Version="4.2.0" />
<PackageReference Include="Nancy" Version="2.0.0" />
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
<PackageReference Include="NLog" Version="4.7.9" /> <PackageReference Include="NLog" Version="4.7.9" />
<PackageReference Include="System.IO.Abstractions" Version="13.2.29" /> <PackageReference Include="System.IO.Abstractions" Version="13.2.29" />
</ItemGroup> </ItemGroup>

@ -1,12 +0,0 @@
using Lidarr.Http;
namespace Lidarr.Api.V1
{
public abstract class LidarrV1FeedModule : LidarrModule
{
protected LidarrV1FeedModule(string resource)
: base("/feed/v1/" + resource.Trim('/'))
{
}
}
}

@ -1,12 +0,0 @@
using Lidarr.Http;
namespace Lidarr.Api.V1
{
public abstract class LidarrV1Module : LidarrModule
{
protected LidarrV1Module(string resource)
: base("/api/v1/" + resource.Trim('/'))
{
}
}
}

@ -1,21 +1,25 @@
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Instrumentation;
namespace Lidarr.Api.V1.Logs namespace Lidarr.Api.V1.Logs
{ {
public class LogModule : LidarrRestModule<LogResource> [V1ApiController]
public class LogController : Controller
{ {
private readonly ILogService _logService; private readonly ILogService _logService;
public LogModule(ILogService logService) public LogController(ILogService logService)
{ {
_logService = logService; _logService = logService;
GetResourcePaged = GetLogs;
} }
private PagingResource<LogResource> GetLogs(PagingResource<LogResource> pagingResource) [HttpGet]
public PagingResource<LogResource> GetLogs()
{ {
var pagingResource = Request.ReadPagingResourceFromRequest<LogResource>();
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(); var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>();
if (pageSpec.SortKey == "time") if (pageSpec.SortKey == "time")
@ -50,7 +54,7 @@ namespace Lidarr.Api.V1.Logs
} }
} }
var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); var response = pageSpec.ApplyToPage(_logService.Paged, LogResourceMapper.ToResource);
if (pageSpec.SortKey == "id") if (pageSpec.SortKey == "id")
{ {

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Lidarr.Http;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -7,12 +8,13 @@ using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Logs namespace Lidarr.Api.V1.Logs
{ {
public class LogFileModule : LogFileModuleBase [V1ApiController("log/file")]
public class LogFileController : LogFileControllerBase
{ {
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
public LogFileModule(IAppFolderInfo appFolderInfo, public LogFileController(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IConfigFileProvider configFileProvider) IConfigFileProvider configFileProvider)
: base(diskProvider, configFileProvider, "") : base(diskProvider, configFileProvider, "")

@ -1,35 +1,32 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Lidarr.Http; using Microsoft.AspNetCore.Mvc;
using Nancy;
using Nancy.Responses;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Logs namespace Lidarr.Api.V1.Logs
{ {
public abstract class LogFileModuleBase : LidarrRestModule<LogFileResource> public abstract class LogFileControllerBase : Controller
{ {
protected const string LOGFILE_ROUTE = @"/(?<filename>[-.a-zA-Z0-9]+?\.txt)"; protected const string LOGFILE_ROUTE = @"/(?<filename>[-.a-zA-Z0-9]+?\.txt)";
protected string _resource;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
public LogFileModuleBase(IDiskProvider diskProvider, public LogFileControllerBase(IDiskProvider diskProvider,
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
string route) string resource)
: base("log/file" + route)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
GetResourceAll = GetLogFilesResponse; _resource = resource;
Get(LOGFILE_ROUTE, options => GetLogFileResponse(options.filename));
} }
private List<LogFileResource> GetLogFilesResponse() [HttpGet]
public List<LogFileResource> GetLogFilesResponse()
{ {
var result = new List<LogFileResource>(); var result = new List<LogFileResource>();
@ -45,7 +42,7 @@ namespace Lidarr.Api.V1.Logs
Id = i + 1, Id = i + 1,
Filename = filename, Filename = filename,
LastWriteTime = _diskProvider.FileGetLastWrite(file), LastWriteTime = _diskProvider.FileGetLastWrite(file),
ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, _resource, filename),
DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename) DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename)
}); });
} }
@ -53,7 +50,8 @@ namespace Lidarr.Api.V1.Logs
return result.OrderByDescending(l => l.LastWriteTime).ToList(); return result.OrderByDescending(l => l.LastWriteTime).ToList();
} }
private object GetLogFileResponse(string filename) [HttpGet(@"{filename:regex([[-.a-zA-Z0-9]]+?\.txt)}")]
public IActionResult GetLogFileResponse(string filename)
{ {
LogManager.Flush(); LogManager.Flush();
@ -61,12 +59,10 @@ namespace Lidarr.Api.V1.Logs
if (!_diskProvider.FileExists(filePath)) if (!_diskProvider.FileExists(filePath))
{ {
return new NotFoundResponse(); return NotFound();
} }
var data = _diskProvider.ReadAllText(filePath); return PhysicalFile(filePath, "text/plain");
return new TextResponse(data);
} }
protected abstract IEnumerable<string> GetLogFiles(); protected abstract IEnumerable<string> GetLogFiles();

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Lidarr.Http;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -9,15 +10,16 @@ using NzbDrone.Core.Configuration;
namespace Lidarr.Api.V1.Logs namespace Lidarr.Api.V1.Logs
{ {
public class UpdateLogFileModule : LogFileModuleBase [V1ApiController("log/file/update")]
public class UpdateLogFileController : LogFileControllerBase
{ {
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
public UpdateLogFileModule(IAppFolderInfo appFolderInfo, public UpdateLogFileController(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IConfigFileProvider configFileProvider) IConfigFileProvider configFileProvider)
: base(diskProvider, configFileProvider, "/update") : base(diskProvider, configFileProvider, "update")
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_diskProvider = diskProvider; _diskProvider = diskProvider;

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Microsoft.AspNetCore.Mvc;
using Nancy;
using NLog; using NLog;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.TrackImport.Manual; using NzbDrone.Core.MediaFiles.TrackImport.Manual;
@ -12,7 +10,8 @@ using NzbDrone.Core.Qualities;
namespace Lidarr.Api.V1.ManualImport namespace Lidarr.Api.V1.ManualImport
{ {
public class ManualImportModule : LidarrRestModule<ManualImportResource> [V1ApiController]
public class ManualImportController : Controller
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
@ -20,7 +19,7 @@ namespace Lidarr.Api.V1.ManualImport
private readonly IManualImportService _manualImportService; private readonly IManualImportService _manualImportService;
private readonly Logger _logger; private readonly Logger _logger;
public ManualImportModule(IManualImportService manualImportService, public ManualImportController(IManualImportService manualImportService,
IArtistService artistService, IArtistService artistService,
IAlbumService albumService, IAlbumService albumService,
IReleaseService releaseService, IReleaseService releaseService,
@ -31,31 +30,25 @@ namespace Lidarr.Api.V1.ManualImport
_releaseService = releaseService; _releaseService = releaseService;
_manualImportService = manualImportService; _manualImportService = manualImportService;
_logger = logger; _logger = logger;
}
GetResourceAll = GetMediaFiles; [HttpPut]
public IActionResult UpdateItems(List<ManualImportResource> resource)
Put("/", options => {
{ return Accepted(UpdateImportItems(resource));
var resource = Request.Body.FromJson<List<ManualImportResource>>();
return ResponseWithCode(UpdateImportItems(resource), HttpStatusCode.Accepted);
});
} }
private List<ManualImportResource> GetMediaFiles() [HttpGet]
public List<ManualImportResource> GetMediaFiles(string folder, string downloadId, int? artistId, bool filterExistingFiles = true, bool replaceExistingFiles = true)
{ {
var folder = (string)Request.Query.folder;
var downloadId = (string)Request.Query.downloadId;
NzbDrone.Core.Music.Artist artist = null; NzbDrone.Core.Music.Artist artist = null;
var artistIdQuery = Request.GetNullableIntegerQueryParameter("artistId", null); if (artistId > 0)
if (artistIdQuery.HasValue && artistIdQuery.Value > 0)
{ {
artist = _artistService.GetArtist(Convert.ToInt32(artistIdQuery.Value)); artist = _artistService.GetArtist(artistId.Value);
} }
var filter = Request.GetBooleanQueryParameter("filterExistingFiles", true) ? FilterFilesType.Matched : FilterFilesType.None; var filter = filterExistingFiles ? FilterFilesType.Matched : FilterFilesType.None;
var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true);
return _manualImportService.GetMediaFiles(folder, downloadId, artist, filter, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList(); return _manualImportService.GetMediaFiles(folder, downloadId, artist, filter, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList();
} }

@ -1,34 +1,32 @@
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Nancy; using Lidarr.Http;
using Nancy.Responses; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace Lidarr.Api.V1.MediaCovers namespace Lidarr.Api.V1.MediaCovers
{ {
public class MediaCoverModule : LidarrV1Module [V1ApiController]
public class MediaCoverController : Controller
{ {
private const string MEDIA_COVER_ARTIST_ROUTE = @"/Artist/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
private const string MEDIA_COVER_ALBUM_ROUTE = @"/Album/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
private static readonly Regex RegexResizedImage = new Regex(@"-\d+(?=\.(jpg|png|gif)$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex RegexResizedImage = new Regex(@"-\d+(?=\.(jpg|png|gif)$)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IContentTypeProvider _mimeTypeProvider;
public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) public MediaCoverController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
: base("MediaCover")
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_mimeTypeProvider = new FileExtensionContentTypeProvider();
Get(MEDIA_COVER_ARTIST_ROUTE, options => GetArtistMediaCover(options.artistId, options.filename));
Get(MEDIA_COVER_ALBUM_ROUTE, options => GetAlbumMediaCover(options.artistId, options.filename));
} }
private object GetArtistMediaCover(int artistId, string filename) [HttpGet(@"artist/{artistId:int}/{filename:regex((.+)\.(jpg|png|gif))}")]
public IActionResult GetArtistMediaCover(int artistId, string filename)
{ {
var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", artistId.ToString(), filename); var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", artistId.ToString(), filename);
@ -39,16 +37,17 @@ namespace Lidarr.Api.V1.MediaCovers
var basefilePath = RegexResizedImage.Replace(filePath, ""); var basefilePath = RegexResizedImage.Replace(filePath, "");
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
{ {
return new NotFoundResponse(); return NotFound();
} }
filePath = basefilePath; filePath = basefilePath;
} }
return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); return PhysicalFile(filePath, GetContentType(filePath));
} }
private object GetAlbumMediaCover(int albumId, string filename) [HttpGet(@"album/{albumId:int}/{filename:regex((.+)\.(jpg|png|gif))}")]
public IActionResult GetAlbumMediaCover(int albumId, string filename)
{ {
var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Albums", albumId.ToString(), filename); var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Albums", albumId.ToString(), filename);
@ -59,13 +58,23 @@ namespace Lidarr.Api.V1.MediaCovers
var basefilePath = RegexResizedImage.Replace(filePath, ""); var basefilePath = RegexResizedImage.Replace(filePath, "");
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
{ {
return new NotFoundResponse(); return NotFound();
} }
filePath = basefilePath; filePath = basefilePath;
} }
return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); return PhysicalFile(filePath, GetContentType(filePath));
}
private string GetContentType(string filePath)
{
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
{
contentType = "application/octet-stream";
}
return contentType;
} }
} }
} }

@ -1,12 +1,14 @@
using NzbDrone.Core.Extras.Metadata; using Lidarr.Http;
using NzbDrone.Core.Extras.Metadata;
namespace Lidarr.Api.V1.Metadata namespace Lidarr.Api.V1.Metadata
{ {
public class MetadataModule : ProviderModuleBase<MetadataResource, IMetadata, MetadataDefinition> [V1ApiController]
public class MetadataController : ProviderControllerBase<MetadataResource, IMetadata, MetadataDefinition>
{ {
public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper();
public MetadataModule(IMetadataFactory metadataFactory) public MetadataController(IMetadataFactory metadataFactory)
: base(metadataFactory, "metadata", ResourceMapper) : base(metadataFactory, "metadata", ResourceMapper)
{ {
} }

@ -1,12 +1,14 @@
using NzbDrone.Core.Notifications; using Lidarr.Http;
using NzbDrone.Core.Notifications;
namespace Lidarr.Api.V1.Notifications namespace Lidarr.Api.V1.Notifications
{ {
public class NotificationModule : ProviderModuleBase<NotificationResource, INotification, NotificationDefinition> [V1ApiController]
public class NotificationController : ProviderControllerBase<NotificationResource, INotification, NotificationDefinition>
{ {
public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
public NotificationModule(NotificationFactory notificationFactory) public NotificationController(NotificationFactory notificationFactory)
: base(notificationFactory, "notification", ResourceMapper) : base(notificationFactory, "notification", ResourceMapper)
{ {
} }

@ -1,24 +1,24 @@
using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Albums;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using Lidarr.Http; using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
namespace Lidarr.Api.V1.Parse namespace Lidarr.Api.V1.Parse
{ {
public class ParseModule : LidarrRestModule<ParseResource> [V1ApiController]
public class ParseController : Controller
{ {
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
public ParseModule(IParsingService parsingService) public ParseController(IParsingService parsingService)
{ {
_parsingService = parsingService; _parsingService = parsingService;
GetResourceSingle = Parse;
} }
private ParseResource Parse() [HttpGet]
public ParseResource Parse(string title)
{ {
var title = Request.Query.Title.Value as string;
var parsedAlbumInfo = Parser.ParseAlbumTitle(title); var parsedAlbumInfo = Parser.ParseAlbumTitle(title);
if (parsedAlbumInfo == null) if (parsedAlbumInfo == null)

@ -1,29 +1,23 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Lidarr.Http.Validation; using Lidarr.Http.Validation;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Profiles.Delay;
namespace Lidarr.Api.V1.Profiles.Delay namespace Lidarr.Api.V1.Profiles.Delay
{ {
public class DelayProfileModule : LidarrRestModule<DelayProfileResource> [V1ApiController]
public class DelayProfileController : RestController<DelayProfileResource>
{ {
private readonly IDelayProfileService _delayProfileService; private readonly IDelayProfileService _delayProfileService;
public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) public DelayProfileController(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator)
{ {
_delayProfileService = delayProfileService; _delayProfileService = delayProfileService;
GetResourceAll = GetAll;
GetResourceById = GetById;
UpdateResource = Update;
CreateResource = Create;
DeleteResource = DeleteProfile;
Put(@"/reorder/(?<id>[\d]{1,10})", options => Reorder(options.Id));
SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1);
SharedValidator.RuleFor(d => d.Tags).EmptyCollection<DelayProfileResource, int>().When(d => d.Id == 1); SharedValidator.RuleFor(d => d.Tags).EmptyCollection<DelayProfileResource, int>().When(d => d.Id == 1);
SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator); SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator);
@ -39,15 +33,17 @@ namespace Lidarr.Api.V1.Profiles.Delay
}); });
} }
private int Create(DelayProfileResource resource) [RestPostById]
public ActionResult<DelayProfileResource> Create(DelayProfileResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
model = _delayProfileService.Add(model); model = _delayProfileService.Add(model);
return model.Id; return Created(model.Id);
} }
private void DeleteProfile(int id) [RestDeleteById]
public void DeleteProfile(int id)
{ {
if (id == 1) if (id == 1)
{ {
@ -57,29 +53,30 @@ namespace Lidarr.Api.V1.Profiles.Delay
_delayProfileService.Delete(id); _delayProfileService.Delete(id);
} }
private void Update(DelayProfileResource resource) [RestPutById]
public ActionResult<DelayProfileResource> Update(DelayProfileResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
_delayProfileService.Update(model); _delayProfileService.Update(model);
return Accepted(model.Id);
} }
private DelayProfileResource GetById(int id) public override DelayProfileResource GetResourceById(int id)
{ {
return _delayProfileService.Get(id).ToResource(); return _delayProfileService.Get(id).ToResource();
} }
private List<DelayProfileResource> GetAll() [HttpGet]
public List<DelayProfileResource> GetAll()
{ {
return _delayProfileService.All().ToResource(); return _delayProfileService.All().ToResource();
} }
private object Reorder(int id) [HttpPut("reorder/{id:int}")]
public object Reorder(int id, [FromQuery] int? afterId = null)
{ {
ValidateId(id); ValidateId(id);
var afterIdQuery = Request.Query.After;
int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null;
return _delayProfileService.Reorder(id, afterId).ToResource(); return _delayProfileService.Reorder(id, afterId).ToResource();
} }
} }

@ -1,54 +1,59 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Metadata;
namespace Lidarr.Api.V1.Profiles.Metadata namespace Lidarr.Api.V1.Profiles.Metadata
{ {
public class MetadataProfileModule : LidarrRestModule<MetadataProfileResource> [V1ApiController]
public class MetadataProfileController : RestController<MetadataProfileResource>
{ {
private readonly IMetadataProfileService _profileService; private readonly IMetadataProfileService _profileService;
public MetadataProfileModule(IMetadataProfileService profileService) public MetadataProfileController(IMetadataProfileService profileService)
{ {
_profileService = profileService; _profileService = profileService;
SharedValidator.RuleFor(c => c.Name).NotEqual("None").WithMessage("'None' is a reserved profile name").NotEmpty(); SharedValidator.RuleFor(c => c.Name).NotEqual("None").WithMessage("'None' is a reserved profile name").NotEmpty();
SharedValidator.RuleFor(c => c.PrimaryAlbumTypes).MustHaveAllowedPrimaryType(); SharedValidator.RuleFor(c => c.PrimaryAlbumTypes).MustHaveAllowedPrimaryType();
SharedValidator.RuleFor(c => c.SecondaryAlbumTypes).MustHaveAllowedSecondaryType(); SharedValidator.RuleFor(c => c.SecondaryAlbumTypes).MustHaveAllowedSecondaryType();
SharedValidator.RuleFor(c => c.ReleaseStatuses).MustHaveAllowedReleaseStatus(); SharedValidator.RuleFor(c => c.ReleaseStatuses).MustHaveAllowedReleaseStatus();
GetResourceAll = GetAll;
GetResourceById = GetById;
UpdateResource = Update;
CreateResource = Create;
DeleteResource = DeleteProfile;
} }
private int Create(MetadataProfileResource resource) [RestPostById]
public ActionResult<MetadataProfileResource> Create(MetadataProfileResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
model = _profileService.Add(model); model = _profileService.Add(model);
return model.Id; return Created(model.Id);
} }
private void DeleteProfile(int id) [RestDeleteById]
public void DeleteProfile(int id)
{ {
_profileService.Delete(id); _profileService.Delete(id);
} }
private void Update(MetadataProfileResource resource) [RestPutById]
public ActionResult<MetadataProfileResource> Update(MetadataProfileResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
_profileService.Update(model); _profileService.Update(model);
return Accepted(model.Id);
} }
private MetadataProfileResource GetById(int id) public override MetadataProfileResource GetResourceById(int id)
{ {
return _profileService.Get(id).ToResource(); return _profileService.Get(id).ToResource();
} }
private List<MetadataProfileResource> GetAll() [HttpGet]
public List<MetadataProfileResource> GetAll()
{ {
var profiles = _profileService.All().ToResource(); var profiles = _profileService.All().ToResource();

@ -1,18 +1,15 @@
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Metadata;
namespace Lidarr.Api.V1.Profiles.Metadata namespace Lidarr.Api.V1.Profiles.Metadata
{ {
public class MetadataProfileSchemaModule : LidarrRestModule<MetadataProfileResource> [V1ApiController("metadataprofile/schema")]
public class MetadataProfileSchemaController : Controller
{ {
public MetadataProfileSchemaModule() [HttpGet]
: base("/metadataprofile/schema") public MetadataProfileResource GetAll()
{
GetResourceSingle = GetAll;
}
private MetadataProfileResource GetAll()
{ {
var orderedPrimTypes = NzbDrone.Core.Music.PrimaryAlbumType.All var orderedPrimTypes = NzbDrone.Core.Music.PrimaryAlbumType.All
.OrderByDescending(l => l.Id) .OrderByDescending(l => l.Id)

@ -1,53 +1,57 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
namespace Lidarr.Api.V1.Profiles.Quality namespace Lidarr.Api.V1.Profiles.Quality
{ {
public class ProfileModule : LidarrRestModule<QualityProfileResource> [V1ApiController]
public class QualityProfileController : RestController<QualityProfileResource>
{ {
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
public ProfileModule(IProfileService profileService) public QualityProfileController(IProfileService profileService)
{ {
_profileService = profileService; _profileService = profileService;
SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
SharedValidator.RuleFor(c => c.Items).ValidItems(); SharedValidator.RuleFor(c => c.Items).ValidItems();
GetResourceAll = GetAll;
GetResourceById = GetById;
UpdateResource = Update;
CreateResource = Create;
DeleteResource = DeleteProfile;
} }
private int Create(QualityProfileResource resource) [RestPostById]
public ActionResult<QualityProfileResource> Create(QualityProfileResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
model = _profileService.Add(model); model = _profileService.Add(model);
return model.Id; return Created(model.Id);
} }
private void DeleteProfile(int id) [RestDeleteById]
public void DeleteProfile(int id)
{ {
_profileService.Delete(id); _profileService.Delete(id);
} }
private void Update(QualityProfileResource resource) [RestPutById]
public ActionResult<QualityProfileResource> Update(QualityProfileResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
_profileService.Update(model); _profileService.Update(model);
return Accepted(model.Id);
} }
private QualityProfileResource GetById(int id) public override QualityProfileResource GetResourceById(int id)
{ {
return _profileService.Get(id).ToResource(); return _profileService.Get(id).ToResource();
} }
private List<QualityProfileResource> GetAll() [HttpGet]
public List<QualityProfileResource> GetAll()
{ {
return _profileService.All().ToResource(); return _profileService.All().ToResource();
} }

@ -1,20 +1,21 @@
using Lidarr.Http; using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
namespace Lidarr.Api.V1.Profiles.Quality namespace Lidarr.Api.V1.Profiles.Quality
{ {
public class QualityProfileSchemaModule : LidarrRestModule<QualityProfileResource> [V1ApiController("qualityprofile/schema")]
public class QualityProfileSchemaController : Controller
{ {
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
public QualityProfileSchemaModule(IProfileService profileService) public QualityProfileSchemaController(IProfileService profileService)
: base("/qualityprofile/schema")
{ {
_profileService = profileService; _profileService = profileService;
GetResourceSingle = GetSchema;
} }
private QualityProfileResource GetSchema() [HttpGet]
public QualityProfileResource GetSchema()
{ {
QualityProfile qualityProfile = _profileService.GetDefaultProfile(string.Empty); QualityProfile qualityProfile = _profileService.GetDefaultProfile(string.Empty);

@ -2,28 +2,26 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Profiles.Releases;
namespace Lidarr.Api.V1.Profiles.Release namespace Lidarr.Api.V1.Profiles.Release
{ {
public class ReleaseProfileModule : LidarrRestModule<ReleaseProfileResource> [V1ApiController]
public class ReleaseProfileController : RestController<ReleaseProfileResource>
{ {
private readonly IReleaseProfileService _releaseProfileService; private readonly IReleaseProfileService _releaseProfileService;
private readonly IIndexerFactory _indexerFactory; private readonly IIndexerFactory _indexerFactory;
public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory) public ReleaseProfileController(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory)
{ {
_releaseProfileService = releaseProfileService; _releaseProfileService = releaseProfileService;
_indexerFactory = indexerFactory; _indexerFactory = indexerFactory;
GetResourceById = GetById;
GetResourceAll = GetAll;
CreateResource = Create;
UpdateResource = Update;
DeleteResource = DeleteById;
SharedValidator.RuleFor(r => r).Custom((restriction, context) => SharedValidator.RuleFor(r => r).Custom((restriction, context) =>
{ {
if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty()) if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty())
@ -43,27 +41,32 @@ namespace Lidarr.Api.V1.Profiles.Release
}); });
} }
private ReleaseProfileResource GetById(int id) public override ReleaseProfileResource GetResourceById(int id)
{ {
return _releaseProfileService.Get(id).ToResource(); return _releaseProfileService.Get(id).ToResource();
} }
private List<ReleaseProfileResource> GetAll() [HttpGet]
public List<ReleaseProfileResource> GetAll()
{ {
return _releaseProfileService.All().ToResource(); return _releaseProfileService.All().ToResource();
} }
private int Create(ReleaseProfileResource resource) [RestPostById]
public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource)
{ {
return _releaseProfileService.Add(resource.ToModel()).Id; return Created(_releaseProfileService.Add(resource.ToModel()).Id);
} }
private void Update(ReleaseProfileResource resource) [RestPutById]
public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource)
{ {
_releaseProfileService.Update(resource.ToModel()); _releaseProfileService.Update(resource.ToModel());
return Accepted(resource.Id);
} }
private void DeleteById(int id) [RestDeleteById]
public void DeleteById(int id)
{ {
_releaseProfileService.Delete(id); _releaseProfileService.Delete(id);
} }

@ -2,15 +2,16 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using FluentValidation.Results; using FluentValidation.Results;
using Lidarr.Http; using Lidarr.Http.REST;
using Nancy; using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace Lidarr.Api.V1 namespace Lidarr.Api.V1
{ {
public abstract class ProviderModuleBase<TProviderResource, TProvider, TProviderDefinition> : LidarrRestModule<TProviderResource> public abstract class ProviderControllerBase<TProviderResource, TProvider, TProviderDefinition> : RestController<TProviderResource>
where TProviderDefinition : ProviderDefinition, new() where TProviderDefinition : ProviderDefinition, new()
where TProvider : IProvider where TProvider : IProvider
where TProviderResource : ProviderResource<TProviderResource>, new() where TProviderResource : ProviderResource<TProviderResource>, new()
@ -18,23 +19,11 @@ namespace Lidarr.Api.V1
private readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory; private readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory;
private readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper; private readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper;
protected ProviderModuleBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper) protected ProviderControllerBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper)
: base(resource)
{ {
_providerFactory = providerFactory; _providerFactory = providerFactory;
_resourceMapper = resourceMapper; _resourceMapper = resourceMapper;
Get("schema", x => GetTemplates());
Post("test", x => Test(ReadResourceFromRequest(true)));
Post("testall", x => TestAll());
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true)));
GetResourceAll = GetAll;
GetResourceById = GetProviderById;
CreateResource = CreateProvider;
UpdateResource = UpdateProvider;
DeleteResource = DeleteProvider;
SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique");
SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); SharedValidator.RuleFor(c => c.Implementation).NotEmpty();
@ -43,7 +32,7 @@ namespace Lidarr.Api.V1
PostValidator.RuleFor(c => c.Fields).NotNull(); PostValidator.RuleFor(c => c.Fields).NotNull();
} }
private TProviderResource GetProviderById(int id) public override TProviderResource GetResourceById(int id)
{ {
var definition = _providerFactory.Get(id); var definition = _providerFactory.Get(id);
_providerFactory.SetProviderCharacteristics(definition); _providerFactory.SetProviderCharacteristics(definition);
@ -51,7 +40,8 @@ namespace Lidarr.Api.V1
return _resourceMapper.ToResource(definition); return _resourceMapper.ToResource(definition);
} }
private List<TProviderResource> GetAll() [HttpGet]
public List<TProviderResource> GetAll()
{ {
var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName);
@ -67,7 +57,8 @@ namespace Lidarr.Api.V1
return result.OrderBy(p => p.Name).ToList(); return result.OrderBy(p => p.Name).ToList();
} }
private int CreateProvider(TProviderResource providerResource) [RestPostById]
public ActionResult<TProviderResource> CreateProvider(TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, false); var providerDefinition = GetDefinition(providerResource, false);
@ -78,10 +69,11 @@ namespace Lidarr.Api.V1
providerDefinition = _providerFactory.Create(providerDefinition); providerDefinition = _providerFactory.Create(providerDefinition);
return providerDefinition.Id; return Created(providerDefinition.Id);
} }
private void UpdateProvider(TProviderResource providerResource) [RestPutById]
public ActionResult<TProviderResource> UpdateProvider(TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, false); var providerDefinition = GetDefinition(providerResource, false);
var existingDefinition = _providerFactory.Get(providerDefinition.Id); var existingDefinition = _providerFactory.Get(providerDefinition.Id);
@ -93,6 +85,8 @@ namespace Lidarr.Api.V1
} }
_providerFactory.Update(providerDefinition); _providerFactory.Update(providerDefinition);
return Accepted(providerResource.Id);
} }
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true)
@ -107,12 +101,14 @@ namespace Lidarr.Api.V1
return definition; return definition;
} }
private void DeleteProvider(int id) [RestDeleteById]
public void DeleteProvider(int id)
{ {
_providerFactory.Delete(id); _providerFactory.Delete(id);
} }
private object GetTemplates() [HttpGet("schema")]
public List<TProviderResource> GetTemplates()
{ {
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
@ -133,7 +129,9 @@ namespace Lidarr.Api.V1
return result; return result;
} }
private object Test(TProviderResource providerResource) [SkipValidation(true, false)]
[HttpPost("test")]
public object Test([FromBody] TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, true); var providerDefinition = GetDefinition(providerResource, true);
@ -142,7 +140,8 @@ namespace Lidarr.Api.V1
return "{}"; return "{}";
} }
private object TestAll() [HttpPost("testall")]
public IActionResult TestAll()
{ {
var providerDefinitions = _providerFactory.All() var providerDefinitions = _providerFactory.All()
.Where(c => c.Settings.Validate().IsValid && c.Enable) .Where(c => c.Settings.Validate().IsValid && c.Enable)
@ -160,19 +159,20 @@ namespace Lidarr.Api.V1
}); });
} }
return ResponseWithCode(result, result.Any(c => !c.IsValid) ? HttpStatusCode.BadRequest : HttpStatusCode.OK); return result.Any(c => !c.IsValid) ? BadRequest(result) : Ok(result);
} }
private object RequestAction(string action, TProviderResource providerResource) [SkipValidation]
[HttpPost("action/{name}")]
public IActionResult RequestAction(string name, [FromBody] TProviderResource resource)
{ {
var providerDefinition = GetDefinition(providerResource, true, false); var providerDefinition = GetDefinition(resource, true, false);
var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString());
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); var data = _providerFactory.RequestAction(providerDefinition, name, query);
var data = _providerFactory.RequestAction(providerDefinition, action, query); return Content(data.ToJson(), "application/json");
Response resp = data.ToJson();
resp.ContentType = "application/json";
return resp;
} }
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)

@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Qualities;
namespace Lidarr.Api.V1.Qualities
{
[V1ApiController]
public class QualityDefinitionController : RestController<QualityDefinitionResource>
{
private readonly IQualityDefinitionService _qualityDefinitionService;
public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService)
{
_qualityDefinitionService = qualityDefinitionService;
}
[RestPutById]
public ActionResult<QualityDefinitionResource> Update(QualityDefinitionResource resource)
{
var model = resource.ToModel();
_qualityDefinitionService.Update(model);
return Accepted(model.Id);
}
public override QualityDefinitionResource GetResourceById(int id)
{
return _qualityDefinitionService.GetById(id).ToResource();
}
[HttpGet]
public List<QualityDefinitionResource> GetAll()
{
return _qualityDefinitionService.All().ToResource();
}
[HttpPut("update")]
public object UpdateMany([FromBody] List<QualityDefinitionResource> resource)
{
//Read from request
var qualityDefinitions = resource
.ToModel()
.ToList();
_qualityDefinitionService.UpdateMany(qualityDefinitions);
return Accepted(_qualityDefinitionService.All()
.ToResource());
}
}
}

@ -1,54 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Nancy;
using NzbDrone.Core.Qualities;
namespace Lidarr.Api.V1.Qualities
{
public class QualityDefinitionModule : LidarrRestModule<QualityDefinitionResource>
{
private readonly IQualityDefinitionService _qualityDefinitionService;
public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService)
{
_qualityDefinitionService = qualityDefinitionService;
GetResourceAll = GetAll;
GetResourceById = GetById;
UpdateResource = Update;
Put("/update", d => UpdateMany());
}
private void Update(QualityDefinitionResource resource)
{
var model = resource.ToModel();
_qualityDefinitionService.Update(model);
}
private QualityDefinitionResource GetById(int id)
{
return _qualityDefinitionService.GetById(id).ToResource();
}
private List<QualityDefinitionResource> GetAll()
{
return _qualityDefinitionService.All().ToResource();
}
private object UpdateMany()
{
//Read from request
var qualityDefinitions = Request.Body.FromJson<List<QualityDefinitionResource>>()
.ToModel()
.ToList();
_qualityDefinitionService.UpdateMany(qualityDefinitions);
return ResponseWithCode(_qualityDefinitionService.All()
.ToResource(),
HttpStatusCode.Accepted);
}
}
}

@ -0,0 +1,55 @@
using Lidarr.Http;
using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
namespace Lidarr.Api.V1.Queue
{
[V1ApiController("queue")]
public class QueueActionController : Controller
{
private readonly IPendingReleaseService _pendingReleaseService;
private readonly IDownloadService _downloadService;
public QueueActionController(IPendingReleaseService pendingReleaseService,
IDownloadService downloadService)
{
_pendingReleaseService = pendingReleaseService;
_downloadService = downloadService;
}
[HttpPost("grab/{id:int}")]
public object Grab(int id)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease == null)
{
throw new NotFoundException();
}
_downloadService.DownloadReport(pendingRelease.RemoteAlbum);
return new object();
}
[HttpPost("grab/bulk")]
public object Grab([FromBody] QueueBulkResource resource)
{
foreach (var id in resource.Ids)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease == null)
{
throw new NotFoundException();
}
_downloadService.DownloadReport(pendingRelease.RemoteAlbum);
}
return new object();
}
}
}

@ -1,184 +0,0 @@
using System.Collections.Generic;
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Lidarr.Http.REST;
using Nancy;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Queue;
namespace Lidarr.Api.V1.Queue
{
public class QueueActionModule : LidarrRestModule<QueueResource>
{
private readonly IQueueService _queueService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IFailedDownloadService _failedDownloadService;
private readonly IIgnoredDownloadService _ignoredDownloadService;
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IPendingReleaseService _pendingReleaseService;
private readonly IDownloadService _downloadService;
public QueueActionModule(IQueueService queueService,
ITrackedDownloadService trackedDownloadService,
IFailedDownloadService failedDownloadService,
IIgnoredDownloadService ignoredDownloadService,
IProvideDownloadClient downloadClientProvider,
IPendingReleaseService pendingReleaseService,
IDownloadService downloadService)
{
_queueService = queueService;
_trackedDownloadService = trackedDownloadService;
_failedDownloadService = failedDownloadService;
_ignoredDownloadService = ignoredDownloadService;
_downloadClientProvider = downloadClientProvider;
_pendingReleaseService = pendingReleaseService;
_downloadService = downloadService;
Post(@"/grab/(?<id>[\d]{1,10})", x => Grab((int)x.Id));
Post("/grab/bulk", x => Grab());
Delete(@"/(?<id>[\d]{1,10})", x => Remove((int)x.Id));
Delete("/bulk", x => Remove());
}
private object Grab(int id)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease == null)
{
throw new NotFoundException();
}
_downloadService.DownloadReport(pendingRelease.RemoteAlbum);
return new object();
}
private object Grab()
{
var resource = Request.Body.FromJson<QueueBulkResource>();
foreach (var id in resource.Ids)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease == null)
{
throw new NotFoundException();
}
_downloadService.DownloadReport(pendingRelease.RemoteAlbum);
}
return new object();
}
private object Remove(int id)
{
var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true);
var blacklist = Request.GetBooleanQueryParameter("blacklist");
var skipReDownload = Request.GetBooleanQueryParameter("skipredownload");
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
}
return new object();
}
private object Remove()
{
var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true);
var blacklist = Request.GetBooleanQueryParameter("blacklist");
var skipReDownload = Request.GetBooleanQueryParameter("skipredownload");
var resource = Request.Body.FromJson<QueueBulkResource>();
var trackedDownloadIds = new List<string>();
foreach (var id in resource.Ids)
{
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
}
}
_trackedDownloadService.StopTracking(trackedDownloadIds);
return new object();
}
private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist, bool skipReDownload)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease != null)
{
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
return null;
}
var trackedDownload = GetTrackedDownload(id);
if (trackedDownload == null)
{
throw new NotFoundException();
}
if (removeFromClient)
{
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
if (downloadClient == null)
{
throw new BadRequestException();
}
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true);
}
if (blacklist)
{
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipReDownload);
}
if (!removeFromClient && !blacklist)
{
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
{
return null;
}
}
return trackedDownload;
}
private TrackedDownload GetTrackedDownload(int queueId)
{
var queueItem = _queueService.Find(queueId);
if (queueItem == null)
{
throw new NotFoundException();
}
var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId);
if (trackedDownload == null)
{
throw new NotFoundException();
}
return trackedDownload;
}
}
}

@ -1,11 +1,17 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@ -14,35 +20,82 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Queue namespace Lidarr.Api.V1.Queue
{ {
public class QueueModule : LidarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>, [V1ApiController]
public class QueueController : RestControllerWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>,
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent> IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
{ {
private readonly IQueueService _queueService; private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService; private readonly IPendingReleaseService _pendingReleaseService;
private readonly QualityModelComparer _qualityComparer; private readonly QualityModelComparer _qualityComparer;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IFailedDownloadService _failedDownloadService;
private readonly IIgnoredDownloadService _ignoredDownloadService;
private readonly IProvideDownloadClient _downloadClientProvider;
public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage,
IQueueService queueService, IQueueService queueService,
IPendingReleaseService pendingReleaseService, IPendingReleaseService pendingReleaseService,
QualityProfileService qualityProfileService) QualityProfileService qualityProfileService,
ITrackedDownloadService trackedDownloadService,
IFailedDownloadService failedDownloadService,
IIgnoredDownloadService ignoredDownloadService,
IProvideDownloadClient downloadClientProvider)
: base(broadcastSignalRMessage) : base(broadcastSignalRMessage)
{ {
_queueService = queueService; _queueService = queueService;
_pendingReleaseService = pendingReleaseService; _pendingReleaseService = pendingReleaseService;
GetResourcePaged = GetQueue; _trackedDownloadService = trackedDownloadService;
_failedDownloadService = failedDownloadService;
_ignoredDownloadService = ignoredDownloadService;
_downloadClientProvider = downloadClientProvider;
_qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty)); _qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty));
} }
private PagingResource<QueueResource> GetQueue(PagingResource<QueueResource> pagingResource) public override QueueResource GetResourceById(int id)
{ {
throw new NotImplementedException();
}
[RestDeleteById]
public void RemoveAction(int id, bool removeFromClient = true, bool blacklist = false, bool skipReDownload = false)
{
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
}
}
[HttpDelete("bulk")]
public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blacklist = false, [FromQuery] bool skipReDownload = false)
{
var trackedDownloadIds = new List<string>();
foreach (var id in resource.Ids)
{
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
}
}
_trackedDownloadService.StopTracking(trackedDownloadIds);
return new object();
}
[HttpGet]
public PagingResource<QueueResource> GetQueue(bool includeUnknownArtistItems = false, bool includeArtist = false, bool includeAlbum = false)
{
var pagingResource = Request.ReadPagingResourceFromRequest<QueueResource>();
var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending); var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending);
var includeUnknownArtistItems = Request.GetBooleanQueryParameter("includeUnknownArtistItems");
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
return ApplyToPage((spec) => GetQueue(spec, includeUnknownArtistItems), pagingSpec, (q) => MapToResource(q, includeArtist, includeAlbum)); return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownArtistItems), (q) => MapToResource(q, includeArtist, includeAlbum));
} }
private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, bool includeUnknownArtistItems) private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, bool includeUnknownArtistItems)
@ -138,16 +191,83 @@ namespace Lidarr.Api.V1.Queue
} }
} }
private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist, bool skipReDownload)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease != null)
{
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
return null;
}
var trackedDownload = GetTrackedDownload(id);
if (trackedDownload == null)
{
throw new NotFoundException();
}
if (removeFromClient)
{
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
if (downloadClient == null)
{
throw new BadRequestException();
}
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true);
}
if (blacklist)
{
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipReDownload);
}
if (!removeFromClient && !blacklist)
{
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
{
return null;
}
}
return trackedDownload;
}
private TrackedDownload GetTrackedDownload(int queueId)
{
var queueItem = _queueService.Find(queueId);
if (queueItem == null)
{
throw new NotFoundException();
}
var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId);
if (trackedDownload == null)
{
throw new NotFoundException();
}
return trackedDownload;
}
private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeArtist, bool includeAlbum) private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeArtist, bool includeAlbum)
{ {
return queueItem.ToResource(includeArtist, includeAlbum); return queueItem.ToResource(includeArtist, includeAlbum);
} }
[NonAction]
public void Handle(QueueUpdatedEvent message) public void Handle(QueueUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);
} }
[NonAction]
public void Handle(PendingReleasesUpdatedEvent message) public void Handle(PendingReleasesUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

@ -2,7 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -11,55 +12,52 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Queue namespace Lidarr.Api.V1.Queue
{ {
public class QueueDetailsModule : LidarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>, [V1ApiController("queue/details")]
public class QueueDetailsController : RestControllerWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>,
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent> IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
{ {
private readonly IQueueService _queueService; private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService; private readonly IPendingReleaseService _pendingReleaseService;
public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
: base(broadcastSignalRMessage, "queue/details") : base(broadcastSignalRMessage)
{ {
_queueService = queueService; _queueService = queueService;
_pendingReleaseService = pendingReleaseService; _pendingReleaseService = pendingReleaseService;
GetResourceAll = GetQueue;
} }
private List<QueueResource> GetQueue() public override QueueResource GetResourceById(int id)
{
throw new NotImplementedException();
}
[HttpGet]
public List<QueueResource> GetQueue(int? artistId, [FromQuery]List<int> albumIds, bool includeArtist = false, bool includeAlbum = true)
{ {
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum", true);
var queue = _queueService.GetQueue(); var queue = _queueService.GetQueue();
var pending = _pendingReleaseService.GetPendingQueue(); var pending = _pendingReleaseService.GetPendingQueue();
var fullQueue = queue.Concat(pending); var fullQueue = queue.Concat(pending);
var artistIdQuery = Request.Query.ArtistId; if (artistId.HasValue)
var albumIdsQuery = Request.Query.AlbumIds;
if (artistIdQuery.HasValue)
{ {
return fullQueue.Where(q => q.Artist?.Id == (int)artistIdQuery).ToResource(includeArtist, includeAlbum); return fullQueue.Where(q => q.Artist?.Id == artistId.Value).ToResource(includeArtist, includeAlbum);
} }
if (albumIdsQuery.HasValue) if (albumIds.Any())
{ {
string albumIdsValue = albumIdsQuery.Value.ToString();
var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
return fullQueue.Where(q => q.Album != null && albumIds.Contains(q.Album.Id)).ToResource(includeArtist, includeAlbum); return fullQueue.Where(q => q.Album != null && albumIds.Contains(q.Album.Id)).ToResource(includeArtist, includeAlbum);
} }
return fullQueue.ToResource(includeArtist, includeAlbum); return fullQueue.ToResource(includeArtist, includeAlbum);
} }
[NonAction]
public void Handle(QueueUpdatedEvent message) public void Handle(QueueUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);
} }
[NonAction]
public void Handle(PendingReleasesUpdatedEvent message) public void Handle(PendingReleasesUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

@ -1,6 +1,8 @@
using System; using System;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
@ -11,30 +13,30 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Queue namespace Lidarr.Api.V1.Queue
{ {
public class QueueStatusModule : LidarrRestModuleWithSignalR<QueueStatusResource, NzbDrone.Core.Queue.Queue>, [V1ApiController("queue/status")]
public class QueueStatusController : RestControllerWithSignalR<QueueStatusResource, NzbDrone.Core.Queue.Queue>,
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent> IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
{ {
private readonly IQueueService _queueService; private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService; private readonly IPendingReleaseService _pendingReleaseService;
private readonly Debouncer _broadcastDebounce; private readonly Debouncer _broadcastDebounce;
public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) public QueueStatusController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
: base(broadcastSignalRMessage, "queue/status") : base(broadcastSignalRMessage)
{ {
_queueService = queueService; _queueService = queueService;
_pendingReleaseService = pendingReleaseService; _pendingReleaseService = pendingReleaseService;
_broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5)); _broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5));
Get("/", x => GetQueueStatusResponse());
} }
private object GetQueueStatusResponse() public override QueueStatusResource GetResourceById(int id)
{ {
return GetQueueStatus(); throw new NotImplementedException();
} }
private QueueStatusResource GetQueueStatus() [HttpGet]
public QueueStatusResource GetQueueStatus()
{ {
_broadcastDebounce.Pause(); _broadcastDebounce.Pause();
@ -62,11 +64,13 @@ namespace Lidarr.Api.V1.Queue
BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); BroadcastResourceChange(ModelAction.Updated, GetQueueStatus());
} }
[NonAction]
public void Handle(QueueUpdatedEvent message) public void Handle(QueueUpdatedEvent message)
{ {
_broadcastDebounce.Execute(); _broadcastDebounce.Execute();
} }
[NonAction]
public void Handle(PendingReleasesUpdatedEvent message) public void Handle(PendingReleasesUpdatedEvent message)
{ {
_broadcastDebounce.Execute(); _broadcastDebounce.Execute();

@ -1,27 +1,25 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
namespace Lidarr.Api.V1.RemotePathMappings namespace Lidarr.Api.V1.RemotePathMappings
{ {
public class RemotePathMappingModule : LidarrRestModule<RemotePathMappingResource> [V1ApiController]
public class RemotePathMappingController : RestController<RemotePathMappingResource>
{ {
private readonly IRemotePathMappingService _remotePathMappingService; private readonly IRemotePathMappingService _remotePathMappingService;
public RemotePathMappingModule(IRemotePathMappingService remotePathMappingService, public RemotePathMappingController(IRemotePathMappingService remotePathMappingService,
PathExistsValidator pathExistsValidator, PathExistsValidator pathExistsValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator) MappedNetworkDriveValidator mappedNetworkDriveValidator)
{ {
_remotePathMappingService = remotePathMappingService; _remotePathMappingService = remotePathMappingService;
GetResourceAll = GetMappings;
GetResourceById = GetMappingById;
CreateResource = CreateMapping;
DeleteResource = DeleteMapping;
UpdateResource = UpdateMapping;
SharedValidator.RuleFor(c => c.Host) SharedValidator.RuleFor(c => c.Host)
.NotEmpty(); .NotEmpty();
@ -36,33 +34,37 @@ namespace Lidarr.Api.V1.RemotePathMappings
.SetValidator(pathExistsValidator); .SetValidator(pathExistsValidator);
} }
private RemotePathMappingResource GetMappingById(int id) public override RemotePathMappingResource GetResourceById(int id)
{ {
return _remotePathMappingService.Get(id).ToResource(); return _remotePathMappingService.Get(id).ToResource();
} }
private int CreateMapping(RemotePathMappingResource resource) [RestPostById]
public ActionResult<RemotePathMappingResource> CreateMapping(RemotePathMappingResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
return _remotePathMappingService.Add(model).Id; return Created(_remotePathMappingService.Add(model).Id);
} }
private List<RemotePathMappingResource> GetMappings() [HttpGet]
public List<RemotePathMappingResource> GetMappings()
{ {
return _remotePathMappingService.All().ToResource(); return _remotePathMappingService.All().ToResource();
} }
private void DeleteMapping(int id) [RestDeleteById]
public void DeleteMapping(int id)
{ {
_remotePathMappingService.Remove(id); _remotePathMappingService.Remove(id);
} }
private void UpdateMapping(RemotePathMappingResource resource) [RestPutById]
public ActionResult<RemotePathMappingResource> UpdateMapping(RemotePathMappingResource resource)
{ {
var mapping = resource.ToModel(); var mapping = resource.ToModel();
_remotePathMappingService.Update(mapping); return Accepted(_remotePathMappingService.Update(mapping));
} }
} }
} }

@ -2,6 +2,8 @@ using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@ -9,11 +11,12 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.RootFolders namespace Lidarr.Api.V1.RootFolders
{ {
public class RootFolderModule : LidarrRestModuleWithSignalR<RootFolderResource, RootFolder> [V1ApiController]
public class RootFolderController : RestControllerWithSignalR<RootFolderResource, RootFolder>
{ {
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
public RootFolderModule(IRootFolderService rootFolderService, public RootFolderController(IRootFolderService rootFolderService,
IBroadcastSignalRMessage signalRBroadcaster, IBroadcastSignalRMessage signalRBroadcaster,
RootFolderValidator rootFolderValidator, RootFolderValidator rootFolderValidator,
PathExistsValidator pathExistsValidator, PathExistsValidator pathExistsValidator,
@ -27,12 +30,6 @@ namespace Lidarr.Api.V1.RootFolders
{ {
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
GetResourceAll = GetRootFolders;
GetResourceById = GetRootFolder;
CreateResource = CreateRootFolder;
UpdateResource = UpdateRootFolder;
DeleteResource = DeleteFolder;
SharedValidator.RuleFor(c => c.Path) SharedValidator.RuleFor(c => c.Path)
.Cascade(CascadeMode.StopOnFirstFailure) .Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath() .IsValidPath()
@ -55,19 +52,21 @@ namespace Lidarr.Api.V1.RootFolders
.SetValidator(qualityProfileExistsValidator); .SetValidator(qualityProfileExistsValidator);
} }
private RootFolderResource GetRootFolder(int id) public override RootFolderResource GetResourceById(int id)
{ {
return _rootFolderService.Get(id).ToResource(); return _rootFolderService.Get(id).ToResource();
} }
private int CreateRootFolder(RootFolderResource rootFolderResource) [RestPostById]
public ActionResult<RootFolderResource> CreateRootFolder(RootFolderResource rootFolderResource)
{ {
var model = rootFolderResource.ToModel(); var model = rootFolderResource.ToModel();
return _rootFolderService.Add(model).Id; return Created(_rootFolderService.Add(model).Id);
} }
private void UpdateRootFolder(RootFolderResource rootFolderResource) [RestPutById]
public ActionResult<RootFolderResource> UpdateRootFolder(RootFolderResource rootFolderResource)
{ {
var model = rootFolderResource.ToModel(); var model = rootFolderResource.ToModel();
@ -77,14 +76,18 @@ namespace Lidarr.Api.V1.RootFolders
} }
_rootFolderService.Update(model); _rootFolderService.Update(model);
return Accepted(model.Id);
} }
private List<RootFolderResource> GetRootFolders() [HttpGet]
public List<RootFolderResource> GetRootFolders()
{ {
return _rootFolderService.AllWithSpaceStats().ToResource(); return _rootFolderService.AllWithSpaceStats().ToResource();
} }
private void DeleteFolder(int id) [RestDeleteById]
public void DeleteFolder(int id)
{ {
_rootFolderService.Remove(id); _rootFolderService.Remove(id);
} }

@ -4,26 +4,26 @@ using System.Linq;
using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Albums;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using Lidarr.Http; using Lidarr.Http;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
namespace Lidarr.Api.V1.Search namespace Lidarr.Api.V1.Search
{ {
public class SearchModule : LidarrRestModule<SearchResource> [V1ApiController]
public class SearchController : Controller
{ {
private readonly ISearchForNewEntity _searchProxy; private readonly ISearchForNewEntity _searchProxy;
public SearchModule(ISearchForNewEntity searchProxy) public SearchController(ISearchForNewEntity searchProxy)
: base("/search")
{ {
_searchProxy = searchProxy; _searchProxy = searchProxy;
Get("/", x => Search());
} }
private object Search() [HttpGet]
public object Search([FromQuery] string term)
{ {
var searchResults = _searchProxy.SearchForNewEntity((string)Request.Query.term); var searchResults = _searchProxy.SearchForNewEntity(term);
return MapToResource(searchResults).ToList(); return MapToResource(searchResults).ToList();
} }

@ -3,6 +3,8 @@ using System.IO;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Crypto; using NzbDrone.Common.Crypto;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -11,7 +13,8 @@ using NzbDrone.Core.Backup;
namespace Lidarr.Api.V1.System.Backup namespace Lidarr.Api.V1.System.Backup
{ {
public class BackupModule : LidarrRestModule<BackupResource> [V1ApiController("system/backup")]
public class BackupController : Controller
{ {
private readonly IBackupService _backupService; private readonly IBackupService _backupService;
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
@ -19,21 +22,16 @@ namespace Lidarr.Api.V1.System.Backup
private static readonly List<string> ValidExtensions = new List<string> { ".zip", ".db", ".xml" }; private static readonly List<string> ValidExtensions = new List<string> { ".zip", ".db", ".xml" };
public BackupModule(IBackupService backupService, public BackupController(IBackupService backupService,
IAppFolderInfo appFolderInfo, IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider) IDiskProvider diskProvider)
: base("system/backup")
{ {
_backupService = backupService; _backupService = backupService;
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_diskProvider = diskProvider; _diskProvider = diskProvider;
GetResourceAll = GetBackupFiles;
DeleteResource = DeleteBackup;
Post(@"/restore/(?<id>[\d]{1,10})", x => Restore((int)x.Id));
Post("/restore/upload", x => UploadAndRestore());
} }
[HttpGet]
public List<BackupResource> GetBackupFiles() public List<BackupResource> GetBackupFiles()
{ {
var backups = _backupService.GetBackups(); var backups = _backupService.GetBackups();
@ -50,7 +48,8 @@ namespace Lidarr.Api.V1.System.Backup
.ToList(); .ToList();
} }
private void DeleteBackup(int id) [RestDeleteById]
public void DeleteBackup(int id)
{ {
var backup = GetBackup(id); var backup = GetBackup(id);
var path = GetBackupPath(backup); var path = GetBackupPath(backup);
@ -63,6 +62,7 @@ namespace Lidarr.Api.V1.System.Backup
_diskProvider.DeleteFile(path); _diskProvider.DeleteFile(path);
} }
[HttpPost("restore/{id:int}")]
public object Restore(int id) public object Restore(int id)
{ {
var backup = GetBackup(id); var backup = GetBackup(id);
@ -82,9 +82,10 @@ namespace Lidarr.Api.V1.System.Backup
}; };
} }
[HttpPost("restore/upload")]
public object UploadAndRestore() public object UploadAndRestore()
{ {
var files = Context.Request.Files.ToList(); var files = Request.Form.Files;
if (files.Empty()) if (files.Empty())
{ {
@ -92,7 +93,7 @@ namespace Lidarr.Api.V1.System.Backup
} }
var file = files.First(); var file = files.First();
var extension = Path.GetExtension(file.Name); var extension = Path.GetExtension(file.FileName);
if (!ValidExtensions.Contains(extension)) if (!ValidExtensions.Contains(extension))
{ {
@ -101,7 +102,7 @@ namespace Lidarr.Api.V1.System.Backup
var path = Path.Combine(_appFolderInfo.TempFolder, $"lidarr_backup_restore{extension}"); var path = Path.Combine(_appFolderInfo.TempFolder, $"lidarr_backup_restore{extension}");
_diskProvider.SaveStream(file.Value, path); _diskProvider.SaveStream(file.OpenReadStream(), path);
_backupService.Restore(path); _backupService.Restore(path);
// Cleanup restored file // Cleanup restored file

@ -1,5 +1,10 @@
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Nancy.Routing; using Lidarr.Http;
using Lidarr.Http.Validation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -8,45 +13,48 @@ using NzbDrone.Core.Lifecycle;
namespace Lidarr.Api.V1.System namespace Lidarr.Api.V1.System
{ {
public class SystemModule : LidarrV1Module [V1ApiController]
public class SystemController : Controller
{ {
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IPlatformInfo _platformInfo; private readonly IPlatformInfo _platformInfo;
private readonly IOsInfo _osInfo; private readonly IOsInfo _osInfo;
private readonly IRouteCacheProvider _routeCacheProvider;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IMainDatabase _database; private readonly IMainDatabase _database;
private readonly ILifecycleService _lifecycleService; private readonly ILifecycleService _lifecycleService;
private readonly IDeploymentInfoProvider _deploymentInfoProvider; private readonly IDeploymentInfoProvider _deploymentInfoProvider;
private readonly EndpointDataSource _endpointData;
private readonly DfaGraphWriter _graphWriter;
private readonly DuplicateEndpointDetector _detector;
public SystemModule(IAppFolderInfo appFolderInfo, public SystemController(IAppFolderInfo appFolderInfo,
IRuntimeInfo runtimeInfo, IRuntimeInfo runtimeInfo,
IPlatformInfo platformInfo, IPlatformInfo platformInfo,
IOsInfo osInfo, IOsInfo osInfo,
IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider,
IConfigFileProvider configFileProvider, IMainDatabase database,
IMainDatabase database, ILifecycleService lifecycleService,
ILifecycleService lifecycleService, IDeploymentInfoProvider deploymentInfoProvider,
IDeploymentInfoProvider deploymentInfoProvider) EndpointDataSource endpoints,
: base("system") DfaGraphWriter graphWriter,
DuplicateEndpointDetector detector)
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_platformInfo = platformInfo; _platformInfo = platformInfo;
_osInfo = osInfo; _osInfo = osInfo;
_routeCacheProvider = routeCacheProvider;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_database = database; _database = database;
_lifecycleService = lifecycleService; _lifecycleService = lifecycleService;
_deploymentInfoProvider = deploymentInfoProvider; _deploymentInfoProvider = deploymentInfoProvider;
Get("/status", x => GetStatus()); _endpointData = endpoints;
Get("/routes", x => GetRoutes()); _graphWriter = graphWriter;
Post("/shutdown", x => Shutdown()); _detector = detector;
Post("/restart", x => Restart());
} }
private object GetStatus() [HttpGet("status")]
public object GetStatus()
{ {
return new return new
{ {
@ -81,18 +89,32 @@ namespace Lidarr.Api.V1.System
}; };
} }
private object GetRoutes() [HttpGet("routes")]
public IActionResult GetRoutes()
{ {
return _routeCacheProvider.GetCache().Values; using (var sw = new StringWriter())
{
_graphWriter.Write(_endpointData, sw);
var graph = sw.ToString();
return Content(graph, "text/plain");
}
}
[HttpGet("routes/duplicate")]
public object DuplicateRoutes()
{
return _detector.GetDuplicateEndpoints(_endpointData);
} }
private object Shutdown() [HttpPost("shutdown")]
public object Shutdown()
{ {
Task.Factory.StartNew(() => _lifecycleService.Shutdown()); Task.Factory.StartNew(() => _lifecycleService.Shutdown());
return new { ShuttingDown = true }; return new { ShuttingDown = true };
} }
private object Restart() [HttpPost("restart")]
public object Restart()
{ {
Task.Factory.StartNew(() => _lifecycleService.Restart()); Task.Factory.StartNew(() => _lifecycleService.Restart());
return new { Restarting = true }; return new { Restarting = true };

@ -1,6 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Jobs; using NzbDrone.Core.Jobs;
@ -9,19 +11,19 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.System.Tasks namespace Lidarr.Api.V1.System.Tasks
{ {
public class TaskModule : LidarrRestModuleWithSignalR<TaskResource, ScheduledTask>, IHandle<CommandExecutedEvent> [V1ApiController("system/task")]
public class TaskController : RestControllerWithSignalR<TaskResource, ScheduledTask>, IHandle<CommandExecutedEvent>
{ {
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) public TaskController(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
: base(broadcastSignalRMessage, "system/task") : base(broadcastSignalRMessage)
{ {
_taskManager = taskManager; _taskManager = taskManager;
GetResourceAll = GetAll;
GetResourceById = GetTask;
} }
private List<TaskResource> GetAll() [HttpGet]
public List<TaskResource> GetAll()
{ {
return _taskManager.GetAll() return _taskManager.GetAll()
.Select(ConvertToResource) .Select(ConvertToResource)
@ -29,7 +31,7 @@ namespace Lidarr.Api.V1.System.Tasks
.ToList(); .ToList();
} }
private TaskResource GetTask(int id) public override TaskResource GetResourceById(int id)
{ {
var task = _taskManager.GetAll() var task = _taskManager.GetAll()
.SingleOrDefault(t => t.Id == id); .SingleOrDefault(t => t.Id == id);
@ -58,6 +60,7 @@ namespace Lidarr.Api.V1.System.Tasks
}; };
} }
[NonAction]
public void Handle(CommandExecutedEvent message) public void Handle(CommandExecutedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

@ -1,5 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tags; using NzbDrone.Core.Tags;
@ -7,48 +10,49 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Tags namespace Lidarr.Api.V1.Tags
{ {
public class TagModule : LidarrRestModuleWithSignalR<TagResource, Tag>, IHandle<TagsUpdatedEvent> [V1ApiController]
public class TagController : RestControllerWithSignalR<TagResource, Tag>, IHandle<TagsUpdatedEvent>
{ {
private readonly ITagService _tagService; private readonly ITagService _tagService;
public TagModule(IBroadcastSignalRMessage signalRBroadcaster, public TagController(IBroadcastSignalRMessage signalRBroadcaster,
ITagService tagService) ITagService tagService)
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_tagService = tagService; _tagService = tagService;
GetResourceById = GetTag;
GetResourceAll = GetAll;
CreateResource = Create;
UpdateResource = Update;
DeleteResource = DeleteTag;
} }
private TagResource GetTag(int id) public override TagResource GetResourceById(int id)
{ {
return _tagService.GetTag(id).ToResource(); return _tagService.GetTag(id).ToResource();
} }
private List<TagResource> GetAll() [HttpGet]
public List<TagResource> GetAll()
{ {
return _tagService.All().ToResource(); return _tagService.All().ToResource();
} }
private int Create(TagResource resource) [RestPostById]
public ActionResult<TagResource> Create(TagResource resource)
{ {
return _tagService.Add(resource.ToModel()).Id; return Created(_tagService.Add(resource.ToModel()).Id);
} }
private void Update(TagResource resource) [RestPutById]
public ActionResult<TagResource> Update(TagResource resource)
{ {
_tagService.Update(resource.ToModel()); _tagService.Update(resource.ToModel());
return Accepted(resource.Id);
} }
private void DeleteTag(int id) [RestDeleteById]
public void DeleteTag(int id)
{ {
_tagService.Delete(id); _tagService.Delete(id);
} }
[NonAction]
public void Handle(TagsUpdatedEvent message) public void Handle(TagsUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

@ -1,28 +1,28 @@
using System.Collections.Generic; using System.Collections.Generic;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Tags; using NzbDrone.Core.Tags;
namespace Lidarr.Api.V1.Tags namespace Lidarr.Api.V1.Tags
{ {
public class TagDetailsModule : LidarrRestModule<TagDetailsResource> [V1ApiController("tag/detail")]
public class TagDetailsController : RestController<TagDetailsResource>
{ {
private readonly ITagService _tagService; private readonly ITagService _tagService;
public TagDetailsModule(ITagService tagService) public TagDetailsController(ITagService tagService)
: base("/tag/detail")
{ {
_tagService = tagService; _tagService = tagService;
GetResourceById = GetTagDetails;
GetResourceAll = GetAll;
} }
private TagDetailsResource GetTagDetails(int id) public override TagDetailsResource GetResourceById(int id)
{ {
return _tagService.Details(id).ToResource(); return _tagService.Details(id).ToResource();
} }
private List<TagDetailsResource> GetAll() [HttpGet]
public List<TagDetailsResource> GetAll()
{ {
var tags = _tagService.Details().ToResource(); var tags = _tagService.Details().ToResource();

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.REST;
using Nancy; using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
@ -12,11 +12,13 @@ using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using BadRequestException = Lidarr.Http.REST.BadRequestException;
using HttpStatusCode = System.Net.HttpStatusCode; using HttpStatusCode = System.Net.HttpStatusCode;
namespace Lidarr.Api.V1.TrackFiles namespace Lidarr.Api.V1.TrackFiles
{ {
public class TrackFileModule : LidarrRestModuleWithSignalR<TrackFileResource, TrackFile>, [V1ApiController]
public class TrackFileController : RestControllerWithSignalR<TrackFileResource, TrackFile>,
IHandle<TrackFileAddedEvent>, IHandle<TrackFileAddedEvent>,
IHandle<TrackFileDeletedEvent> IHandle<TrackFileDeletedEvent>
{ {
@ -27,7 +29,7 @@ namespace Lidarr.Api.V1.TrackFiles
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster, public TrackFileController(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IDeleteMediaFiles mediaFileDeletionService, IDeleteMediaFiles mediaFileDeletionService,
IAudioTagService audioTagService, IAudioTagService audioTagService,
@ -42,14 +44,6 @@ namespace Lidarr.Api.V1.TrackFiles
_artistService = artistService; _artistService = artistService;
_albumService = albumService; _albumService = albumService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
GetResourceById = GetTrackFile;
GetResourceAll = GetTrackFiles;
UpdateResource = SetQuality;
DeleteResource = DeleteTrackFile;
Put("/editor", trackFiles => SetQuality());
Delete("/bulk", trackFiles => DeleteTrackFiles());
} }
private TrackFileResource MapToResource(TrackFile trackFile) private TrackFileResource MapToResource(TrackFile trackFile)
@ -64,47 +58,36 @@ namespace Lidarr.Api.V1.TrackFiles
} }
} }
private TrackFileResource GetTrackFile(int id) public override TrackFileResource GetResourceById(int id)
{ {
var resource = MapToResource(_mediaFileService.Get(id)); var resource = MapToResource(_mediaFileService.Get(id));
resource.AudioTags = _audioTagService.ReadTags(resource.Path); resource.AudioTags = _audioTagService.ReadTags(resource.Path);
return resource; return resource;
} }
private List<TrackFileResource> GetTrackFiles() [HttpGet]
public List<TrackFileResource> GetTrackFiles(int? artistId, [FromQuery] List<int> trackFileIds, [FromQuery(Name = "albumId")] List<int> albumIds, bool? unmapped)
{ {
var artistIdQuery = Request.Query.ArtistId; if (!artistId.HasValue && !trackFileIds.Any() && !albumIds.Any() && !unmapped.HasValue)
var trackFileIdsQuery = Request.Query.TrackFileIds;
var albumIdQuery = Request.Query.AlbumId;
var unmappedQuery = Request.Query.Unmapped;
if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue && !albumIdQuery.HasValue && !unmappedQuery.HasValue)
{ {
throw new Lidarr.Http.REST.BadRequestException("artistId, albumId, trackFileIds or unmapped must be provided"); throw new BadRequestException("artistId, albumId, trackFileIds or unmapped must be provided");
} }
if (unmappedQuery.HasValue && Convert.ToBoolean(unmappedQuery.Value)) if (unmapped.HasValue && unmapped.Value)
{ {
var files = _mediaFileService.GetUnmappedFiles(); var files = _mediaFileService.GetUnmappedFiles();
return files.ConvertAll(f => MapToResource(f)); return files.ConvertAll(f => MapToResource(f));
} }
if (artistIdQuery.HasValue && !albumIdQuery.HasValue) if (artistId.HasValue && !albumIds.Any())
{ {
int artistId = Convert.ToInt32(artistIdQuery.Value); var artist = _artistService.GetArtist(artistId.Value);
var artist = _artistService.GetArtist(artistId);
return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); return _mediaFileService.GetFilesByArtist(artistId.Value).ConvertAll(f => f.ToResource(artist, _upgradableSpecification));
} }
if (albumIdQuery.HasValue) if (albumIds.Any())
{ {
string albumIdValue = albumIdQuery.Value.ToString();
var albumIds = albumIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
var result = new List<TrackFileResource>(); var result = new List<TrackFileResource>();
foreach (var albumId in albumIds) foreach (var albumId in albumIds)
{ {
@ -117,28 +100,24 @@ namespace Lidarr.Api.V1.TrackFiles
} }
else else
{ {
string trackFileIdsValue = trackFileIdsQuery.Value.ToString();
var trackFileIds = trackFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
// trackfiles will come back with the artist already populated // trackfiles will come back with the artist already populated
var trackFiles = _mediaFileService.Get(trackFileIds); var trackFiles = _mediaFileService.Get(trackFileIds);
return trackFiles.ConvertAll(e => MapToResource(e)); return trackFiles.ConvertAll(e => MapToResource(e));
} }
} }
private void SetQuality(TrackFileResource trackFileResource) [RestPutById]
public ActionResult<TrackFileResource> SetQuality([FromBody] TrackFileResource trackFileResource)
{ {
var trackFile = _mediaFileService.Get(trackFileResource.Id); var trackFile = _mediaFileService.Get(trackFileResource.Id);
trackFile.Quality = trackFileResource.Quality; trackFile.Quality = trackFileResource.Quality;
_mediaFileService.Update(trackFile); _mediaFileService.Update(trackFile);
return Accepted(trackFile.Id);
} }
private object SetQuality() [HttpPut("editor")]
public IActionResult SetQuality([FromBody] TrackFileListResource resource)
{ {
var resource = Request.Body.FromJson<TrackFileListResource>();
var trackFiles = _mediaFileService.Get(resource.TrackFileIds); var trackFiles = _mediaFileService.Get(resource.TrackFileIds);
foreach (var trackFile in trackFiles) foreach (var trackFile in trackFiles)
@ -151,11 +130,11 @@ namespace Lidarr.Api.V1.TrackFiles
_mediaFileService.Update(trackFiles); _mediaFileService.Update(trackFiles);
return ResponseWithCode(trackFiles.ConvertAll(f => f.ToResource(trackFiles.First().Artist.Value, _upgradableSpecification)), return Accepted(trackFiles.ConvertAll(f => f.ToResource(trackFiles.First().Artist.Value, _upgradableSpecification)));
Nancy.HttpStatusCode.Accepted);
} }
private void DeleteTrackFile(int id) [RestDeleteById]
public void DeleteTrackFile(int id)
{ {
var trackFile = _mediaFileService.Get(id); var trackFile = _mediaFileService.Get(id);
@ -174,9 +153,9 @@ namespace Lidarr.Api.V1.TrackFiles
} }
} }
private object DeleteTrackFiles() [HttpDelete("bulk")]
public IActionResult DeleteTrackFiles([FromBody] TrackFileListResource resource)
{ {
var resource = Request.Body.FromJson<TrackFileListResource>();
var trackFiles = _mediaFileService.Get(resource.TrackFileIds); var trackFiles = _mediaFileService.Get(resource.TrackFileIds);
var artist = trackFiles.First().Artist.Value; var artist = trackFiles.First().Artist.Value;
@ -185,14 +164,16 @@ namespace Lidarr.Api.V1.TrackFiles
_mediaFileDeletionService.DeleteTrackFile(artist, trackFile); _mediaFileDeletionService.DeleteTrackFile(artist, trackFile);
} }
return new object(); return Ok();
} }
[NonAction]
public void Handle(TrackFileAddedEvent message) public void Handle(TrackFileAddedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.TrackFile)); BroadcastResourceChange(ModelAction.Updated, MapToResource(message.TrackFile));
} }
[NonAction]
public void Handle(TrackFileDeletedEvent message) public void Handle(TrackFileDeletedEvent message)
{ {
BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.TrackFile)); BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.TrackFile));

@ -0,0 +1,29 @@
using System.Collections.Generic;
using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles;
namespace Lidarr.Api.V1.Tracks
{
[V1ApiController("rename")]
public class RenameTrackController : Controller
{
private readonly IRenameTrackFileService _renameTrackFileService;
public RenameTrackController(IRenameTrackFileService renameTrackFileService)
{
_renameTrackFileService = renameTrackFileService;
}
[HttpGet]
public List<RenameTrackResource> GetTracks(int artistId, int? albumId)
{
if (albumId.HasValue)
{
return _renameTrackFileService.GetRenamePreviews(artistId, albumId.Value).ToResource();
}
return _renameTrackFileService.GetRenamePreviews(artistId).ToResource();
}
}
}

@ -1,42 +0,0 @@
using System.Collections.Generic;
using Lidarr.Http;
using Lidarr.Http.REST;
using NzbDrone.Core.MediaFiles;
namespace Lidarr.Api.V1.Tracks
{
public class RenameTrackModule : LidarrRestModule<RenameTrackResource>
{
private readonly IRenameTrackFileService _renameTrackFileService;
public RenameTrackModule(IRenameTrackFileService renameTrackFileService)
: base("rename")
{
_renameTrackFileService = renameTrackFileService;
GetResourceAll = GetTracks;
}
private List<RenameTrackResource> GetTracks()
{
int artistId;
if (Request.Query.ArtistId.HasValue)
{
artistId = (int)Request.Query.ArtistId;
}
else
{
throw new BadRequestException("artistId is missing");
}
if (Request.Query.albumId.HasValue)
{
var albumId = (int)Request.Query.albumId;
return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource();
}
return _renameTrackFileService.GetRenamePreviews(artistId).ToResource();
}
}
}

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles;
namespace Lidarr.Api.V1.Tracks
{
[V1ApiController("retag")]
public class RetagTrackController : Controller
{
private readonly IAudioTagService _audioTagService;
public RetagTrackController(IAudioTagService audioTagService)
{
_audioTagService = audioTagService;
}
[HttpGet]
public List<RetagTrackResource> GetTracks(int artistId, int? albumId)
{
if (albumId.HasValue)
{
return _audioTagService.GetRetagPreviewsByAlbum(albumId.Value).Where(x => x.Changes.Any()).ToResource();
}
return _audioTagService.GetRetagPreviewsByArtist(artistId).Where(x => x.Changes.Any()).ToResource();
}
}
}

@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http;
using Lidarr.Http.REST;
using NzbDrone.Core.MediaFiles;
namespace Lidarr.Api.V1.Tracks
{
public class RetagTrackModule : LidarrRestModule<RetagTrackResource>
{
private readonly IAudioTagService _audioTagService;
public RetagTrackModule(IAudioTagService audioTagService)
: base("retag")
{
_audioTagService = audioTagService;
GetResourceAll = GetTracks;
}
private List<RetagTrackResource> GetTracks()
{
if (Request.Query.albumId.HasValue)
{
var albumId = (int)Request.Query.albumId;
return _audioTagService.GetRetagPreviewsByAlbum(albumId).Where(x => x.Changes.Any()).ToResource();
}
else if (Request.Query.ArtistId.HasValue)
{
var artistId = (int)Request.Query.ArtistId;
return _audioTagService.GetRetagPreviewsByArtist(artistId).Where(x => x.Changes.Any()).ToResource();
}
else
{
throw new BadRequestException("One of artistId or albumId must be specified");
}
}
}
}

@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http;
using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Music;
using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Tracks
{
[V1ApiController]
public class TrackController : TrackControllerWithSignalR
{
public TrackController(IArtistService artistService,
ITrackService trackService,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(trackService, artistService, upgradableSpecification, signalRBroadcaster)
{
}
[HttpGet]
public List<TrackResource> GetTracks([FromQuery]int? artistId,
[FromQuery]int? albumId,
[FromQuery]int? albumReleaseId,
[FromQuery]List<int> trackIds)
{
if (!artistId.HasValue && !trackIds.Any() && !albumId.HasValue && !albumReleaseId.HasValue)
{
throw new BadRequestException("One of artistId, albumId, albumReleaseId or trackIds must be provided");
}
if (artistId.HasValue && !albumId.HasValue)
{
return MapToResource(_trackService.GetTracksByArtist(artistId.Value), false, false);
}
if (albumReleaseId.HasValue)
{
return MapToResource(_trackService.GetTracksByRelease(albumReleaseId.Value), false, false);
}
if (albumId.HasValue)
{
return MapToResource(_trackService.GetTracksByAlbum(albumId.Value), false, false);
}
return MapToResource(_trackService.GetTracks(trackIds), false, false);
}
}
}

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using Lidarr.Api.V1.TrackFiles; using Lidarr.Api.V1.TrackFiles;
using Lidarr.Http; using Lidarr.Http.REST;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
@ -11,7 +12,7 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Tracks namespace Lidarr.Api.V1.Tracks
{ {
public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>, public abstract class TrackControllerWithSignalR : RestControllerWithSignalR<TrackResource, Track>,
IHandle<TrackImportedEvent>, IHandle<TrackImportedEvent>,
IHandle<TrackFileDeletedEvent> IHandle<TrackFileDeletedEvent>
{ {
@ -19,7 +20,7 @@ namespace Lidarr.Api.V1.Tracks
protected readonly IArtistService _artistService; protected readonly IArtistService _artistService;
protected readonly IUpgradableSpecification _upgradableSpecification; protected readonly IUpgradableSpecification _upgradableSpecification;
protected TrackModuleWithSignalR(ITrackService trackService, protected TrackControllerWithSignalR(ITrackService trackService,
IArtistService artistService, IArtistService artistService,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster) IBroadcastSignalRMessage signalRBroadcaster)
@ -28,25 +29,9 @@ namespace Lidarr.Api.V1.Tracks
_trackService = trackService; _trackService = trackService;
_artistService = artistService; _artistService = artistService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
GetResourceById = GetTrack;
}
protected TrackModuleWithSignalR(ITrackService trackService,
IArtistService artistService,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster,
string resource)
: base(signalRBroadcaster, resource)
{
_trackService = trackService;
_artistService = artistService;
_upgradableSpecification = upgradableSpecification;
GetResourceById = GetTrack;
} }
protected TrackResource GetTrack(int id) public override TrackResource GetResourceById(int id)
{ {
var track = _trackService.GetTrack(id); var track = _trackService.GetTrack(id);
var resource = MapToResource(track, true, true); var resource = MapToResource(track, true, true);
@ -103,6 +88,7 @@ namespace Lidarr.Api.V1.Tracks
return result; return result;
} }
[NonAction]
public void Handle(TrackImportedEvent message) public void Handle(TrackImportedEvent message)
{ {
foreach (var track in message.TrackInfo.Tracks) foreach (var track in message.TrackInfo.Tracks)
@ -112,6 +98,7 @@ namespace Lidarr.Api.V1.Tracks
} }
} }
[NonAction]
public void Handle(TrackFileDeletedEvent message) public void Handle(TrackFileDeletedEvent message)
{ {
foreach (var track in message.TrackFile.Tracks.Value) foreach (var track in message.TrackFile.Tracks.Value)

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http.REST;
using Nancy;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Music;
using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Tracks
{
public class TrackModule : TrackModuleWithSignalR
{
public TrackModule(IArtistService artistService,
ITrackService trackService,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(trackService, artistService, upgradableSpecification, signalRBroadcaster)
{
GetResourceAll = GetTracks;
}
private List<TrackResource> GetTracks()
{
var artistIdQuery = Request.Query.ArtistId;
var albumIdQuery = Request.Query.AlbumId;
var albumReleaseIdQuery = Request.Query.AlbumReleaseId;
var trackIdsQuery = Request.Query.TrackIds;
if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue && !albumReleaseIdQuery.HasValue)
{
throw new BadRequestException("One of artistId, albumId, albumReleaseId or trackIds must be provided");
}
if (artistIdQuery.HasValue && !albumIdQuery.HasValue)
{
int artistId = Convert.ToInt32(artistIdQuery.Value);
return MapToResource(_trackService.GetTracksByArtist(artistId), false, false);
}
if (albumReleaseIdQuery.HasValue)
{
int releaseId = Convert.ToInt32(albumReleaseIdQuery.Value);
return MapToResource(_trackService.GetTracksByRelease(releaseId), false, false);
}
if (albumIdQuery.HasValue)
{
int albumId = Convert.ToInt32(albumIdQuery.Value);
return MapToResource(_trackService.GetTracksByAlbum(albumId), false, false);
}
string trackIdsValue = trackIdsQuery.Value.ToString();
var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
return MapToResource(_trackService.GetTracks(trackIds), false, false);
}
}
}

@ -1,22 +1,24 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http; using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
namespace Lidarr.Api.V1.Update namespace Lidarr.Api.V1.Update
{ {
public class UpdateModule : LidarrRestModule<UpdateResource> [V1ApiController]
public class UpdateController : Controller
{ {
private readonly IRecentUpdateProvider _recentUpdateProvider; private readonly IRecentUpdateProvider _recentUpdateProvider;
public UpdateModule(IRecentUpdateProvider recentUpdateProvider) public UpdateController(IRecentUpdateProvider recentUpdateProvider)
{ {
_recentUpdateProvider = recentUpdateProvider; _recentUpdateProvider = recentUpdateProvider;
GetResourceAll = GetRecentUpdates;
} }
private List<UpdateResource> GetRecentUpdates() [HttpGet]
public List<UpdateResource> GetRecentUpdates()
{ {
var resources = _recentUpdateProvider.GetRecentUpdatePackages() var resources = _recentUpdateProvider.GetRecentUpdatePackages()
.OrderByDescending(u => u.Version) .OrderByDescending(u => u.Version)

@ -2,6 +2,7 @@ using System.Linq;
using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Albums;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
@ -11,24 +12,26 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Wanted namespace Lidarr.Api.V1.Wanted
{ {
public class CutoffModule : AlbumModuleWithSignalR [V1ApiController("wanted/cutoff")]
public class CutoffController : AlbumControllerWithSignalR
{ {
private readonly IAlbumCutoffService _albumCutoffService; private readonly IAlbumCutoffService _albumCutoffService;
public CutoffModule(IAlbumCutoffService albumCutoffService, public CutoffController(IAlbumCutoffService albumCutoffService,
IAlbumService albumService, IAlbumService albumService,
IArtistStatisticsService artistStatisticsService, IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster) IBroadcastSignalRMessage signalRBroadcaster)
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "wanted/cutoff") : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{ {
_albumCutoffService = albumCutoffService; _albumCutoffService = albumCutoffService;
GetResourcePaged = GetCutoffUnmetAlbums;
} }
private PagingResource<AlbumResource> GetCutoffUnmetAlbums(PagingResource<AlbumResource> pagingResource) [HttpGet]
public PagingResource<AlbumResource> GetCutoffUnmetAlbums(bool includeArtist = false)
{ {
var pagingResource = Request.ReadPagingResourceFromRequest<AlbumResource>();
var pagingSpec = new PagingSpec<Album> var pagingSpec = new PagingSpec<Album>
{ {
Page = pagingResource.Page, Page = pagingResource.Page,
@ -37,7 +40,6 @@ namespace Lidarr.Api.V1.Wanted
SortDirection = pagingResource.SortDirection SortDirection = pagingResource.SortDirection
}; };
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored");
if (filter != null && filter.Value == "false") if (filter != null && filter.Value == "false")
@ -49,9 +51,7 @@ namespace Lidarr.Api.V1.Wanted
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true);
} }
var resource = ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeArtist)); return pagingSpec.ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, v => MapToResource(v, includeArtist));
return resource;
} }
} }
} }

@ -2,6 +2,7 @@ using System.Linq;
using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Albums;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
@ -11,20 +12,22 @@ using NzbDrone.SignalR;
namespace Lidarr.Api.V1.Wanted namespace Lidarr.Api.V1.Wanted
{ {
public class MissingModule : AlbumModuleWithSignalR [V1ApiController("wanted/missing")]
public class MissingController : AlbumControllerWithSignalR
{ {
public MissingModule(IAlbumService albumService, public MissingController(IAlbumService albumService,
IArtistStatisticsService artistStatisticsService, IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster) IBroadcastSignalRMessage signalRBroadcaster)
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "wanted/missing") : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{ {
GetResourcePaged = GetMissingAlbums;
} }
private PagingResource<AlbumResource> GetMissingAlbums(PagingResource<AlbumResource> pagingResource) [HttpGet]
public PagingResource<AlbumResource> GetMissingAlbums(bool includeArtist = false)
{ {
var pagingResource = Request.ReadPagingResourceFromRequest<AlbumResource>();
var pagingSpec = new PagingSpec<Album> var pagingSpec = new PagingSpec<Album>
{ {
Page = pagingResource.Page, Page = pagingResource.Page,
@ -33,7 +36,6 @@ namespace Lidarr.Api.V1.Wanted
SortDirection = pagingResource.SortDirection SortDirection = pagingResource.SortDirection
}; };
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored");
if (monitoredFilter != null && monitoredFilter.Value == "false") if (monitoredFilter != null && monitoredFilter.Value == "false")
@ -45,9 +47,7 @@ namespace Lidarr.Api.V1.Wanted
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true);
} }
var resource = ApplyToPage(_albumService.AlbumsWithoutFiles, pagingSpec, v => MapToResource(v, includeArtist)); return pagingSpec.ApplyToPage(_albumService.AlbumsWithoutFiles, v => MapToResource(v, includeArtist));
return resource;
} }
} }
} }

@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Lidarr.Http.Authentication
{
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
public string HeaderName { get; set; }
public string QueryName { get; set; }
public string ApiKey { get; set; }
}
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
private string ParseApiKey()
{
// Try query parameter
if (Request.Query.TryGetValue(Options.QueryName, out var value))
{
return value.FirstOrDefault();
}
// No ApiKey query parameter found try headers
if (Request.Headers.TryGetValue(Options.HeaderName, out var headerValue))
{
return headerValue.FirstOrDefault();
}
return Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", "");
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var providedApiKey = ParseApiKey();
if (string.IsNullOrWhiteSpace(providedApiKey))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
if (Options.ApiKey == providedApiKey)
{
var claims = new List<Claim>
{
new Claim("ApiKey", "true")
};
var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
var identities = new List<ClaimsIdentity> { identity };
var principal = new ClaimsPrincipal(identities);
var ticket = new AuthenticationTicket(principal, Options.Scheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
return Task.FromResult(AuthenticateResult.NoResult());
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
return Task.CompletedTask;
}
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
return Task.CompletedTask;
}
}
}

@ -0,0 +1,65 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
{
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action<ApiKeyAuthenticationOptions> options)
{
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(name, options);
}
public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder authenticationBuilder)
{
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AuthenticationType.Basic.ToString(), options => { });
}
public static AuthenticationBuilder AddNoAuthentication(this AuthenticationBuilder authenticationBuilder)
{
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(AuthenticationType.None.ToString(), options => { });
}
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services, IConfigFileProvider config)
{
var authBuilder = services.AddAuthentication(config.AuthenticationMethod.ToString());
if (config.AuthenticationMethod == AuthenticationType.Basic)
{
authBuilder.AddBasicAuthentication();
}
else if (config.AuthenticationMethod == AuthenticationType.Forms)
{
authBuilder.AddCookie(AuthenticationType.Forms.ToString(), options =>
{
options.AccessDeniedPath = "/login?loginFailed=true";
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
});
}
else
{
authBuilder.AddNoAuthentication();
}
authBuilder.AddApiKey("API", options =>
{
options.HeaderName = "X-Api-Key";
options.QueryName = "apikey";
options.ApiKey = config.ApiKey;
});
authBuilder.AddApiKey("SignalR", options =>
{
options.HeaderName = "X-Api-Key";
options.QueryName = "access_token";
options.ApiKey = config.ApiKey;
});
return authBuilder;
}
}
}

@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
{
[AllowAnonymous]
[ApiController]
public class AuthenticationController : Controller
{
private readonly IAuthenticationService _authService;
private readonly IConfigFileProvider _configFileProvider;
public AuthenticationController(IAuthenticationService authService, IConfigFileProvider configFileProvider)
{
_authService = authService;
_configFileProvider = configFileProvider;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromForm] LoginResource resource, [FromQuery] string returnUrl = null)
{
var user = _authService.Login(HttpContext.Request, resource.Username, resource.Password);
if (user == null)
{
return Redirect($"~/login?returnUrl={returnUrl}&loginFailed=true");
}
var claims = new List<Claim>
{
new Claim("user", user.Username),
new Claim("identifier", user.Identifier.ToString()),
new Claim("UiAuth", "true")
};
var authProperties = new AuthenticationProperties
{
IsPersistent = resource.RememberMe == "on"
};
await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties);
return Redirect("/");
}
[HttpGet("logout")]
public async Task<IActionResult> Logout()
{
_authService.Logout(HttpContext);
await HttpContext.SignOutAsync();
return Redirect("/");
}
}
}

@ -1,50 +0,0 @@
using System;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
using Nancy.ModelBinding;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
{
public class AuthenticationModule : NancyModule
{
private readonly IAuthenticationService _authService;
private readonly IConfigFileProvider _configFileProvider;
public AuthenticationModule(IAuthenticationService authService, IConfigFileProvider configFileProvider)
{
_authService = authService;
_configFileProvider = configFileProvider;
Post("/login", x => Login(this.Bind<LoginResource>()));
Get("/logout", x => Logout());
}
private Response Login(LoginResource resource)
{
var user = _authService.Login(Context, resource.Username, resource.Password);
if (user == null)
{
var returnUrl = (string)Request.Query.returnUrl;
return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true");
}
DateTime? expiry = null;
if (resource.RememberMe)
{
expiry = DateTime.UtcNow.AddDays(7);
}
return this.LoginAndRedirect(user.Identifier, expiry, _configFileProvider.UrlBase + "/");
}
private Response Logout()
{
_authService.Logout(Context);
return this.LogoutAndRedirect(_configFileProvider.UrlBase + "/");
}
}
}

@ -1,28 +1,16 @@
using System;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Principal;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
using Nancy; using Microsoft.AspNetCore.Http;
using Nancy.Authentication.Basic;
using Nancy.Authentication.Forms;
using Nancy.Routing.Trie.Nodes;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication namespace Lidarr.Http.Authentication
{ {
public interface IAuthenticationService : IUserValidator, IUserMapper public interface IAuthenticationService
{ {
void SetContext(NancyContext context); void LogUnauthorized(HttpRequest context);
User Login(HttpRequest request, string username, string password);
void LogUnauthorized(NancyContext context); void Logout(HttpContext context);
User Login(NancyContext context, string username, string password);
void Logout(NancyContext context);
bool IsAuthenticated(NancyContext context);
} }
public class AuthenticationService : IAuthenticationService public class AuthenticationService : IAuthenticationService
@ -34,9 +22,6 @@ namespace Lidarr.Http.Authentication
private static string API_KEY; private static string API_KEY;
private static AuthenticationType AUTH_METHOD; private static AuthenticationType AUTH_METHOD;
[ThreadStatic]
private static NancyContext _context;
public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService)
{ {
_userService = userService; _userService = userService;
@ -44,13 +29,7 @@ namespace Lidarr.Http.Authentication
AUTH_METHOD = configFileProvider.AuthenticationMethod; AUTH_METHOD = configFileProvider.AuthenticationMethod;
} }
public void SetContext(NancyContext context) public User Login(HttpRequest request, string username, string password)
{
// Validate and GetUserIdentifier don't have access to the NancyContext so get it from the pipeline earlier
_context = context;
}
public User Login(NancyContext context, string username, string password)
{ {
if (AUTH_METHOD == AuthenticationType.None) if (AUTH_METHOD == AuthenticationType.None)
{ {
@ -61,179 +40,50 @@ namespace Lidarr.Http.Authentication
if (user != null) if (user != null)
{ {
LogSuccess(context, username); LogSuccess(request, username);
return user; return user;
} }
LogFailure(context, username); LogFailure(request, username);
return null; return null;
} }
public void Logout(NancyContext context) public void Logout(HttpContext context)
{ {
if (AUTH_METHOD == AuthenticationType.None) if (AUTH_METHOD == AuthenticationType.None)
{ {
return; return;
} }
if (context.CurrentUser != null) if (context.User != null)
{
LogLogout(context, context.CurrentUser.Identity.Name);
}
}
public ClaimsPrincipal Validate(string username, string password)
{
if (AUTH_METHOD == AuthenticationType.None)
{
return new ClaimsPrincipal(new GenericIdentity(AnonymousUser));
}
var user = _userService.FindUser(username, password);
if (user != null)
{
if (AUTH_METHOD != AuthenticationType.Basic)
{
// Don't log success for basic auth
LogSuccess(_context, username);
}
return new ClaimsPrincipal(new GenericIdentity(user.Username));
}
LogFailure(_context, username);
return null;
}
public ClaimsPrincipal GetUserFromIdentifier(Guid identifier, NancyContext context)
{
if (AUTH_METHOD == AuthenticationType.None)
{
return new ClaimsPrincipal(new GenericIdentity(AnonymousUser));
}
var user = _userService.FindUser(identifier);
if (user != null)
{
return new ClaimsPrincipal(new GenericIdentity(user.Username));
}
LogInvalidated(_context);
return null;
}
public bool IsAuthenticated(NancyContext context)
{
var apiKey = GetApiKey(context);
if (context.Request.IsApiRequest())
{
return ValidApiKey(apiKey);
}
if (AUTH_METHOD == AuthenticationType.None)
{
return true;
}
if (context.Request.IsFeedRequest())
{
if (ValidUser(context) || ValidApiKey(apiKey))
{
return true;
}
return false;
}
if (context.Request.IsLoginRequest())
{
return true;
}
if (context.Request.IsContentRequest())
{
return true;
}
if (context.Request.IsBundledJsRequest())
{
return true;
}
if (ValidUser(context))
{
return true;
}
return false;
}
private bool ValidUser(NancyContext context)
{
if (context.CurrentUser != null)
{ {
return true; LogLogout(context.Request, context.User.Identity.Name);
} }
return false;
}
private bool ValidApiKey(string apiKey)
{
if (API_KEY.Equals(apiKey))
{
return true;
}
return false;
}
private string GetApiKey(NancyContext context)
{
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
var apiKeyQueryString = context.Request.Query["ApiKey"];
if (!apiKeyHeader.IsNullOrWhiteSpace())
{
return apiKeyHeader;
}
if (apiKeyQueryString.HasValue)
{
return apiKeyQueryString.Value;
}
return context.Request.Headers.Authorization;
} }
public void LogUnauthorized(NancyContext context) public void LogUnauthorized(HttpRequest context)
{ {
_authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Request.Url.ToString()); _authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Path);
} }
private void LogInvalidated(NancyContext context) private void LogInvalidated(HttpRequest context)
{ {
_authLogger.Info("Auth-Invalidated ip {0}", context.GetRemoteIP()); _authLogger.Info("Auth-Invalidated ip {0}", context.GetRemoteIP());
} }
private void LogFailure(NancyContext context, string username) private void LogFailure(HttpRequest context, string username)
{ {
_authLogger.Warn("Auth-Failure ip {0} username '{1}'", context.GetRemoteIP(), username); _authLogger.Warn("Auth-Failure ip {0} username '{1}'", context.GetRemoteIP(), username);
} }
private void LogSuccess(NancyContext context, string username) private void LogSuccess(HttpRequest context, string username)
{ {
_authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username); _authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username);
} }
private void LogLogout(NancyContext context, string username) private void LogLogout(HttpRequest context, string username)
{ {
_authLogger.Info("Auth-Logout ip {0} username '{1}'", context.GetRemoteIP(), username); _authLogger.Info("Auth-Logout ip {0} username '{1}'", context.GetRemoteIP(), username);
} }

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NzbDrone.Common.EnvironmentInfo;
namespace Lidarr.Http.Authentication
{
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IAuthenticationService _authService;
public BasicAuthenticationHandler(IAuthenticationService authService,
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
_authService = authService;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
}
// Get authorization key
var authorizationHeader = Request.Headers["Authorization"].ToString();
var authHeaderRegex = new Regex(@"Basic (.*)");
if (!authHeaderRegex.IsMatch(authorizationHeader))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly."));
}
var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1")));
var authSplit = authBase64.Split(':', 2);
var authUsername = authSplit[0];
var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password");
var user = _authService.Login(Request, authUsername, authPassword);
if (user == null)
{
return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct."));
}
var claims = new List<Claim>
{
new Claim("user", user.Username),
new Claim("identifier", user.Identifier.ToString()),
new Claim("UiAuth", "true")
};
var identity = new ClaimsIdentity(claims, "Basic", "user", "identifier");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Basic");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers.Add("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\"");
Response.StatusCode = 401;
return Task.CompletedTask;
}
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
return Task.CompletedTask;
}
}
}

@ -1,142 +0,0 @@
using System;
using System.Text;
using Lidarr.Http.Extensions;
using Lidarr.Http.Extensions.Pipelines;
using Nancy;
using Nancy.Authentication.Basic;
using Nancy.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.Cryptography;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
{
public class EnableAuthInNancy : IRegisterNancyPipeline
{
private readonly IAuthenticationService _authenticationService;
private readonly IConfigService _configService;
private readonly IConfigFileProvider _configFileProvider;
private FormsAuthenticationConfiguration _formsAuthConfig;
public EnableAuthInNancy(IAuthenticationService authenticationService,
IConfigService configService,
IConfigFileProvider configFileProvider)
{
_authenticationService = authenticationService;
_configService = configService;
_configFileProvider = configFileProvider;
}
public int Order => 10;
public void Register(IPipelines pipelines)
{
if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms)
{
RegisterFormsAuth(pipelines);
pipelines.AfterRequest.AddItemToEndOfPipeline(SlidingAuthenticationForFormsAuth);
}
else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic)
{
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, BuildInfo.AppName));
pipelines.BeforeRequest.AddItemToStartOfPipeline(CaptureContext);
}
pipelines.BeforeRequest.AddItemToEndOfPipeline(RequiresAuthentication);
pipelines.AfterRequest.AddItemToEndOfPipeline(RemoveLoginHooksForApiCalls);
}
private Response CaptureContext(NancyContext context)
{
_authenticationService.SetContext(context);
return null;
}
private Response RequiresAuthentication(NancyContext context)
{
Response response = null;
if (!_authenticationService.IsAuthenticated(context))
{
_authenticationService.LogUnauthorized(context);
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
}
return response;
}
private void RegisterFormsAuth(IPipelines pipelines)
{
FormsAuthentication.FormsAuthenticationCookieName = "LidarrAuth";
var cryptographyConfiguration = new CryptographyConfiguration(
new AesEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))),
new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))));
_formsAuthConfig = new FormsAuthenticationConfiguration
{
RedirectUrl = _configFileProvider.UrlBase + "/login",
UserMapper = _authenticationService,
Path = GetCookiePath(),
CryptographyConfiguration = cryptographyConfiguration
};
FormsAuthentication.Enable(pipelines, _formsAuthConfig);
}
private void RemoveLoginHooksForApiCalls(NancyContext context)
{
if (context.Request.IsApiRequest())
{
if ((context.Response.StatusCode == HttpStatusCode.SeeOther &&
context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) ||
context.Response.StatusCode == HttpStatusCode.Unauthorized)
{
context.Response = new { Error = "Unauthorized" }.AsResponse(context, HttpStatusCode.Unauthorized);
}
}
}
private void SlidingAuthenticationForFormsAuth(NancyContext context)
{
if (context.CurrentUser == null)
{
return;
}
var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName;
if (!context.Request.Path.Equals("/logout") &&
context.Request.Cookies.ContainsKey(formsAuthCookieName))
{
var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName];
if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, _formsAuthConfig).IsNotNullOrWhiteSpace())
{
var formsAuthCookie = new LidarrNancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7))
{
Path = GetCookiePath()
};
context.Response.WithCookie(formsAuthCookie);
}
}
}
private string GetCookiePath()
{
var urlBase = _configFileProvider.UrlBase;
if (urlBase.IsNullOrWhiteSpace())
{
return "/";
}
return urlBase;
}
}
}

@ -1,38 +0,0 @@
using System;
using Nancy.Cookies;
namespace Lidarr.Http.Authentication
{
public class LidarrNancyCookie : NancyCookie
{
public LidarrNancyCookie(string name, string value)
: base(name, value)
{
}
public LidarrNancyCookie(string name, string value, DateTime expires)
: base(name, value, expires)
{
}
public LidarrNancyCookie(string name, string value, bool httpOnly)
: base(name, value, httpOnly)
{
}
public LidarrNancyCookie(string name, string value, bool httpOnly, bool secure)
: base(name, value, httpOnly, secure)
{
}
public LidarrNancyCookie(string name, string value, bool httpOnly, bool secure, DateTime? expires)
: base(name, value, httpOnly, secure, expires)
{
}
public override string ToString()
{
return base.ToString() + "; SameSite=Lax";
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save