mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-17 21:25:39 -04:00
New: Lidarr to Readarr
This commit is contained in:
12
src/Readarr.Api.V1/AlbumStudio/AlbumStudioArtistResource.cs
Normal file
12
src/Readarr.Api.V1/AlbumStudio/AlbumStudioArtistResource.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using Readarr.Api.V1.Albums;
|
||||
|
||||
namespace Readarr.Api.V1.AlbumStudio
|
||||
{
|
||||
public class AlbumStudioArtistResource
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool? Monitored { get; set; }
|
||||
public List<AlbumResource> Albums { get; set; }
|
||||
}
|
||||
}
|
||||
47
src/Readarr.Api.V1/AlbumStudio/AlbumStudioModule.cs
Normal file
47
src/Readarr.Api.V1/AlbumStudio/AlbumStudioModule.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Music;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.AlbumStudio
|
||||
{
|
||||
public class AlbumStudioModule : ReadarrV1Module
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumMonitoredService _albumMonitoredService;
|
||||
|
||||
public AlbumStudioModule(IArtistService artistService, IAlbumMonitoredService albumMonitoredService)
|
||||
: base("/albumstudio")
|
||||
{
|
||||
_artistService = artistService;
|
||||
_albumMonitoredService = albumMonitoredService;
|
||||
Post("/", artist => UpdateAll());
|
||||
}
|
||||
|
||||
private object UpdateAll()
|
||||
{
|
||||
//Read from request
|
||||
var request = Request.Body.FromJson<AlbumStudioResource>();
|
||||
var artistToUpdate = _artistService.GetArtists(request.Artist.Select(s => s.Id));
|
||||
|
||||
foreach (var s in request.Artist)
|
||||
{
|
||||
var artist = artistToUpdate.Single(c => c.Id == s.Id);
|
||||
|
||||
if (s.Monitored.HasValue)
|
||||
{
|
||||
artist.Monitored = s.Monitored.Value;
|
||||
}
|
||||
|
||||
if (request.MonitoringOptions != null && request.MonitoringOptions.Monitor == MonitorTypes.None)
|
||||
{
|
||||
artist.Monitored = false;
|
||||
}
|
||||
|
||||
_albumMonitoredService.SetAlbumMonitoredStatus(artist, request.MonitoringOptions);
|
||||
}
|
||||
|
||||
return ResponseWithCode("ok", HttpStatusCode.Accepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/Readarr.Api.V1/AlbumStudio/AlbumStudioResource.cs
Normal file
11
src/Readarr.Api.V1/AlbumStudio/AlbumStudioResource.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace Readarr.Api.V1.AlbumStudio
|
||||
{
|
||||
public class AlbumStudioResource
|
||||
{
|
||||
public List<AlbumStudioArtistResource> Artist { get; set; }
|
||||
public MonitoringOptions MonitoringOptions { get; set; }
|
||||
}
|
||||
}
|
||||
42
src/Readarr.Api.V1/Albums/AlbumLookupModule.cs
Normal file
42
src/Readarr.Api.V1/Albums/AlbumLookupModule.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumLookupModule : ReadarrRestModule<AlbumResource>
|
||||
{
|
||||
private readonly ISearchForNewAlbum _searchProxy;
|
||||
|
||||
public AlbumLookupModule(ISearchForNewAlbum searchProxy)
|
||||
: base("/album/lookup")
|
||||
{
|
||||
_searchProxy = searchProxy;
|
||||
Get("/", x => Search());
|
||||
}
|
||||
|
||||
private object Search()
|
||||
{
|
||||
var searchResults = _searchProxy.SearchForNewAlbum((string)Request.Query.term, null);
|
||||
return MapToResource(searchResults).ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<AlbumResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Album> albums)
|
||||
{
|
||||
foreach (var currentAlbum in albums)
|
||||
{
|
||||
var resource = currentAlbum.ToResource();
|
||||
var cover = currentAlbum.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Cover);
|
||||
if (cover != null)
|
||||
{
|
||||
resource.RemoteCover = cover.Url;
|
||||
}
|
||||
|
||||
yield return resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
215
src/Readarr.Api.V1/Albums/AlbumModule.cs
Normal file
215
src/Readarr.Api.V1/Albums/AlbumModule.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ArtistStats;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumModule : AlbumModuleWithSignalR,
|
||||
IHandle<AlbumGrabbedEvent>,
|
||||
IHandle<AlbumEditedEvent>,
|
||||
IHandle<AlbumUpdatedEvent>,
|
||||
IHandle<AlbumImportedEvent>,
|
||||
IHandle<TrackImportedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>
|
||||
{
|
||||
protected readonly IArtistService _artistService;
|
||||
protected readonly IReleaseService _releaseService;
|
||||
protected readonly IAddAlbumService _addAlbumService;
|
||||
|
||||
public AlbumModule(IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IAddAlbumService addAlbumService,
|
||||
IReleaseService releaseService,
|
||||
IArtistStatisticsService artistStatisticsService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
|
||||
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_releaseService = releaseService;
|
||||
_addAlbumService = addAlbumService;
|
||||
|
||||
GetResourceAll = GetAlbums;
|
||||
CreateResource = AddAlbum;
|
||||
UpdateResource = UpdateAlbum;
|
||||
DeleteResource = DeleteAlbum;
|
||||
Put("/monitor", x => SetAlbumsMonitored());
|
||||
|
||||
PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator);
|
||||
PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
|
||||
PostValidator.RuleFor(s => s.Artist.RootFolderPath).IsValidPath().When(s => s.Artist.Path.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty();
|
||||
}
|
||||
|
||||
private List<AlbumResource> GetAlbums()
|
||||
{
|
||||
var artistIdQuery = Request.Query.ArtistId;
|
||||
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 artists = _artistService.GetAllArtists().ToDictionary(x => x.ArtistMetadataId);
|
||||
var releases = _releaseService.GetAllReleases().GroupBy(x => x.AlbumId).ToDictionary(x => x.Key, y => y.ToList());
|
||||
|
||||
foreach (var album in albums)
|
||||
{
|
||||
album.Artist = artists[album.ArtistMetadataId];
|
||||
if (releases.TryGetValue(album.Id, out var albumReleases))
|
||||
{
|
||||
album.AlbumReleases = albumReleases;
|
||||
}
|
||||
else
|
||||
{
|
||||
album.AlbumReleases = new List<AlbumRelease>();
|
||||
}
|
||||
}
|
||||
|
||||
return MapToResource(albums, false);
|
||||
}
|
||||
|
||||
if (artistIdQuery.HasValue)
|
||||
{
|
||||
int artistId = Convert.ToInt32(artistIdQuery.Value);
|
||||
|
||||
return MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
|
||||
}
|
||||
|
||||
if (foreignIdQuery.HasValue)
|
||||
{
|
||||
string foreignAlbumId = foreignIdQuery.Value.ToString();
|
||||
|
||||
var album = _albumService.FindById(foreignAlbumId);
|
||||
|
||||
if (album == null)
|
||||
{
|
||||
return MapToResource(new List<Album>(), false);
|
||||
}
|
||||
|
||||
if (includeAllArtistAlbumsQuery.HasValue && Convert.ToBoolean(includeAllArtistAlbumsQuery.Value))
|
||||
{
|
||||
return MapToResource(_albumService.GetAlbumsByArtist(album.ArtistId), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MapToResource(new List<Album> { album }, false);
|
||||
}
|
||||
}
|
||||
|
||||
string albumIdsValue = albumIdsQuery.Value.ToString();
|
||||
|
||||
var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(e => Convert.ToInt32(e))
|
||||
.ToList();
|
||||
|
||||
return MapToResource(_albumService.GetAlbums(albumIds), false);
|
||||
}
|
||||
|
||||
private int AddAlbum(AlbumResource albumResource)
|
||||
{
|
||||
var album = _addAlbumService.AddAlbum(albumResource.ToModel());
|
||||
|
||||
return album.Id;
|
||||
}
|
||||
|
||||
private void UpdateAlbum(AlbumResource albumResource)
|
||||
{
|
||||
var album = _albumService.GetAlbum(albumResource.Id);
|
||||
|
||||
var model = albumResource.ToModel(album);
|
||||
|
||||
_albumService.UpdateAlbum(model);
|
||||
_releaseService.UpdateMany(model.AlbumReleases.Value);
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, model.Id);
|
||||
}
|
||||
|
||||
private void DeleteAlbum(int id)
|
||||
{
|
||||
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
|
||||
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
|
||||
|
||||
_albumService.DeleteAlbum(id, deleteFiles, addImportListExclusion);
|
||||
}
|
||||
|
||||
private object SetAlbumsMonitored()
|
||||
{
|
||||
var resource = Request.Body.FromJson<AlbumsMonitoredResource>();
|
||||
|
||||
_albumService.SetMonitored(resource.AlbumIds, resource.Monitored);
|
||||
|
||||
return ResponseWithCode(MapToResource(_albumService.GetAlbums(resource.AlbumIds), false), HttpStatusCode.Accepted);
|
||||
}
|
||||
|
||||
public void Handle(AlbumGrabbedEvent message)
|
||||
{
|
||||
foreach (var album in message.Album.Albums)
|
||||
{
|
||||
var resource = album.ToResource();
|
||||
resource.Grabbed = true;
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(AlbumEditedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
|
||||
}
|
||||
|
||||
public void Handle(AlbumUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
|
||||
}
|
||||
|
||||
public void Handle(AlbumDeletedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Deleted, message.Album.ToResource());
|
||||
}
|
||||
|
||||
public void Handle(AlbumImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
|
||||
}
|
||||
|
||||
public void Handle(TrackImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.TrackInfo.Album.ToResource());
|
||||
}
|
||||
|
||||
public void Handle(TrackFileDeletedEvent message)
|
||||
{
|
||||
if (message.Reason == DeleteMediaFileReason.Upgrade)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.TrackFile.Album.Value, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/Readarr.Api.V1/Albums/AlbumModuleWithSignalR.cs
Normal file
133
src/Readarr.Api.V1/Albums/AlbumModuleWithSignalR.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ArtistStats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public abstract class AlbumModuleWithSignalR : ReadarrRestModuleWithSignalR<AlbumResource, Album>
|
||||
{
|
||||
protected readonly IAlbumService _albumService;
|
||||
protected readonly IArtistStatisticsService _artistStatisticsService;
|
||||
protected readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
protected readonly IMapCoversToLocal _coverMapper;
|
||||
|
||||
protected AlbumModuleWithSignalR(IAlbumService albumService,
|
||||
IArtistStatisticsService artistStatisticsService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_albumService = albumService;
|
||||
_artistStatisticsService = artistStatisticsService;
|
||||
_coverMapper = coverMapper;
|
||||
_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)
|
||||
{
|
||||
var album = _albumService.GetAlbum(id);
|
||||
var resource = MapToResource(album, true);
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected AlbumResource MapToResource(Album album, bool includeArtist)
|
||||
{
|
||||
var resource = album.ToResource();
|
||||
|
||||
if (includeArtist)
|
||||
{
|
||||
var artist = album.Artist.Value;
|
||||
|
||||
resource.Artist = artist.ToResource();
|
||||
}
|
||||
|
||||
FetchAndLinkAlbumStatistics(resource);
|
||||
MapCoversToLocal(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected List<AlbumResource> MapToResource(List<Album> albums, bool includeArtist)
|
||||
{
|
||||
var result = albums.ToResource();
|
||||
|
||||
if (includeArtist)
|
||||
{
|
||||
var artistDict = new Dictionary<int, NzbDrone.Core.Music.Artist>();
|
||||
for (var i = 0; i < albums.Count; i++)
|
||||
{
|
||||
var album = albums[i];
|
||||
var resource = result[i];
|
||||
var artist = artistDict.GetValueOrDefault(albums[i].ArtistMetadataId) ?? album.Artist?.Value;
|
||||
artistDict[artist.ArtistMetadataId] = artist;
|
||||
|
||||
resource.Artist = artist.ToResource();
|
||||
}
|
||||
}
|
||||
|
||||
var artistStats = _artistStatisticsService.ArtistStatistics();
|
||||
LinkArtistStatistics(result, artistStats);
|
||||
MapCoversToLocal(result.ToArray());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void FetchAndLinkAlbumStatistics(AlbumResource resource)
|
||||
{
|
||||
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.ArtistId));
|
||||
}
|
||||
|
||||
private void LinkArtistStatistics(List<AlbumResource> resources, List<ArtistStatistics> artistStatistics)
|
||||
{
|
||||
foreach (var album in resources)
|
||||
{
|
||||
var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == album.ArtistId);
|
||||
LinkArtistStatistics(album, stats);
|
||||
}
|
||||
}
|
||||
|
||||
private void LinkArtistStatistics(AlbumResource resource, ArtistStatistics artistStatistics)
|
||||
{
|
||||
if (artistStatistics?.AlbumStatistics != null)
|
||||
{
|
||||
var dictAlbumStats = artistStatistics.AlbumStatistics.ToDictionary(v => v.AlbumId);
|
||||
|
||||
resource.Statistics = dictAlbumStats.GetValueOrDefault(resource.Id).ToResource();
|
||||
}
|
||||
}
|
||||
|
||||
private void MapCoversToLocal(params AlbumResource[] albums)
|
||||
{
|
||||
foreach (var albumResource in albums)
|
||||
{
|
||||
_coverMapper.ConvertToLocalUrls(albumResource.Id, MediaCoverEntity.Album, albumResource.Images);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/Readarr.Api.V1/Albums/AlbumReleaseResource.cs
Normal file
107
src/Readarr.Api.V1/Albums/AlbumReleaseResource.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumReleaseResource
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AlbumId { get; set; }
|
||||
public string ForeignReleaseId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Status { get; set; }
|
||||
public int Duration { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public List<MediumResource> Media { get; set; }
|
||||
public int MediumCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Media == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Media.Where(s => s.MediumNumber > 0).Count();
|
||||
}
|
||||
}
|
||||
|
||||
public string Disambiguation { get; set; }
|
||||
public List<string> Country { get; set; }
|
||||
public List<string> Label { get; set; }
|
||||
public string Format { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
}
|
||||
|
||||
public static class AlbumReleaseResourceMapper
|
||||
{
|
||||
public static AlbumReleaseResource ToResource(this AlbumRelease model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AlbumReleaseResource
|
||||
{
|
||||
Id = model.Id,
|
||||
AlbumId = model.AlbumId,
|
||||
ForeignReleaseId = model.ForeignReleaseId,
|
||||
Title = model.Title,
|
||||
Status = model.Status,
|
||||
Duration = model.Duration,
|
||||
TrackCount = model.TrackCount,
|
||||
Media = model.Media.ToResource(),
|
||||
Disambiguation = model.Disambiguation,
|
||||
Country = model.Country,
|
||||
Label = model.Label,
|
||||
Monitored = model.Monitored,
|
||||
Format = string.Join(", ",
|
||||
model.Media.OrderBy(x => x.Number)
|
||||
.GroupBy(x => x.Format)
|
||||
.Select(g => MediaFormatHelper(g.Key, g.Count()))
|
||||
.ToList())
|
||||
};
|
||||
}
|
||||
|
||||
public static AlbumRelease ToModel(this AlbumReleaseResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AlbumRelease
|
||||
{
|
||||
Id = resource.Id,
|
||||
AlbumId = resource.AlbumId,
|
||||
ForeignReleaseId = resource.ForeignReleaseId,
|
||||
Title = resource.Title,
|
||||
Status = resource.Status,
|
||||
Duration = resource.Duration,
|
||||
Label = resource.Label,
|
||||
Disambiguation = resource.Disambiguation,
|
||||
Country = resource.Country,
|
||||
Media = resource.Media.ToModel(),
|
||||
TrackCount = resource.TrackCount,
|
||||
Monitored = resource.Monitored
|
||||
};
|
||||
}
|
||||
|
||||
private static string MediaFormatHelper(string name, int count)
|
||||
{
|
||||
return count == 1 ? name : string.Join("x", new List<string> { count.ToString(), name });
|
||||
}
|
||||
|
||||
public static List<AlbumReleaseResource> ToResource(this IEnumerable<AlbumRelease> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<AlbumRelease> ToModel(this IEnumerable<AlbumReleaseResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/Readarr.Api.V1/Albums/AlbumResource.cs
Normal file
138
src/Readarr.Api.V1/Albums/AlbumResource.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Music;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumResource : RestResource
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Disambiguation { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public int ArtistId { get; set; }
|
||||
public string ForeignAlbumId { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public bool AnyReleaseOk { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
public int Duration { get; set; }
|
||||
public string AlbumType { get; set; }
|
||||
public List<string> SecondaryTypes { get; set; }
|
||||
public int MediumCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Media == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Media.Where(s => s.MediumNumber > 0).Count();
|
||||
}
|
||||
}
|
||||
|
||||
public Ratings Ratings { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public List<AlbumReleaseResource> Releases { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public List<MediumResource> Media { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
public List<MediaCover> Images { get; set; }
|
||||
public List<Links> Links { get; set; }
|
||||
public AlbumStatisticsResource Statistics { get; set; }
|
||||
public AddAlbumOptions AddOptions { get; set; }
|
||||
public string RemoteCover { get; set; }
|
||||
|
||||
//Hiding this so people don't think its usable (only used to set the initial state)
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool Grabbed { get; set; }
|
||||
}
|
||||
|
||||
public static class AlbumResourceMapper
|
||||
{
|
||||
public static AlbumResource ToResource(this Album model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var selectedRelease = model.AlbumReleases?.Value.Where(x => x.Monitored).SingleOrDefault();
|
||||
|
||||
return new AlbumResource
|
||||
{
|
||||
Id = model.Id,
|
||||
ArtistId = model.ArtistId,
|
||||
ForeignAlbumId = model.ForeignAlbumId,
|
||||
ProfileId = model.ProfileId,
|
||||
Monitored = model.Monitored,
|
||||
AnyReleaseOk = model.AnyReleaseOk,
|
||||
ReleaseDate = model.ReleaseDate,
|
||||
Genres = model.Genres,
|
||||
Title = model.Title,
|
||||
Disambiguation = model.Disambiguation,
|
||||
Overview = model.Overview,
|
||||
Images = model.Images,
|
||||
Links = model.Links,
|
||||
Ratings = model.Ratings,
|
||||
Duration = selectedRelease?.Duration ?? 0,
|
||||
AlbumType = model.AlbumType,
|
||||
SecondaryTypes = model.SecondaryTypes.Select(s => s.Name).ToList(),
|
||||
Releases = model.AlbumReleases?.Value.ToResource() ?? new List<AlbumReleaseResource>(),
|
||||
Media = selectedRelease?.Media.ToResource() ?? new List<MediumResource>(),
|
||||
Artist = model.Artist?.Value.ToResource()
|
||||
};
|
||||
}
|
||||
|
||||
public static Album ToModel(this AlbumResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var artist = resource.Artist?.ToModel() ?? new NzbDrone.Core.Music.Artist();
|
||||
|
||||
return new Album
|
||||
{
|
||||
Id = resource.Id,
|
||||
ForeignAlbumId = resource.ForeignAlbumId,
|
||||
Title = resource.Title,
|
||||
Disambiguation = resource.Disambiguation,
|
||||
Overview = resource.Overview,
|
||||
Images = resource.Images,
|
||||
AlbumType = resource.AlbumType,
|
||||
Monitored = resource.Monitored,
|
||||
AnyReleaseOk = resource.AnyReleaseOk,
|
||||
AlbumReleases = resource.Releases.ToModel(),
|
||||
AddOptions = resource.AddOptions,
|
||||
Artist = artist,
|
||||
ArtistMetadata = artist.Metadata.Value
|
||||
};
|
||||
}
|
||||
|
||||
public static Album ToModel(this AlbumResource resource, Album album)
|
||||
{
|
||||
var updatedAlbum = resource.ToModel();
|
||||
|
||||
album.ApplyChanges(updatedAlbum);
|
||||
album.AlbumReleases = updatedAlbum.AlbumReleases;
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
public static List<AlbumResource> ToResource(this IEnumerable<Album> models)
|
||||
{
|
||||
return models?.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<Album> ToModel(this IEnumerable<AlbumResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Readarr.Api.V1/Albums/AlbumStatisticsResource.cs
Normal file
44
src/Readarr.Api.V1/Albums/AlbumStatisticsResource.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using NzbDrone.Core.ArtistStats;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumStatisticsResource
|
||||
{
|
||||
public int TrackFileCount { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public int TotalTrackCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
|
||||
public decimal PercentOfTracks
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TrackCount == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return TrackFileCount / (decimal)TrackCount * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AlbumStatisticsResourceMapper
|
||||
{
|
||||
public static AlbumStatisticsResource ToResource(this AlbumStatistics model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AlbumStatisticsResource
|
||||
{
|
||||
TrackFileCount = model.TrackFileCount,
|
||||
TrackCount = model.TrackCount,
|
||||
TotalTrackCount = model.TotalTrackCount,
|
||||
SizeOnDisk = model.SizeOnDisk
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Readarr.Api.V1/Albums/AlbumsMonitoredResource.cs
Normal file
10
src/Readarr.Api.V1/Albums/AlbumsMonitoredResource.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumsMonitoredResource
|
||||
{
|
||||
public List<int> AlbumIds { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
}
|
||||
}
|
||||
56
src/Readarr.Api.V1/Albums/MediumResource.cs
Normal file
56
src/Readarr.Api.V1/Albums/MediumResource.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace Readarr.Api.V1.Albums
|
||||
{
|
||||
public class MediumResource
|
||||
{
|
||||
public int MediumNumber { get; set; }
|
||||
public string MediumName { get; set; }
|
||||
public string MediumFormat { get; set; }
|
||||
}
|
||||
|
||||
public static class MediumResourceMapper
|
||||
{
|
||||
public static MediumResource ToResource(this Medium model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MediumResource
|
||||
{
|
||||
MediumNumber = model.Number,
|
||||
MediumName = model.Name,
|
||||
MediumFormat = model.Format
|
||||
};
|
||||
}
|
||||
|
||||
public static Medium ToModel(this MediumResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Medium
|
||||
{
|
||||
Number = resource.MediumNumber,
|
||||
Name = resource.MediumName,
|
||||
Format = resource.MediumFormat
|
||||
};
|
||||
}
|
||||
|
||||
public static List<MediumResource> ToResource(this IEnumerable<Medium> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<Medium> ToModel(this IEnumerable<MediumResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Readarr.Api.V1/Artist/AlternateTitleResource.cs
Normal file
9
src/Readarr.Api.V1/Artist/AlternateTitleResource.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class AlternateTitleResource
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public int? SeasonNumber { get; set; }
|
||||
public int? SceneSeasonNumber { get; set; }
|
||||
}
|
||||
}
|
||||
10
src/Readarr.Api.V1/Artist/ArtistEditorDeleteResource.cs
Normal file
10
src/Readarr.Api.V1/Artist/ArtistEditorDeleteResource.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistEditorDeleteResource
|
||||
{
|
||||
public List<int> ArtistIds { get; set; }
|
||||
public bool DeleteFiles { get; set; }
|
||||
}
|
||||
}
|
||||
110
src/Readarr.Api.V1/Artist/ArtistEditorModule.cs
Normal file
110
src/Readarr.Api.V1/Artist/ArtistEditorModule.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Music.Commands;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistEditorModule : ReadarrV1Module
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public ArtistEditorModule(IArtistService artistService, IManageCommandQueue commandQueueManager)
|
||||
: base("/artist/editor")
|
||||
{
|
||||
_artistService = artistService;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
Put("/", artist => SaveAll());
|
||||
Delete("/", artist => DeleteArtist());
|
||||
}
|
||||
|
||||
private object SaveAll()
|
||||
{
|
||||
var resource = Request.Body.FromJson<ArtistEditorResource>();
|
||||
var artistToUpdate = _artistService.GetArtists(resource.ArtistIds);
|
||||
var artistToMove = new List<BulkMoveArtist>();
|
||||
|
||||
foreach (var artist in artistToUpdate)
|
||||
{
|
||||
if (resource.Monitored.HasValue)
|
||||
{
|
||||
artist.Monitored = resource.Monitored.Value;
|
||||
}
|
||||
|
||||
if (resource.QualityProfileId.HasValue)
|
||||
{
|
||||
artist.QualityProfileId = resource.QualityProfileId.Value;
|
||||
}
|
||||
|
||||
if (resource.MetadataProfileId.HasValue)
|
||||
{
|
||||
artist.MetadataProfileId = resource.MetadataProfileId.Value;
|
||||
}
|
||||
|
||||
if (resource.AlbumFolder.HasValue)
|
||||
{
|
||||
artist.AlbumFolder = resource.AlbumFolder.Value;
|
||||
}
|
||||
|
||||
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
artist.RootFolderPath = resource.RootFolderPath;
|
||||
artistToMove.Add(new BulkMoveArtist
|
||||
{
|
||||
ArtistId = artist.Id,
|
||||
SourcePath = artist.Path
|
||||
});
|
||||
}
|
||||
|
||||
if (resource.Tags != null)
|
||||
{
|
||||
var newTags = resource.Tags;
|
||||
var applyTags = resource.ApplyTags;
|
||||
|
||||
switch (applyTags)
|
||||
{
|
||||
case ApplyTags.Add:
|
||||
newTags.ForEach(t => artist.Tags.Add(t));
|
||||
break;
|
||||
case ApplyTags.Remove:
|
||||
newTags.ForEach(t => artist.Tags.Remove(t));
|
||||
break;
|
||||
case ApplyTags.Replace:
|
||||
artist.Tags = new HashSet<int>(newTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.MoveFiles && artistToMove.Any())
|
||||
{
|
||||
_commandQueueManager.Push(new BulkMoveArtistCommand
|
||||
{
|
||||
DestinationRootFolder = resource.RootFolderPath,
|
||||
Artist = artistToMove
|
||||
});
|
||||
}
|
||||
|
||||
return ResponseWithCode(_artistService.UpdateArtists(artistToUpdate, !resource.MoveFiles)
|
||||
.ToResource(),
|
||||
HttpStatusCode.Accepted);
|
||||
}
|
||||
|
||||
private object DeleteArtist()
|
||||
{
|
||||
var resource = Request.Body.FromJson<ArtistEditorResource>();
|
||||
|
||||
foreach (var artistId in resource.ArtistIds)
|
||||
{
|
||||
_artistService.DeleteArtist(artistId, false);
|
||||
}
|
||||
|
||||
return new object();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Readarr.Api.V1/Artist/ArtistEditorResource.cs
Normal file
24
src/Readarr.Api.V1/Artist/ArtistEditorResource.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistEditorResource
|
||||
{
|
||||
public List<int> ArtistIds { get; set; }
|
||||
public bool? Monitored { get; set; }
|
||||
public int? QualityProfileId { get; set; }
|
||||
public int? MetadataProfileId { get; set; }
|
||||
public bool? AlbumFolder { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
public ApplyTags ApplyTags { get; set; }
|
||||
public bool MoveFiles { get; set; }
|
||||
}
|
||||
|
||||
public enum ApplyTags
|
||||
{
|
||||
Add,
|
||||
Remove,
|
||||
Replace
|
||||
}
|
||||
}
|
||||
28
src/Readarr.Api.V1/Artist/ArtistImportModule.cs
Normal file
28
src/Readarr.Api.V1/Artist/ArtistImportModule.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Music;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistImportModule : ReadarrRestModule<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/Readarr.Api.V1/Artist/ArtistLookupModule.cs
Normal file
42
src/Readarr.Api.V1/Artist/ArtistLookupModule.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistLookupModule : ReadarrRestModule<ArtistResource>
|
||||
{
|
||||
private readonly ISearchForNewArtist _searchProxy;
|
||||
|
||||
public ArtistLookupModule(ISearchForNewArtist searchProxy)
|
||||
: base("/artist/lookup")
|
||||
{
|
||||
_searchProxy = searchProxy;
|
||||
Get("/", x => Search());
|
||||
}
|
||||
|
||||
private object Search()
|
||||
{
|
||||
var searchResults = _searchProxy.SearchForNewArtist((string)Request.Query.term);
|
||||
return MapToResource(searchResults).ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<ArtistResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Artist> artist)
|
||||
{
|
||||
foreach (var currentArtist in artist)
|
||||
{
|
||||
var resource = currentArtist.ToResource();
|
||||
var poster = currentArtist.Metadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
|
||||
if (poster != null)
|
||||
{
|
||||
resource.RemotePoster = poster.Url;
|
||||
}
|
||||
|
||||
yield return resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
src/Readarr.Api.V1/Artist/ArtistModule.cs
Normal file
287
src/Readarr.Api.V1/Artist/ArtistModule.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ArtistStats;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Music.Commands;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistModule : ReadarrRestModuleWithSignalR<ArtistResource, NzbDrone.Core.Music.Artist>,
|
||||
IHandle<AlbumImportedEvent>,
|
||||
IHandle<AlbumEditedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>,
|
||||
IHandle<ArtistUpdatedEvent>,
|
||||
IHandle<ArtistEditedEvent>,
|
||||
IHandle<ArtistDeletedEvent>,
|
||||
IHandle<ArtistRenamedEvent>,
|
||||
IHandle<MediaCoversUpdatedEvent>
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IAddArtistService _addArtistService;
|
||||
private readonly IArtistStatisticsService _artistStatisticsService;
|
||||
private readonly IMapCoversToLocal _coverMapper;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
|
||||
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IAddArtistService addArtistService,
|
||||
IArtistStatisticsService artistStatisticsService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
IRootFolderService rootFolderService,
|
||||
RootFolderValidator rootFolderValidator,
|
||||
MappedNetworkDriveValidator mappedNetworkDriveValidator,
|
||||
ArtistPathValidator artistPathValidator,
|
||||
ArtistExistsValidator artistExistsValidator,
|
||||
ArtistAncestorValidator artistAncestorValidator,
|
||||
SystemFolderValidator systemFolderValidator,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_albumService = albumService;
|
||||
_addArtistService = addArtistService;
|
||||
_artistStatisticsService = artistStatisticsService;
|
||||
|
||||
_coverMapper = coverMapper;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_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.MetadataProfileId));
|
||||
|
||||
SharedValidator.RuleFor(s => s.Path)
|
||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(artistPathValidator)
|
||||
.SetValidator(artistAncestorValidator)
|
||||
.SetValidator(systemFolderValidator)
|
||||
.When(s => !s.Path.IsNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(qualityProfileExistsValidator);
|
||||
SharedValidator.RuleFor(s => s.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
|
||||
|
||||
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.ArtistName).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.ForeignArtistId).NotEmpty().SetValidator(artistExistsValidator);
|
||||
|
||||
PutValidator.RuleFor(s => s.Path).IsValidPath();
|
||||
}
|
||||
|
||||
private ArtistResource GetArtist(int id)
|
||||
{
|
||||
var artist = _artistService.GetArtist(id);
|
||||
return GetArtistResource(artist);
|
||||
}
|
||||
|
||||
private ArtistResource GetArtistResource(NzbDrone.Core.Music.Artist artist)
|
||||
{
|
||||
if (artist == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resource = artist.ToResource();
|
||||
MapCoversToLocal(resource);
|
||||
FetchAndLinkArtistStatistics(resource);
|
||||
LinkNextPreviousAlbums(resource);
|
||||
|
||||
//PopulateAlternateTitles(resource);
|
||||
LinkRootFolderPath(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private List<ArtistResource> AllArtists()
|
||||
{
|
||||
var artistStats = _artistStatisticsService.ArtistStatistics();
|
||||
var artistsResources = _artistService.GetAllArtists().ToResource();
|
||||
|
||||
MapCoversToLocal(artistsResources.ToArray());
|
||||
LinkNextPreviousAlbums(artistsResources.ToArray());
|
||||
LinkArtistStatistics(artistsResources, artistStats);
|
||||
|
||||
//PopulateAlternateTitles(seriesResources);
|
||||
return artistsResources;
|
||||
}
|
||||
|
||||
private int AddArtist(ArtistResource artistResource)
|
||||
{
|
||||
var artist = _addArtistService.AddArtist(artistResource.ToModel());
|
||||
|
||||
return artist.Id;
|
||||
}
|
||||
|
||||
private void UpdateArtist(ArtistResource artistResource)
|
||||
{
|
||||
var moveFiles = Request.GetBooleanQueryParameter("moveFiles");
|
||||
var artist = _artistService.GetArtist(artistResource.Id);
|
||||
|
||||
if (moveFiles)
|
||||
{
|
||||
var sourcePath = artist.Path;
|
||||
var destinationPath = artistResource.Path;
|
||||
|
||||
_commandQueueManager.Push(new MoveArtistCommand
|
||||
{
|
||||
ArtistId = artist.Id,
|
||||
SourcePath = sourcePath,
|
||||
DestinationPath = destinationPath,
|
||||
Trigger = CommandTrigger.Manual
|
||||
});
|
||||
}
|
||||
|
||||
var model = artistResource.ToModel(artist);
|
||||
|
||||
_artistService.UpdateArtist(model);
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, artistResource);
|
||||
}
|
||||
|
||||
private void DeleteArtist(int id)
|
||||
{
|
||||
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
|
||||
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
|
||||
|
||||
_artistService.DeleteArtist(id, deleteFiles, addImportListExclusion);
|
||||
}
|
||||
|
||||
private void MapCoversToLocal(params ArtistResource[] artists)
|
||||
{
|
||||
foreach (var artistResource in artists)
|
||||
{
|
||||
_coverMapper.ConvertToLocalUrls(artistResource.Id, MediaCoverEntity.Artist, artistResource.Images);
|
||||
}
|
||||
}
|
||||
|
||||
private void LinkNextPreviousAlbums(params ArtistResource[] artists)
|
||||
{
|
||||
var nextAlbums = _albumService.GetNextAlbumsByArtistMetadataId(artists.Select(x => x.ArtistMetadataId));
|
||||
var lastAlbums = _albumService.GetLastAlbumsByArtistMetadataId(artists.Select(x => x.ArtistMetadataId));
|
||||
|
||||
foreach (var artistResource in artists)
|
||||
{
|
||||
artistResource.NextAlbum = nextAlbums.FirstOrDefault(x => x.ArtistMetadataId == artistResource.ArtistMetadataId);
|
||||
artistResource.LastAlbum = lastAlbums.FirstOrDefault(x => x.ArtistMetadataId == artistResource.ArtistMetadataId);
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchAndLinkArtistStatistics(ArtistResource resource)
|
||||
{
|
||||
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id));
|
||||
}
|
||||
|
||||
private void LinkArtistStatistics(List<ArtistResource> resources, List<ArtistStatistics> artistStatistics)
|
||||
{
|
||||
foreach (var artist in resources)
|
||||
{
|
||||
var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == artist.Id);
|
||||
if (stats == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LinkArtistStatistics(artist, stats);
|
||||
}
|
||||
}
|
||||
|
||||
private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics)
|
||||
{
|
||||
resource.Statistics = artistStatistics.ToResource();
|
||||
}
|
||||
|
||||
//private void PopulateAlternateTitles(List<ArtistResource> resources)
|
||||
//{
|
||||
// foreach (var resource in resources)
|
||||
// {
|
||||
// PopulateAlternateTitles(resource);
|
||||
// }
|
||||
//}
|
||||
|
||||
//private void PopulateAlternateTitles(ArtistResource resource)
|
||||
//{
|
||||
// var mappings = _sceneMappingService.FindByTvdbId(resource.TvdbId);
|
||||
|
||||
// if (mappings == null) return;
|
||||
|
||||
// resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList();
|
||||
//}
|
||||
private void LinkRootFolderPath(ArtistResource resource)
|
||||
{
|
||||
resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path);
|
||||
}
|
||||
|
||||
public void Handle(AlbumImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
|
||||
}
|
||||
|
||||
public void Handle(AlbumEditedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value));
|
||||
}
|
||||
|
||||
public void Handle(TrackFileDeletedEvent message)
|
||||
{
|
||||
if (message.Reason == DeleteMediaFileReason.Upgrade)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.TrackFile.Artist.Value));
|
||||
}
|
||||
|
||||
public void Handle(ArtistUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
|
||||
}
|
||||
|
||||
public void Handle(ArtistEditedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
|
||||
}
|
||||
|
||||
public void Handle(ArtistDeletedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource());
|
||||
}
|
||||
|
||||
public void Handle(ArtistRenamedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
|
||||
}
|
||||
|
||||
public void Handle(MediaCoversUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/Readarr.Api.V1/Artist/ArtistResource.cs
Normal file
173
src/Readarr.Api.V1/Artist/ArtistResource.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Music;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistResource : RestResource
|
||||
{
|
||||
//Todo: Sorters should be done completely on the client
|
||||
//Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
|
||||
//Todo: We should get the entire Profile instead of ID and Name separately
|
||||
[JsonIgnore]
|
||||
public int ArtistMetadataId { get; set; }
|
||||
public ArtistStatusType Status { get; set; }
|
||||
|
||||
public bool Ended => Status == ArtistStatusType.Ended;
|
||||
|
||||
public string ArtistName { get; set; }
|
||||
public string ForeignArtistId { get; set; }
|
||||
public string MBId { get; set; }
|
||||
public int TADBId { get; set; }
|
||||
public int DiscogsId { get; set; }
|
||||
public string AllMusicId { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public string ArtistType { get; set; }
|
||||
public string Disambiguation { get; set; }
|
||||
public List<Links> Links { get; set; }
|
||||
|
||||
public Album NextAlbum { get; set; }
|
||||
public Album LastAlbum { get; set; }
|
||||
|
||||
public List<MediaCover> Images { get; set; }
|
||||
public List<Member> Members { get; set; }
|
||||
|
||||
public string RemotePoster { get; set; }
|
||||
|
||||
//View & Edit
|
||||
public string Path { get; set; }
|
||||
public int QualityProfileId { get; set; }
|
||||
public int MetadataProfileId { get; set; }
|
||||
|
||||
//Editing Only
|
||||
public bool AlbumFolder { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
|
||||
public string RootFolderPath { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public string CleanName { get; set; }
|
||||
public string SortName { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public AddArtistOptions AddOptions { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
|
||||
public ArtistStatisticsResource Statistics { get; set; }
|
||||
}
|
||||
|
||||
public static class ArtistResourceMapper
|
||||
{
|
||||
public static ArtistResource ToResource(this NzbDrone.Core.Music.Artist model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ArtistResource
|
||||
{
|
||||
Id = model.Id,
|
||||
ArtistMetadataId = model.ArtistMetadataId,
|
||||
|
||||
ArtistName = model.Name,
|
||||
|
||||
//AlternateTitles
|
||||
SortName = model.SortName,
|
||||
|
||||
Status = model.Metadata.Value.Status,
|
||||
Overview = model.Metadata.Value.Overview,
|
||||
ArtistType = model.Metadata.Value.Type,
|
||||
Disambiguation = model.Metadata.Value.Disambiguation,
|
||||
|
||||
Images = model.Metadata.Value.Images.JsonClone(),
|
||||
|
||||
Path = model.Path,
|
||||
QualityProfileId = model.QualityProfileId,
|
||||
MetadataProfileId = model.MetadataProfileId,
|
||||
Links = model.Metadata.Value.Links,
|
||||
|
||||
AlbumFolder = model.AlbumFolder,
|
||||
Monitored = model.Monitored,
|
||||
|
||||
CleanName = model.CleanName,
|
||||
ForeignArtistId = model.Metadata.Value.ForeignArtistId,
|
||||
|
||||
// Root folder path is now calculated from the artist path
|
||||
// RootFolderPath = model.RootFolderPath,
|
||||
Genres = model.Metadata.Value.Genres,
|
||||
Tags = model.Tags,
|
||||
Added = model.Added,
|
||||
AddOptions = model.AddOptions,
|
||||
Ratings = model.Metadata.Value.Ratings,
|
||||
|
||||
Statistics = new ArtistStatisticsResource()
|
||||
};
|
||||
}
|
||||
|
||||
public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NzbDrone.Core.Music.Artist
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Metadata = new NzbDrone.Core.Music.ArtistMetadata
|
||||
{
|
||||
ForeignArtistId = resource.ForeignArtistId,
|
||||
Name = resource.ArtistName,
|
||||
Status = resource.Status,
|
||||
Overview = resource.Overview,
|
||||
Links = resource.Links,
|
||||
Images = resource.Images,
|
||||
Genres = resource.Genres,
|
||||
Ratings = resource.Ratings,
|
||||
Type = resource.ArtistType
|
||||
},
|
||||
|
||||
//AlternateTitles
|
||||
SortName = resource.SortName,
|
||||
Path = resource.Path,
|
||||
QualityProfileId = resource.QualityProfileId,
|
||||
MetadataProfileId = resource.MetadataProfileId,
|
||||
|
||||
AlbumFolder = resource.AlbumFolder,
|
||||
Monitored = resource.Monitored,
|
||||
|
||||
CleanName = resource.CleanName,
|
||||
RootFolderPath = resource.RootFolderPath,
|
||||
|
||||
Tags = resource.Tags,
|
||||
Added = resource.Added,
|
||||
AddOptions = resource.AddOptions,
|
||||
};
|
||||
}
|
||||
|
||||
public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource, NzbDrone.Core.Music.Artist artist)
|
||||
{
|
||||
var updatedArtist = resource.ToModel();
|
||||
|
||||
artist.ApplyChanges(updatedArtist);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
public static List<ArtistResource> ToResource(this IEnumerable<NzbDrone.Core.Music.Artist> artist)
|
||||
{
|
||||
return artist.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<NzbDrone.Core.Music.Artist> ToModel(this IEnumerable<ArtistResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Readarr.Api.V1/Artist/ArtistStatisticsResource.cs
Normal file
46
src/Readarr.Api.V1/Artist/ArtistStatisticsResource.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using NzbDrone.Core.ArtistStats;
|
||||
|
||||
namespace Readarr.Api.V1.Artist
|
||||
{
|
||||
public class ArtistStatisticsResource
|
||||
{
|
||||
public int AlbumCount { get; set; }
|
||||
public int TrackFileCount { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public int TotalTrackCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
|
||||
public decimal PercentOfTracks
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TrackCount == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return TrackFileCount / (decimal)TrackCount * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ArtistStatisticsResourceMapper
|
||||
{
|
||||
public static ArtistStatisticsResource ToResource(this ArtistStatistics model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ArtistStatisticsResource
|
||||
{
|
||||
AlbumCount = model.AlbumCount,
|
||||
TrackFileCount = model.TrackFileCount,
|
||||
TrackCount = model.TrackCount,
|
||||
TotalTrackCount = model.TotalTrackCount,
|
||||
SizeOnDisk = model.SizeOnDisk
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Readarr.Api.V1/Blacklist/BlacklistModule.cs
Normal file
30
src/Readarr.Api.V1/Blacklist/BlacklistModule.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using NzbDrone.Core.Blacklisting;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Blacklist
|
||||
{
|
||||
public class BlacklistModule : ReadarrRestModule<BlacklistResource>
|
||||
{
|
||||
private readonly IBlacklistService _blacklistService;
|
||||
|
||||
public BlacklistModule(IBlacklistService blacklistService)
|
||||
{
|
||||
_blacklistService = blacklistService;
|
||||
GetResourcePaged = GetBlacklist;
|
||||
DeleteResource = DeleteBlacklist;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/Readarr.Api.V1/Blacklist/BlacklistResource.cs
Normal file
50
src/Readarr.Api.V1/Blacklist/BlacklistResource.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Blacklist
|
||||
{
|
||||
public class BlacklistResource : RestResource
|
||||
{
|
||||
public int ArtistId { get; set; }
|
||||
public List<int> AlbumIds { get; set; }
|
||||
public string SourceTitle { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public ArtistResource Artist { get; set; }
|
||||
}
|
||||
|
||||
public static class BlacklistResourceMapper
|
||||
{
|
||||
public static BlacklistResource MapToResource(this NzbDrone.Core.Blacklisting.Blacklist model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BlacklistResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
ArtistId = model.ArtistId,
|
||||
AlbumIds = model.AlbumIds,
|
||||
SourceTitle = model.SourceTitle,
|
||||
Quality = model.Quality,
|
||||
Date = model.Date,
|
||||
Protocol = model.Protocol,
|
||||
Indexer = model.Indexer,
|
||||
Message = model.Message,
|
||||
|
||||
Artist = model.Artist.ToResource()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/Readarr.Api.V1/Calendar/CalendarFeedModule.cs
Normal file
101
src/Readarr.Api.V1/Calendar/CalendarFeedModule.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
using Ical.Net.Serialization;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Tags;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Calendar
|
||||
{
|
||||
public class CalendarFeedModule : ReadarrV1FeedModule
|
||||
{
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public CalendarFeedModule(IAlbumService albumService, IArtistService artistService, ITagService tagService)
|
||||
: base("calendar")
|
||||
{
|
||||
_albumService = albumService;
|
||||
_artistService = artistService;
|
||||
_tagService = tagService;
|
||||
|
||||
Get("/Readarr.ics", options => GetCalendarFeed());
|
||||
}
|
||||
|
||||
private object GetCalendarFeed()
|
||||
{
|
||||
var pastDays = 7;
|
||||
var futureDays = 28;
|
||||
var start = DateTime.Today.AddDays(-pastDays);
|
||||
var end = DateTime.Today.AddDays(futureDays);
|
||||
var unmonitored = Request.GetBooleanQueryParameter("unmonitored");
|
||||
var tags = new List<int>();
|
||||
|
||||
var queryPastDays = Request.Query.PastDays;
|
||||
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(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
|
||||
}
|
||||
|
||||
var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored);
|
||||
var calendar = new Ical.Net.Calendar
|
||||
{
|
||||
ProductId = "-//readarr.audio//Readarr//EN"
|
||||
};
|
||||
|
||||
var calendarName = "Readarr Music Schedule";
|
||||
calendar.AddProperty(new CalendarProperty("NAME", calendarName));
|
||||
calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName));
|
||||
|
||||
foreach (var album in albums.OrderBy(v => v.ReleaseDate.Value))
|
||||
{
|
||||
var artist = _artistService.GetArtist(album.ArtistId); // Temp fix TODO: Figure out why Album.Artist is not populated during AlbumsBetweenDates Query
|
||||
|
||||
if (tags.Any() && tags.None(artist.Tags.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var occurrence = calendar.Create<CalendarEvent>();
|
||||
occurrence.Uid = "Readarr_album_" + album.Id;
|
||||
|
||||
//occurrence.Status = album.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||
occurrence.Description = album.Overview;
|
||||
occurrence.Categories = album.Genres;
|
||||
|
||||
occurrence.Start = new CalDateTime(album.ReleaseDate.Value.ToLocalTime()) { HasTime = false };
|
||||
|
||||
occurrence.Summary = $"{artist.Name} - {album.Title}";
|
||||
}
|
||||
|
||||
var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext());
|
||||
var icalendar = serializer.SerializeToString(calendar);
|
||||
|
||||
return new TextResponse(icalendar, "text/calendar");
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Readarr.Api.V1/Calendar/CalendarModule.cs
Normal file
54
src/Readarr.Api.V1/Calendar/CalendarModule.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.ArtistStats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Api.V1.Albums;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/Readarr.Api.V1/Commands/CommandModule.cs
Normal file
107
src/Readarr.Api.V1/Commands/CommandModule.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ProgressMessaging;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
using Readarr.Http.Validation;
|
||||
|
||||
namespace Readarr.Api.V1.Commands
|
||||
{
|
||||
public class CommandModule : ReadarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IServiceFactory _serviceFactory;
|
||||
private readonly Debouncer _debouncer;
|
||||
private readonly Dictionary<int, CommandResource> _pendingUpdates;
|
||||
|
||||
public CommandModule(IManageCommandQueue commandQueueManager,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IServiceFactory serviceFactory)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_serviceFactory = serviceFactory;
|
||||
|
||||
GetResourceById = GetCommand;
|
||||
CreateResource = StartCommand;
|
||||
GetResourceAll = GetStartedCommands;
|
||||
DeleteResource = CancelCommand;
|
||||
|
||||
PostValidator.RuleFor(c => c.Name).NotBlank();
|
||||
|
||||
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
|
||||
_pendingUpdates = new Dictionary<int, CommandResource>();
|
||||
}
|
||||
|
||||
private CommandResource GetCommand(int id)
|
||||
{
|
||||
return _commandQueueManager.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private int StartCommand(CommandResource commandResource)
|
||||
{
|
||||
var commandType =
|
||||
_serviceFactory.GetImplementations(typeof(Command))
|
||||
.Single(c => c.Name.Replace("Command", "")
|
||||
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
dynamic command = Request.Body.FromJson(commandType);
|
||||
command.Trigger = CommandTrigger.Manual;
|
||||
command.SuppressMessages = !command.SendUpdatesToClient;
|
||||
command.SendUpdatesToClient = true;
|
||||
|
||||
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
|
||||
return trackedCommand.Id;
|
||||
}
|
||||
|
||||
private List<CommandResource> GetStartedCommands()
|
||||
{
|
||||
return _commandQueueManager.All().ToResource();
|
||||
}
|
||||
|
||||
private void CancelCommand(int id)
|
||||
{
|
||||
_commandQueueManager.Cancel(id);
|
||||
}
|
||||
|
||||
public void Handle(CommandUpdatedEvent message)
|
||||
{
|
||||
if (message.Command.Body.SendUpdatesToClient)
|
||||
{
|
||||
lock (_pendingUpdates)
|
||||
{
|
||||
_pendingUpdates[message.Command.Id] = message.Command.ToResource();
|
||||
}
|
||||
|
||||
_debouncer.Execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendUpdates()
|
||||
{
|
||||
lock (_pendingUpdates)
|
||||
{
|
||||
var pendingUpdates = _pendingUpdates.Values.ToArray();
|
||||
_pendingUpdates.Clear();
|
||||
|
||||
foreach (var pendingUpdate in pendingUpdates)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, pendingUpdate);
|
||||
|
||||
if (pendingUpdate.Name == typeof(MessagingCleanupCommand).Name.Replace("Command", "") &&
|
||||
pendingUpdate.Status == CommandStatus.Completed)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/Readarr.Api.V1/Commands/CommandResource.cs
Normal file
119
src/Readarr.Api.V1/Commands/CommandResource.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Commands
|
||||
{
|
||||
public class CommandResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string CommandName { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Command Body { get; set; }
|
||||
public CommandPriority Priority { get; set; }
|
||||
public CommandStatus Status { get; set; }
|
||||
public DateTime Queued { get; set; }
|
||||
public DateTime? Started { get; set; }
|
||||
public DateTime? Ended { get; set; }
|
||||
public TimeSpan? Duration { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public CommandTrigger Trigger { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string CompletionMessage { get; set; }
|
||||
|
||||
public DateTime? StateChangeTime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Started.HasValue)
|
||||
{
|
||||
return Started.Value;
|
||||
}
|
||||
|
||||
return Ended;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public bool SendUpdatesToClient
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Body != null)
|
||||
{
|
||||
return Body.SendUpdatesToClient;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateScheduledTask
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Body != null)
|
||||
{
|
||||
return Body.UpdateScheduledTask;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastExecutionTime { get; set; }
|
||||
}
|
||||
|
||||
public static class CommandResourceMapper
|
||||
{
|
||||
public static CommandResource ToResource(this CommandModel model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CommandResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Name = model.Name,
|
||||
CommandName = model.Name.SplitCamelCase(),
|
||||
Message = model.Message,
|
||||
Body = model.Body,
|
||||
Priority = model.Priority,
|
||||
Status = model.Status,
|
||||
Queued = model.QueuedAt,
|
||||
Started = model.StartedAt,
|
||||
Ended = model.EndedAt,
|
||||
Duration = model.Duration,
|
||||
Exception = model.Exception,
|
||||
Trigger = model.Trigger,
|
||||
|
||||
CompletionMessage = model.Body.CompletionMessage,
|
||||
LastExecutionTime = model.Body.LastExecutionTime
|
||||
};
|
||||
}
|
||||
|
||||
public static List<CommandResource> ToResource(this IEnumerable<CommandModel> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Readarr.Api.V1/Config/DownloadClientConfigModule.cs
Normal file
17
src/Readarr.Api.V1/Config/DownloadClientConfigModule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class DownloadClientConfigModule : ReadarrConfigModule<DownloadClientConfigResource>
|
||||
{
|
||||
public DownloadClientConfigModule(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
}
|
||||
|
||||
protected override DownloadClientConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return DownloadClientConfigResourceMapper.ToResource(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Readarr.Api.V1/Config/DownloadClientConfigResource.cs
Normal file
33
src/Readarr.Api.V1/Config/DownloadClientConfigResource.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class DownloadClientConfigResource : RestResource
|
||||
{
|
||||
public string DownloadClientWorkingFolders { get; set; }
|
||||
|
||||
public bool EnableCompletedDownloadHandling { get; set; }
|
||||
public bool RemoveCompletedDownloads { get; set; }
|
||||
|
||||
public bool AutoRedownloadFailed { get; set; }
|
||||
public bool RemoveFailedDownloads { get; set; }
|
||||
}
|
||||
|
||||
public static class DownloadClientConfigResourceMapper
|
||||
{
|
||||
public static DownloadClientConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return new DownloadClientConfigResource
|
||||
{
|
||||
DownloadClientWorkingFolders = model.DownloadClientWorkingFolders,
|
||||
|
||||
EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling,
|
||||
RemoveCompletedDownloads = model.RemoveCompletedDownloads,
|
||||
|
||||
AutoRedownloadFailed = model.AutoRedownloadFailed,
|
||||
RemoveFailedDownloads = model.RemoveFailedDownloads
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/Readarr.Api.V1/Config/HostConfigModule.cs
Normal file
91
src/Readarr.Api.V1/Config/HostConfigModule.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class HostConfigModule : ReadarrRestModule<HostConfigResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService configService, IUserService userService)
|
||||
: base("/config/host")
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_configService = configService;
|
||||
_userService = userService;
|
||||
|
||||
GetResourceSingle = GetHostConfig;
|
||||
GetResourceById = GetHostConfig;
|
||||
UpdateResource = SaveHostConfig;
|
||||
|
||||
SharedValidator.RuleFor(c => c.BindAddress)
|
||||
.ValidIp4Address()
|
||||
.NotListenAllIp4Address()
|
||||
.When(c => c.BindAddress != "*");
|
||||
|
||||
SharedValidator.RuleFor(c => c.Port).ValidPort();
|
||||
|
||||
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||
|
||||
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
|
||||
SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl);
|
||||
SharedValidator.RuleFor(c => c.SslCertPath).NotEmpty().When(c => c.EnableSsl);
|
||||
|
||||
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
|
||||
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
|
||||
|
||||
SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder));
|
||||
SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7);
|
||||
SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90);
|
||||
}
|
||||
|
||||
private HostConfigResource GetHostConfig()
|
||||
{
|
||||
var resource = _configFileProvider.ToResource(_configService);
|
||||
resource.Id = 1;
|
||||
|
||||
var user = _userService.FindUser();
|
||||
if (user != null)
|
||||
{
|
||||
resource.Username = user.Username;
|
||||
resource.Password = user.Password;
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private HostConfigResource GetHostConfig(int id)
|
||||
{
|
||||
return GetHostConfig();
|
||||
}
|
||||
|
||||
private void SaveHostConfig(HostConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||
_configService.SaveConfigDictionary(dictionary);
|
||||
|
||||
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_userService.Upsert(resource.Username, resource.Password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/Readarr.Api.V1/Config/HostConfigResource.cs
Normal file
87
src/Readarr.Api.V1/Config/HostConfigResource.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Security;
|
||||
using NzbDrone.Core.Update;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class HostConfigResource : RestResource
|
||||
{
|
||||
public string BindAddress { get; set; }
|
||||
public int Port { get; set; }
|
||||
public int SslPort { get; set; }
|
||||
public bool EnableSsl { get; set; }
|
||||
public bool LaunchBrowser { get; set; }
|
||||
public AuthenticationType AuthenticationMethod { get; set; }
|
||||
public bool AnalyticsEnabled { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string LogLevel { get; set; }
|
||||
public string ConsoleLogLevel { get; set; }
|
||||
public string Branch { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
public string SslCertPath { get; set; }
|
||||
public string SslCertPassword { get; set; }
|
||||
public string UrlBase { get; set; }
|
||||
public bool UpdateAutomatically { get; set; }
|
||||
public UpdateMechanism UpdateMechanism { get; set; }
|
||||
public string UpdateScriptPath { get; set; }
|
||||
public bool ProxyEnabled { get; set; }
|
||||
public ProxyType ProxyType { get; set; }
|
||||
public string ProxyHostname { get; set; }
|
||||
public int ProxyPort { get; set; }
|
||||
public string ProxyUsername { get; set; }
|
||||
public string ProxyPassword { get; set; }
|
||||
public string ProxyBypassFilter { get; set; }
|
||||
public bool ProxyBypassLocalAddresses { get; set; }
|
||||
public CertificateValidationType CertificateValidation { get; set; }
|
||||
public string BackupFolder { get; set; }
|
||||
public int BackupInterval { get; set; }
|
||||
public int BackupRetention { get; set; }
|
||||
}
|
||||
|
||||
public static class HostConfigResourceMapper
|
||||
{
|
||||
public static HostConfigResource ToResource(this IConfigFileProvider model, IConfigService configService)
|
||||
{
|
||||
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead?
|
||||
return new HostConfigResource
|
||||
{
|
||||
BindAddress = model.BindAddress,
|
||||
Port = model.Port,
|
||||
SslPort = model.SslPort,
|
||||
EnableSsl = model.EnableSsl,
|
||||
LaunchBrowser = model.LaunchBrowser,
|
||||
AuthenticationMethod = model.AuthenticationMethod,
|
||||
AnalyticsEnabled = model.AnalyticsEnabled,
|
||||
|
||||
//Username
|
||||
//Password
|
||||
LogLevel = model.LogLevel,
|
||||
ConsoleLogLevel = model.ConsoleLogLevel,
|
||||
Branch = model.Branch,
|
||||
ApiKey = model.ApiKey,
|
||||
SslCertPath = model.SslCertPath,
|
||||
SslCertPassword = model.SslCertPassword,
|
||||
UrlBase = model.UrlBase,
|
||||
UpdateAutomatically = model.UpdateAutomatically,
|
||||
UpdateMechanism = model.UpdateMechanism,
|
||||
UpdateScriptPath = model.UpdateScriptPath,
|
||||
ProxyEnabled = configService.ProxyEnabled,
|
||||
ProxyType = configService.ProxyType,
|
||||
ProxyHostname = configService.ProxyHostname,
|
||||
ProxyPort = configService.ProxyPort,
|
||||
ProxyUsername = configService.ProxyUsername,
|
||||
ProxyPassword = configService.ProxyPassword,
|
||||
ProxyBypassFilter = configService.ProxyBypassFilter,
|
||||
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
|
||||
CertificateValidation = configService.CertificateValidation,
|
||||
BackupFolder = configService.BackupFolder,
|
||||
BackupInterval = configService.BackupInterval,
|
||||
BackupRetention = configService.BackupRetention
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Readarr.Api.V1/Config/IndexerConfigModule.cs
Normal file
30
src/Readarr.Api.V1/Config/IndexerConfigModule.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http.Validation;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class IndexerConfigModule : ReadarrConfigModule<IndexerConfigResource>
|
||||
{
|
||||
public IndexerConfigModule(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
SharedValidator.RuleFor(c => c.MinimumAge)
|
||||
.GreaterThanOrEqualTo(0);
|
||||
|
||||
SharedValidator.RuleFor(c => c.MaximumSize)
|
||||
.GreaterThanOrEqualTo(0);
|
||||
|
||||
SharedValidator.RuleFor(c => c.Retention)
|
||||
.GreaterThanOrEqualTo(0);
|
||||
|
||||
SharedValidator.RuleFor(c => c.RssSyncInterval)
|
||||
.IsValidRssSyncInterval();
|
||||
}
|
||||
|
||||
protected override IndexerConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return IndexerConfigResourceMapper.ToResource(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Readarr.Api.V1/Config/IndexerConfigResource.cs
Normal file
27
src/Readarr.Api.V1/Config/IndexerConfigResource.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class IndexerConfigResource : RestResource
|
||||
{
|
||||
public int MinimumAge { get; set; }
|
||||
public int MaximumSize { get; set; }
|
||||
public int Retention { get; set; }
|
||||
public int RssSyncInterval { get; set; }
|
||||
}
|
||||
|
||||
public static class IndexerConfigResourceMapper
|
||||
{
|
||||
public static IndexerConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return new IndexerConfigResource
|
||||
{
|
||||
MinimumAge = model.MinimumAge,
|
||||
MaximumSize = model.MaximumSize,
|
||||
Retention = model.Retention,
|
||||
RssSyncInterval = model.RssSyncInterval,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Readarr.Api.V1/Config/MediaManagementConfigModule.cs
Normal file
24
src/Readarr.Api.V1/Config/MediaManagementConfigModule.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class MediaManagementConfigModule : ReadarrConfigModule<MediaManagementConfigResource>
|
||||
{
|
||||
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator)
|
||||
: base(configService)
|
||||
{
|
||||
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
|
||||
SharedValidator.RuleFor(c => c.FileChmod).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.FolderChmod).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
|
||||
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
|
||||
}
|
||||
|
||||
protected override MediaManagementConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return MediaManagementConfigResourceMapper.ToResource(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Readarr.Api.V1/Config/MediaManagementConfigResource.cs
Normal file
65
src/Readarr.Api.V1/Config/MediaManagementConfigResource.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class MediaManagementConfigResource : RestResource
|
||||
{
|
||||
public bool AutoUnmonitorPreviouslyDownloadedTracks { get; set; }
|
||||
public string RecycleBin { get; set; }
|
||||
public int RecycleBinCleanupDays { get; set; }
|
||||
public ProperDownloadTypes DownloadPropersAndRepacks { get; set; }
|
||||
public bool CreateEmptyArtistFolders { get; set; }
|
||||
public bool DeleteEmptyFolders { get; set; }
|
||||
public FileDateType FileDate { get; set; }
|
||||
public bool WatchLibraryForChanges { get; set; }
|
||||
public RescanAfterRefreshType RescanAfterRefresh { get; set; }
|
||||
public AllowFingerprinting AllowFingerprinting { get; set; }
|
||||
|
||||
public bool SetPermissionsLinux { get; set; }
|
||||
public string FileChmod { get; set; }
|
||||
public string FolderChmod { get; set; }
|
||||
public string ChownUser { get; set; }
|
||||
public string ChownGroup { get; set; }
|
||||
|
||||
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||
public int MinimumFreeSpaceWhenImporting { get; set; }
|
||||
public bool CopyUsingHardlinks { get; set; }
|
||||
public bool ImportExtraFiles { get; set; }
|
||||
public string ExtraFileExtensions { get; set; }
|
||||
}
|
||||
|
||||
public static class MediaManagementConfigResourceMapper
|
||||
{
|
||||
public static MediaManagementConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return new MediaManagementConfigResource
|
||||
{
|
||||
AutoUnmonitorPreviouslyDownloadedTracks = model.AutoUnmonitorPreviouslyDownloadedTracks,
|
||||
RecycleBin = model.RecycleBin,
|
||||
RecycleBinCleanupDays = model.RecycleBinCleanupDays,
|
||||
DownloadPropersAndRepacks = model.DownloadPropersAndRepacks,
|
||||
CreateEmptyArtistFolders = model.CreateEmptyArtistFolders,
|
||||
DeleteEmptyFolders = model.DeleteEmptyFolders,
|
||||
FileDate = model.FileDate,
|
||||
WatchLibraryForChanges = model.WatchLibraryForChanges,
|
||||
RescanAfterRefresh = model.RescanAfterRefresh,
|
||||
AllowFingerprinting = model.AllowFingerprinting,
|
||||
|
||||
SetPermissionsLinux = model.SetPermissionsLinux,
|
||||
FileChmod = model.FileChmod,
|
||||
FolderChmod = model.FolderChmod,
|
||||
ChownUser = model.ChownUser,
|
||||
ChownGroup = model.ChownGroup,
|
||||
|
||||
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
||||
MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting,
|
||||
CopyUsingHardlinks = model.CopyUsingHardlinks,
|
||||
ImportExtraFiles = model.ImportExtraFiles,
|
||||
ExtraFileExtensions = model.ExtraFileExtensions,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Readarr.Api.V1/Config/MetadataProviderConfigModule.cs
Normal file
21
src/Readarr.Api.V1/Config/MetadataProviderConfigModule.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class MetadataProviderConfigModule : ReadarrConfigModule<MetadataProviderConfigResource>
|
||||
{
|
||||
public MetadataProviderConfigModule(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace());
|
||||
}
|
||||
|
||||
protected override MetadataProviderConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return MetadataProviderConfigResourceMapper.ToResource(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Readarr.Api.V1/Config/MetadataProviderConfigResource.cs
Normal file
25
src/Readarr.Api.V1/Config/MetadataProviderConfigResource.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class MetadataProviderConfigResource : RestResource
|
||||
{
|
||||
public string MetadataSource { get; set; }
|
||||
public WriteAudioTagsType WriteAudioTags { get; set; }
|
||||
public bool ScrubAudioTags { get; set; }
|
||||
}
|
||||
|
||||
public static class MetadataProviderConfigResourceMapper
|
||||
{
|
||||
public static MetadataProviderConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return new MetadataProviderConfigResource
|
||||
{
|
||||
MetadataSource = model.MetadataSource,
|
||||
WriteAudioTags = model.WriteAudioTags,
|
||||
ScrubAudioTags = model.ScrubAudioTags,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/Readarr.Api.V1/Config/NamingConfigModule.cs
Normal file
122
src/Readarr.Api.V1/Config/NamingConfigModule.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Nancy.ModelBinding;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class NamingConfigModule : ReadarrRestModule<NamingConfigResource>
|
||||
{
|
||||
private readonly INamingConfigService _namingConfigService;
|
||||
private readonly IFilenameSampleService _filenameSampleService;
|
||||
private readonly IFilenameValidationService _filenameValidationService;
|
||||
private readonly IBuildFileNames _filenameBuilder;
|
||||
|
||||
public NamingConfigModule(INamingConfigService namingConfigService,
|
||||
IFilenameSampleService filenameSampleService,
|
||||
IFilenameValidationService filenameValidationService,
|
||||
IBuildFileNames filenameBuilder)
|
||||
: base("config/naming")
|
||||
{
|
||||
_namingConfigService = namingConfigService;
|
||||
_filenameSampleService = filenameSampleService;
|
||||
_filenameValidationService = filenameValidationService;
|
||||
_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.MultiDiscTrackFormat).ValidTrackFormat();
|
||||
SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat();
|
||||
SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat();
|
||||
}
|
||||
|
||||
private void UpdateNamingConfig(NamingConfigResource resource)
|
||||
{
|
||||
var nameSpec = resource.ToModel();
|
||||
ValidateFormatResult(nameSpec);
|
||||
|
||||
_namingConfigService.Save(nameSpec);
|
||||
}
|
||||
|
||||
private NamingConfigResource GetNamingConfig()
|
||||
{
|
||||
var nameSpec = _namingConfigService.GetConfig();
|
||||
var resource = nameSpec.ToResource();
|
||||
|
||||
if (resource.StandardTrackFormat.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec);
|
||||
basicConfig.AddToResource(resource);
|
||||
}
|
||||
|
||||
if (resource.MultiDiscTrackFormat.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec);
|
||||
basicConfig.AddToResource(resource);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private NamingConfigResource GetNamingConfig(int id)
|
||||
{
|
||||
return GetNamingConfig();
|
||||
}
|
||||
|
||||
private object GetExamples(NamingConfigResource config)
|
||||
{
|
||||
if (config.Id == 0)
|
||||
{
|
||||
config = GetNamingConfig();
|
||||
}
|
||||
|
||||
var nameSpec = config.ToModel();
|
||||
var sampleResource = new NamingExampleResource();
|
||||
|
||||
var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec);
|
||||
var multiDiscTrackSampleResult = _filenameSampleService.GetMultiDiscTrackSample(nameSpec);
|
||||
|
||||
sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null
|
||||
? null
|
||||
: singleTrackSampleResult.FileName;
|
||||
|
||||
sampleResource.MultiDiscTrackExample = _filenameValidationService.ValidateTrackFilename(multiDiscTrackSampleResult) != null
|
||||
? null
|
||||
: multiDiscTrackSampleResult.FileName;
|
||||
|
||||
sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _filenameSampleService.GetArtistFolderSample(nameSpec);
|
||||
|
||||
sampleResource.AlbumFolderExample = nameSpec.AlbumFolderFormat.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _filenameSampleService.GetAlbumFolderSample(nameSpec);
|
||||
|
||||
return sampleResource;
|
||||
}
|
||||
|
||||
private void ValidateFormatResult(NamingConfig nameSpec)
|
||||
{
|
||||
var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec);
|
||||
|
||||
var singleTrackValidationResult = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult);
|
||||
|
||||
var validationFailures = new List<ValidationFailure>();
|
||||
|
||||
validationFailures.AddIfNotNull(singleTrackValidationResult);
|
||||
|
||||
if (validationFailures.Any())
|
||||
{
|
||||
throw new ValidationException(validationFailures.DistinctBy(v => v.PropertyName).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Readarr.Api.V1/Config/NamingConfigResource.cs
Normal file
20
src/Readarr.Api.V1/Config/NamingConfigResource.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class NamingConfigResource : RestResource
|
||||
{
|
||||
public bool RenameTracks { get; set; }
|
||||
public bool ReplaceIllegalCharacters { get; set; }
|
||||
public string StandardTrackFormat { get; set; }
|
||||
public string MultiDiscTrackFormat { get; set; }
|
||||
public string ArtistFolderFormat { get; set; }
|
||||
public string AlbumFolderFormat { get; set; }
|
||||
public bool IncludeArtistName { get; set; }
|
||||
public bool IncludeAlbumTitle { get; set; }
|
||||
public bool IncludeQuality { get; set; }
|
||||
public bool ReplaceSpaces { get; set; }
|
||||
public string Separator { get; set; }
|
||||
public string NumberStyle { get; set; }
|
||||
}
|
||||
}
|
||||
56
src/Readarr.Api.V1/Config/NamingExampleResource.cs
Normal file
56
src/Readarr.Api.V1/Config/NamingExampleResource.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using NzbDrone.Core.Organizer;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class NamingExampleResource
|
||||
{
|
||||
public string SingleTrackExample { get; set; }
|
||||
public string MultiDiscTrackExample { get; set; }
|
||||
public string ArtistFolderExample { get; set; }
|
||||
public string AlbumFolderExample { get; set; }
|
||||
}
|
||||
|
||||
public static class NamingConfigResourceMapper
|
||||
{
|
||||
public static NamingConfigResource ToResource(this NamingConfig model)
|
||||
{
|
||||
return new NamingConfigResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
RenameTracks = model.RenameTracks,
|
||||
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
|
||||
StandardTrackFormat = model.StandardTrackFormat,
|
||||
MultiDiscTrackFormat = model.MultiDiscTrackFormat,
|
||||
ArtistFolderFormat = model.ArtistFolderFormat,
|
||||
AlbumFolderFormat = model.AlbumFolderFormat
|
||||
};
|
||||
}
|
||||
|
||||
public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource)
|
||||
{
|
||||
resource.IncludeArtistName = basicNamingConfig.IncludeArtistName;
|
||||
resource.IncludeAlbumTitle = basicNamingConfig.IncludeAlbumTitle;
|
||||
resource.IncludeQuality = basicNamingConfig.IncludeQuality;
|
||||
resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces;
|
||||
resource.Separator = basicNamingConfig.Separator;
|
||||
resource.NumberStyle = basicNamingConfig.NumberStyle;
|
||||
}
|
||||
|
||||
public static NamingConfig ToModel(this NamingConfigResource resource)
|
||||
{
|
||||
return new NamingConfig
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
RenameTracks = resource.RenameTracks,
|
||||
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
|
||||
StandardTrackFormat = resource.StandardTrackFormat,
|
||||
MultiDiscTrackFormat = resource.MultiDiscTrackFormat,
|
||||
|
||||
ArtistFolderFormat = resource.ArtistFolderFormat,
|
||||
AlbumFolderFormat = resource.AlbumFolderFormat
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/Readarr.Api.V1/Config/ReadarrConfigModule.cs
Normal file
53
src/Readarr.Api.V1/Config/ReadarrConfigModule.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public abstract class ReadarrConfigModule<TResource> : ReadarrRestModule<TResource>
|
||||
where TResource : RestResource, new()
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
protected ReadarrConfigModule(IConfigService configService)
|
||||
: this(new TResource().ResourceName.Replace("config", ""), configService)
|
||||
{
|
||||
}
|
||||
|
||||
protected ReadarrConfigModule(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Readarr.Api.V1/Config/UiConfigModule.cs
Normal file
17
src/Readarr.Api.V1/Config/UiConfigModule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class UiConfigModule : ReadarrConfigModule<UiConfigResource>
|
||||
{
|
||||
public UiConfigModule(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
}
|
||||
|
||||
protected override UiConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return UiConfigResourceMapper.ToResource(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Readarr.Api.V1/Config/UiConfigResource.cs
Normal file
51
src/Readarr.Api.V1/Config/UiConfigResource.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public class UiConfigResource : RestResource
|
||||
{
|
||||
//Calendar
|
||||
public int FirstDayOfWeek { get; set; }
|
||||
public string CalendarWeekColumnHeader { get; set; }
|
||||
|
||||
//Dates
|
||||
public string ShortDateFormat { get; set; }
|
||||
public string LongDateFormat { get; set; }
|
||||
public string TimeFormat { get; set; }
|
||||
public bool ShowRelativeDates { get; set; }
|
||||
|
||||
public bool EnableColorImpairedMode { get; set; }
|
||||
|
||||
public bool ExpandAlbumByDefault { get; set; }
|
||||
public bool ExpandSingleByDefault { get; set; }
|
||||
public bool ExpandEPByDefault { get; set; }
|
||||
public bool ExpandBroadcastByDefault { get; set; }
|
||||
public bool ExpandOtherByDefault { get; set; }
|
||||
}
|
||||
|
||||
public static class UiConfigResourceMapper
|
||||
{
|
||||
public static UiConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return new UiConfigResource
|
||||
{
|
||||
FirstDayOfWeek = model.FirstDayOfWeek,
|
||||
CalendarWeekColumnHeader = model.CalendarWeekColumnHeader,
|
||||
|
||||
ShortDateFormat = model.ShortDateFormat,
|
||||
LongDateFormat = model.LongDateFormat,
|
||||
TimeFormat = model.TimeFormat,
|
||||
ShowRelativeDates = model.ShowRelativeDates,
|
||||
|
||||
EnableColorImpairedMode = model.EnableColorImpairedMode,
|
||||
|
||||
ExpandAlbumByDefault = model.ExpandAlbumByDefault,
|
||||
ExpandSingleByDefault = model.ExpandSingleByDefault,
|
||||
ExpandEPByDefault = model.ExpandEPByDefault,
|
||||
ExpandBroadcastByDefault = model.ExpandBroadcastByDefault,
|
||||
ExpandOtherByDefault = model.ExpandOtherByDefault
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/Readarr.Api.V1/CustomFilters/CustomFilterModule.cs
Normal file
49
src/Readarr.Api.V1/CustomFilters/CustomFilterModule.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterModule : ReadarrRestModule<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/Readarr.Api.V1/CustomFilters/CustomFilterResource.cs
Normal file
55
src/Readarr.Api.V1/CustomFilters/CustomFilterResource.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterResource : RestResource
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Label { get; set; }
|
||||
public List<dynamic> Filters { get; set; }
|
||||
}
|
||||
|
||||
public static class CustomFilterResourceMapper
|
||||
{
|
||||
public static CustomFilterResource ToResource(this CustomFilter model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CustomFilterResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Type = model.Type,
|
||||
Label = model.Label,
|
||||
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
|
||||
};
|
||||
}
|
||||
|
||||
public static CustomFilter ToModel(this CustomFilterResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CustomFilter
|
||||
{
|
||||
Id = resource.Id,
|
||||
Type = resource.Type,
|
||||
Label = resource.Label,
|
||||
Filters = Json.ToJson(resource.Filters)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<CustomFilterResource> ToResource(this IEnumerable<CustomFilter> filters)
|
||||
{
|
||||
return filters.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Readarr.Api.V1/DiskSpace/DiskSpaceModule.cs
Normal file
23
src/Readarr.Api.V1/DiskSpace/DiskSpaceModule.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.DiskSpace;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.DiskSpace
|
||||
{
|
||||
public class DiskSpaceModule : ReadarrRestModule<DiskSpaceResource>
|
||||
{
|
||||
private readonly IDiskSpaceService _diskSpaceService;
|
||||
|
||||
public DiskSpaceModule(IDiskSpaceService diskSpaceService)
|
||||
: base("diskspace")
|
||||
{
|
||||
_diskSpaceService = diskSpaceService;
|
||||
GetResourceAll = GetFreeSpace;
|
||||
}
|
||||
|
||||
public List<DiskSpaceResource> GetFreeSpace()
|
||||
{
|
||||
return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/Readarr.Api.V1/DiskSpace/DiskSpaceResource.cs
Normal file
31
src/Readarr.Api.V1/DiskSpace/DiskSpaceResource.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.DiskSpace
|
||||
{
|
||||
public class DiskSpaceResource : RestResource
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public string Label { get; set; }
|
||||
public long FreeSpace { get; set; }
|
||||
public long TotalSpace { get; set; }
|
||||
}
|
||||
|
||||
public static class DiskSpaceResourceMapper
|
||||
{
|
||||
public static DiskSpaceResource MapToResource(this NzbDrone.Core.DiskSpace.DiskSpace model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DiskSpaceResource
|
||||
{
|
||||
Path = model.Path,
|
||||
Label = model.Label,
|
||||
FreeSpace = model.FreeSpace,
|
||||
TotalSpace = model.TotalSpace
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Readarr.Api.V1/DownloadClient/DownloadClientModule.cs
Normal file
24
src/Readarr.Api.V1/DownloadClient/DownloadClientModule.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using NzbDrone.Core.Download;
|
||||
|
||||
namespace Readarr.Api.V1.DownloadClient
|
||||
{
|
||||
public class DownloadClientModule : ProviderModuleBase<DownloadClientResource, IDownloadClient, DownloadClientDefinition>
|
||||
{
|
||||
public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper();
|
||||
|
||||
public DownloadClientModule(IDownloadClientFactory downloadClientFactory)
|
||||
: base(downloadClientFactory, "downloadclient", ResourceMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Validate(DownloadClientDefinition definition, bool includeWarnings)
|
||||
{
|
||||
if (!definition.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Readarr.Api.V1/DownloadClient/DownloadClientResource.cs
Normal file
47
src/Readarr.Api.V1/DownloadClient/DownloadClientResource.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace Readarr.Api.V1.DownloadClient
|
||||
{
|
||||
public class DownloadClientResource : ProviderResource
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
public class DownloadClientResourceMapper : ProviderResourceMapper<DownloadClientResource, DownloadClientDefinition>
|
||||
{
|
||||
public override DownloadClientResource ToResource(DownloadClientDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resource = base.ToResource(definition);
|
||||
|
||||
resource.Enable = definition.Enable;
|
||||
resource.Protocol = definition.Protocol;
|
||||
resource.Priority = definition.Priority;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public override DownloadClientDefinition ToModel(DownloadClientResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var definition = base.ToModel(resource);
|
||||
|
||||
definition.Enable = resource.Enable;
|
||||
definition.Protocol = resource.Protocol;
|
||||
definition.Priority = resource.Priority;
|
||||
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/Readarr.Api.V1/FileSystem/FileSystemModule.cs
Normal file
69
src/Readarr.Api.V1/FileSystem/FileSystemModule.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.FileSystem
|
||||
{
|
||||
public class FileSystemModule : ReadarrV1Module
|
||||
{
|
||||
private readonly IFileSystemLookupService _fileSystemLookupService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IDiskScanService _diskScanService;
|
||||
|
||||
public FileSystemModule(IFileSystemLookupService fileSystemLookupService,
|
||||
IDiskProvider diskProvider,
|
||||
IDiskScanService diskScanService)
|
||||
: base("/filesystem")
|
||||
{
|
||||
_fileSystemLookupService = fileSystemLookupService;
|
||||
_diskProvider = diskProvider;
|
||||
_diskScanService = diskScanService;
|
||||
Get("/", x => GetContents());
|
||||
Get("/type", x => GetEntityType());
|
||||
Get("/mediafiles", x => GetMediaFiles());
|
||||
}
|
||||
|
||||
private object GetContents()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var includeFiles = Request.GetBooleanQueryParameter("includeFiles");
|
||||
var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes");
|
||||
|
||||
return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes);
|
||||
}
|
||||
|
||||
private object GetEntityType()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var path = (string)pathQuery.Value;
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
{
|
||||
return new { type = "file" };
|
||||
}
|
||||
|
||||
//Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system
|
||||
return new { type = "folder" };
|
||||
}
|
||||
|
||||
private object GetMediaFiles()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var path = (string)pathQuery.Value;
|
||||
|
||||
if (!_diskProvider.FolderExists(path))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
return _diskScanService.GetAudioFiles(path).Select(f => new
|
||||
{
|
||||
Path = f.FullName,
|
||||
Name = f.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Readarr.Api.V1/Health/HealthModule.cs
Normal file
32
src/Readarr.Api.V1/Health/HealthModule.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Health
|
||||
{
|
||||
public class HealthModule : ReadarrRestModuleWithSignalR<HealthResource, HealthCheck>,
|
||||
IHandle<HealthCheckCompleteEvent>
|
||||
{
|
||||
private readonly IHealthCheckService _healthCheckService;
|
||||
|
||||
public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_healthCheckService = healthCheckService;
|
||||
GetResourceAll = GetHealth;
|
||||
}
|
||||
|
||||
private List<HealthResource> GetHealth()
|
||||
{
|
||||
return _healthCheckService.Results().ToResource();
|
||||
}
|
||||
|
||||
public void Handle(HealthCheckCompleteEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Readarr.Api.V1/Health/HealthResource.cs
Normal file
41
src/Readarr.Api.V1/Health/HealthResource.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Health
|
||||
{
|
||||
public class HealthResource : RestResource
|
||||
{
|
||||
public string Source { get; set; }
|
||||
public HealthCheckResult Type { get; set; }
|
||||
public string Message { get; set; }
|
||||
public HttpUri WikiUrl { get; set; }
|
||||
}
|
||||
|
||||
public static class HealthResourceMapper
|
||||
{
|
||||
public static HealthResource ToResource(this HealthCheck model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HealthResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Source = model.Source.Name,
|
||||
Type = model.Type,
|
||||
Message = model.Message,
|
||||
WikiUrl = model.WikiUrl
|
||||
};
|
||||
}
|
||||
|
||||
public static List<HealthResource> ToResource(this IEnumerable<HealthCheck> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/Readarr.Api.V1/History/HistoryModule.cs
Normal file
160
src/Readarr.Api.V1/History/HistoryModule.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.History;
|
||||
using Readarr.Api.V1.Albums;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Api.V1.Tracks;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.History
|
||||
{
|
||||
public class HistoryModule : ReadarrRestModule<HistoryResource>
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IUpgradableSpecification _upgradableSpecification;
|
||||
private readonly IFailedDownloadService _failedDownloadService;
|
||||
|
||||
public HistoryModule(IHistoryService historyService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IFailedDownloadService failedDownloadService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_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)
|
||||
{
|
||||
var resource = model.ToResource();
|
||||
|
||||
if (includeArtist)
|
||||
{
|
||||
resource.Artist = model.Artist.ToResource();
|
||||
}
|
||||
|
||||
if (includeAlbum)
|
||||
{
|
||||
resource.Album = model.Album.ToResource();
|
||||
}
|
||||
|
||||
if (includeTrack)
|
||||
{
|
||||
resource.Track = model.Track.ToResource();
|
||||
}
|
||||
|
||||
if (model.Artist != null)
|
||||
{
|
||||
resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Artist.QualityProfile.Value, model.Quality);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private PagingResource<HistoryResource> GetHistory(PagingResource<HistoryResource> pagingResource)
|
||||
{
|
||||
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 albumIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "albumId");
|
||||
var downloadIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "downloadId");
|
||||
|
||||
if (eventTypeFilter != null)
|
||||
{
|
||||
var filterValue = (HistoryEventType)Convert.ToInt32(eventTypeFilter.Value);
|
||||
pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue);
|
||||
}
|
||||
|
||||
if (albumIdFilter != null)
|
||||
{
|
||||
var albumId = Convert.ToInt32(albumIdFilter.Value);
|
||||
pagingSpec.FilterExpressions.Add(h => h.AlbumId == albumId);
|
||||
}
|
||||
|
||||
if (downloadIdFilter != null)
|
||||
{
|
||||
var downloadId = downloadIdFilter.Value;
|
||||
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
||||
}
|
||||
|
||||
return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
|
||||
}
|
||||
|
||||
private List<HistoryResource> GetHistorySince()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
private List<HistoryResource> GetArtistHistory()
|
||||
{
|
||||
var queryArtistId = Request.Query.ArtistId;
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private object MarkAsFailed()
|
||||
{
|
||||
var id = (int)Request.Form.Id;
|
||||
_failedDownloadService.MarkAsFailed(id);
|
||||
return new object();
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/Readarr.Api.V1/History/HistoryResource.cs
Normal file
64
src/Readarr.Api.V1/History/HistoryResource.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.Albums;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Api.V1.Tracks;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.History
|
||||
{
|
||||
public class HistoryResource : RestResource
|
||||
{
|
||||
public int AlbumId { get; set; }
|
||||
public int ArtistId { get; set; }
|
||||
public int TrackId { get; set; }
|
||||
public string SourceTitle { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public bool QualityCutoffNotMet { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
|
||||
public HistoryEventType EventType { get; set; }
|
||||
|
||||
public Dictionary<string, string> Data { get; set; }
|
||||
|
||||
public AlbumResource Album { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
public TrackResource Track { get; set; }
|
||||
}
|
||||
|
||||
public static class HistoryResourceMapper
|
||||
{
|
||||
public static HistoryResource ToResource(this NzbDrone.Core.History.History model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HistoryResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
AlbumId = model.AlbumId,
|
||||
ArtistId = model.ArtistId,
|
||||
TrackId = model.TrackId,
|
||||
SourceTitle = model.SourceTitle,
|
||||
Quality = model.Quality,
|
||||
|
||||
//QualityCutoffNotMet
|
||||
Date = model.Date,
|
||||
DownloadId = model.DownloadId,
|
||||
|
||||
EventType = model.EventType,
|
||||
|
||||
Data = model.Data
|
||||
|
||||
//Episode
|
||||
//Series
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Readarr.Api.V1/ImportLists/ImportListExclusionModule.cs
Normal file
56
src/Readarr.Api.V1/ImportLists/ImportListExclusionModule.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.ImportLists.Exclusions;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.ImportLists
|
||||
{
|
||||
public class ImportListExclusionModule : ReadarrRestModule<ImportListExclusionResource>
|
||||
{
|
||||
private readonly IImportListExclusionService _importListExclusionService;
|
||||
|
||||
public ImportListExclusionModule(IImportListExclusionService importListExclusionService,
|
||||
ImportListExclusionExistsValidator importListExclusionExistsValidator,
|
||||
GuidValidator guidValidator)
|
||||
{
|
||||
_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.ArtistName).NotEmpty();
|
||||
}
|
||||
|
||||
private ImportListExclusionResource GetImportListExclusion(int id)
|
||||
{
|
||||
return _importListExclusionService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<ImportListExclusionResource> GetImportListExclusions()
|
||||
{
|
||||
return _importListExclusionService.All().ToResource();
|
||||
}
|
||||
|
||||
private int AddImportListExclusion(ImportListExclusionResource resource)
|
||||
{
|
||||
var customFilter = _importListExclusionService.Add(resource.ToModel());
|
||||
|
||||
return customFilter.Id;
|
||||
}
|
||||
|
||||
private void UpdateImportListExclusion(ImportListExclusionResource resource)
|
||||
{
|
||||
_importListExclusionService.Update(resource.ToModel());
|
||||
}
|
||||
|
||||
private void DeleteImportListExclusionResource(int id)
|
||||
{
|
||||
_importListExclusionService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.ImportLists.Exclusions;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.ImportLists
|
||||
{
|
||||
public class ImportListExclusionResource : RestResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
}
|
||||
|
||||
public static class ImportListExclusionResourceMapper
|
||||
{
|
||||
public static ImportListExclusionResource ToResource(this ImportListExclusion model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImportListExclusionResource
|
||||
{
|
||||
Id = model.Id,
|
||||
ForeignId = model.ForeignId,
|
||||
ArtistName = model.Name,
|
||||
};
|
||||
}
|
||||
|
||||
public static ImportListExclusion ToModel(this ImportListExclusionResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImportListExclusion
|
||||
{
|
||||
Id = resource.Id,
|
||||
ForeignId = resource.ForeignId,
|
||||
Name = resource.ArtistName
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ImportListExclusionResource> ToResource(this IEnumerable<ImportListExclusion> filters)
|
||||
{
|
||||
return filters.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Readarr.Api.V1/ImportLists/ImportListModule.cs
Normal file
34
src/Readarr.Api.V1/ImportLists/ImportListModule.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace Readarr.Api.V1.ImportLists
|
||||
{
|
||||
public class ImportListModule : ProviderModuleBase<ImportListResource, IImportList, ImportListDefinition>
|
||||
{
|
||||
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
|
||||
|
||||
public ImportListModule(ImportListFactory importListFactory,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
: base(importListFactory, "importlist", ResourceMapper)
|
||||
{
|
||||
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId));
|
||||
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId));
|
||||
|
||||
SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath();
|
||||
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator);
|
||||
SharedValidator.RuleFor(c => c.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
|
||||
}
|
||||
|
||||
protected override void Validate(ImportListDefinition definition, bool includeWarnings)
|
||||
{
|
||||
if (!definition.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/Readarr.Api.V1/ImportLists/ImportListResource.cs
Normal file
57
src/Readarr.Api.V1/ImportLists/ImportListResource.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using NzbDrone.Core.ImportLists;
|
||||
|
||||
namespace Readarr.Api.V1.ImportLists
|
||||
{
|
||||
public class ImportListResource : ProviderResource
|
||||
{
|
||||
public bool EnableAutomaticAdd { get; set; }
|
||||
public ImportListMonitorType ShouldMonitor { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public int QualityProfileId { get; set; }
|
||||
public int MetadataProfileId { get; set; }
|
||||
public ImportListType ListType { get; set; }
|
||||
public int ListOrder { get; set; }
|
||||
}
|
||||
|
||||
public class ImportListResourceMapper : ProviderResourceMapper<ImportListResource, ImportListDefinition>
|
||||
{
|
||||
public override ImportListResource ToResource(ImportListDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resource = base.ToResource(definition);
|
||||
|
||||
resource.EnableAutomaticAdd = definition.EnableAutomaticAdd;
|
||||
resource.ShouldMonitor = definition.ShouldMonitor;
|
||||
resource.RootFolderPath = definition.RootFolderPath;
|
||||
resource.QualityProfileId = definition.ProfileId;
|
||||
resource.MetadataProfileId = definition.MetadataProfileId;
|
||||
resource.ListType = definition.ListType;
|
||||
resource.ListOrder = (int)definition.ListType;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public override ImportListDefinition ToModel(ImportListResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var definition = base.ToModel(resource);
|
||||
|
||||
definition.EnableAutomaticAdd = resource.EnableAutomaticAdd;
|
||||
definition.ShouldMonitor = resource.ShouldMonitor;
|
||||
definition.RootFolderPath = resource.RootFolderPath;
|
||||
definition.ProfileId = resource.QualityProfileId;
|
||||
definition.MetadataProfileId = resource.MetadataProfileId;
|
||||
definition.ListType = resource.ListType;
|
||||
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Readarr.Api.V1/Indexers/IndexerModule.cs
Normal file
24
src/Readarr.Api.V1/Indexers/IndexerModule.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerModule : ProviderModuleBase<IndexerResource, IIndexer, IndexerDefinition>
|
||||
{
|
||||
public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper();
|
||||
|
||||
public IndexerModule(IndexerFactory indexerFactory)
|
||||
: base(indexerFactory, "indexer", ResourceMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Validate(IndexerDefinition definition, bool includeWarnings)
|
||||
{
|
||||
if (!definition.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/Readarr.Api.V1/Indexers/IndexerResource.cs
Normal file
52
src/Readarr.Api.V1/Indexers/IndexerResource.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerResource : ProviderResource
|
||||
{
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableAutomaticSearch { get; set; }
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
public bool SupportsRss { get; set; }
|
||||
public bool SupportsSearch { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
}
|
||||
|
||||
public class IndexerResourceMapper : ProviderResourceMapper<IndexerResource, IndexerDefinition>
|
||||
{
|
||||
public override IndexerResource ToResource(IndexerDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resource = base.ToResource(definition);
|
||||
|
||||
resource.EnableRss = definition.EnableRss;
|
||||
resource.EnableAutomaticSearch = definition.EnableAutomaticSearch;
|
||||
resource.EnableInteractiveSearch = definition.EnableInteractiveSearch;
|
||||
resource.SupportsRss = definition.SupportsRss;
|
||||
resource.SupportsSearch = definition.SupportsSearch;
|
||||
resource.Protocol = definition.Protocol;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public override IndexerDefinition ToModel(IndexerResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var definition = base.ToModel(resource);
|
||||
|
||||
definition.EnableRss = resource.EnableRss;
|
||||
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
|
||||
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
|
||||
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/Readarr.Api.V1/Indexers/ReleaseModule.cs
Normal file
147
src/Readarr.Api.V1/Indexers/ReleaseModule.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
using HttpStatusCode = System.Net.HttpStatusCode;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
public class ReleaseModule : ReleaseModuleBase
|
||||
{
|
||||
private readonly IFetchAndParseRss _rssFetcherAndParser;
|
||||
private readonly ISearchForNzb _nzbSearchService;
|
||||
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
||||
private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision;
|
||||
private readonly IDownloadService _downloadService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<RemoteAlbum> _remoteAlbumCache;
|
||||
|
||||
public ReleaseModule(IFetchAndParseRss rssFetcherAndParser,
|
||||
ISearchForNzb nzbSearchService,
|
||||
IMakeDownloadDecision downloadDecisionMaker,
|
||||
IPrioritizeDownloadDecision prioritizeDownloadDecision,
|
||||
IDownloadService downloadService,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_rssFetcherAndParser = rssFetcherAndParser;
|
||||
_nzbSearchService = nzbSearchService;
|
||||
_downloadDecisionMaker = downloadDecisionMaker;
|
||||
_prioritizeDownloadDecision = prioritizeDownloadDecision;
|
||||
_downloadService = downloadService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceAll = GetReleases;
|
||||
Post("/", x => DownloadRelease(ReadResourceFromRequest()));
|
||||
|
||||
PostValidator.RuleFor(s => s.IndexerId).ValidId();
|
||||
PostValidator.RuleFor(s => s.Guid).NotEmpty();
|
||||
|
||||
_remoteAlbumCache = cacheManager.GetCache<RemoteAlbum>(GetType(), "remoteAlbums");
|
||||
}
|
||||
|
||||
private object DownloadRelease(ReleaseResource release)
|
||||
{
|
||||
var remoteAlbum = _remoteAlbumCache.Find(GetCacheKey(release));
|
||||
|
||||
if (remoteAlbum == null)
|
||||
{
|
||||
_logger.Debug("Couldn't find requested release in cache, cache timeout probably expired.");
|
||||
|
||||
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Couldn't find requested release in cache, try searching again");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_downloadService.DownloadReport(remoteAlbum);
|
||||
}
|
||||
catch (ReleaseDownloadException ex)
|
||||
{
|
||||
_logger.Error(ex, "Getting release from indexer failed");
|
||||
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed");
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetReleases()
|
||||
{
|
||||
if (Request.Query.albumId.HasValue)
|
||||
{
|
||||
return GetAlbumReleases(Request.Query.albumId);
|
||||
}
|
||||
|
||||
if (Request.Query.artistId.HasValue)
|
||||
{
|
||||
return GetArtistReleases(Request.Query.artistId);
|
||||
}
|
||||
|
||||
return GetRss();
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetAlbumReleases(int albumId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decisions = _nzbSearchService.AlbumSearch(albumId, true, true, true);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Album search failed");
|
||||
}
|
||||
|
||||
return new List<ReleaseResource>();
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetArtistReleases(int artistId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decisions = _nzbSearchService.ArtistSearch(artistId, false, true, true);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Artist search failed");
|
||||
}
|
||||
|
||||
return new List<ReleaseResource>();
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetRss()
|
||||
{
|
||||
var reports = _rssFetcherAndParser.Fetch();
|
||||
var decisions = _downloadDecisionMaker.GetRssDecision(reports);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
}
|
||||
|
||||
protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight)
|
||||
{
|
||||
var resource = base.MapDecision(decision, initialWeight);
|
||||
_remoteAlbumCache.Set(GetCacheKey(resource), decision.RemoteAlbum, TimeSpan.FromMinutes(30));
|
||||
return resource;
|
||||
}
|
||||
|
||||
private string GetCacheKey(ReleaseResource resource)
|
||||
{
|
||||
return string.Concat(resource.IndexerId, "_", resource.Guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/Readarr.Api.V1/Indexers/ReleaseModuleBase.cs
Normal file
42
src/Readarr.Api.V1/Indexers/ReleaseModuleBase.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
public abstract class ReleaseModuleBase : ReadarrRestModule<ReleaseResource>
|
||||
{
|
||||
protected virtual List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions)
|
||||
{
|
||||
var result = new List<ReleaseResource>();
|
||||
|
||||
foreach (var downloadDecision in decisions)
|
||||
{
|
||||
var release = MapDecision(downloadDecision, result.Count);
|
||||
|
||||
result.Add(release);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual ReleaseResource MapDecision(DownloadDecision decision, int initialWeight)
|
||||
{
|
||||
var release = decision.ToResource();
|
||||
|
||||
release.ReleaseWeight = initialWeight;
|
||||
|
||||
if (decision.RemoteAlbum.Artist != null)
|
||||
{
|
||||
release.QualityWeight = decision.RemoteAlbum
|
||||
.Artist
|
||||
.QualityProfile.Value.GetIndex(release.Quality.Quality).Index * 100;
|
||||
}
|
||||
|
||||
release.QualityWeight += release.Quality.Revision.Real * 10;
|
||||
release.QualityWeight += release.Quality.Revision.Version;
|
||||
|
||||
return release;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Readarr.Api.V1/Indexers/ReleasePushModule.cs
Normal file
98
src/Readarr.Api.V1/Indexers/ReleasePushModule.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
internal class ReleasePushModule : ReleaseModuleBase
|
||||
{
|
||||
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
||||
private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker,
|
||||
IProcessDownloadDecisions downloadDecisionProcessor,
|
||||
IIndexerFactory indexerFactory,
|
||||
Logger logger)
|
||||
{
|
||||
_downloadDecisionMaker = downloadDecisionMaker;
|
||||
_downloadDecisionProcessor = downloadDecisionProcessor;
|
||||
_indexerFactory = indexerFactory;
|
||||
_logger = logger;
|
||||
|
||||
Post("/push", x => ProcessRelease(ReadResourceFromRequest()));
|
||||
|
||||
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.Protocol).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.PublishDate).NotEmpty();
|
||||
}
|
||||
|
||||
private object ProcessRelease(ReleaseResource release)
|
||||
{
|
||||
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
|
||||
|
||||
var info = release.ToModel();
|
||||
|
||||
info.Guid = "PUSH-" + info.DownloadUrl;
|
||||
|
||||
ResolveIndexer(info);
|
||||
|
||||
var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info });
|
||||
_downloadDecisionProcessor.ProcessDecisions(decisions);
|
||||
|
||||
var firstDecision = decisions.FirstOrDefault();
|
||||
|
||||
if (firstDecision?.RemoteAlbum.ParsedAlbumInfo == null)
|
||||
{
|
||||
throw new ValidationException(new List<ValidationFailure> { new ValidationFailure("Title", "Unable to parse", release.Title) });
|
||||
}
|
||||
|
||||
return MapDecisions(new[] { firstDecision }).First();
|
||||
}
|
||||
|
||||
private void ResolveIndexer(ReleaseInfo release)
|
||||
{
|
||||
if (release.IndexerId == 0 && release.Indexer.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name == release.Indexer);
|
||||
if (indexer != null)
|
||||
{
|
||||
release.IndexerId = indexer.Id;
|
||||
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Push Release {0} not associated with unknown indexer {1}.", release.Title, release.Indexer);
|
||||
}
|
||||
}
|
||||
else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace())
|
||||
{
|
||||
try
|
||||
{
|
||||
var indexer = _indexerFactory.Get(release.IndexerId);
|
||||
release.Indexer = indexer.Name;
|
||||
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
|
||||
}
|
||||
catch (ModelNotFoundException)
|
||||
{
|
||||
_logger.Debug("Push Release {0} not associated with unknown indexer {0}.", release.Title, release.IndexerId);
|
||||
release.IndexerId = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Push Release {0} not associated with an indexer.", release.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
src/Readarr.Api.V1/Indexers/ReleaseResource.cs
Normal file
145
src/Readarr.Api.V1/Indexers/ReleaseResource.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
public class ReleaseResource : RestResource
|
||||
{
|
||||
public string Guid { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public int Age { get; set; }
|
||||
public double AgeHours { get; set; }
|
||||
public double AgeMinutes { get; set; }
|
||||
public long Size { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public string SubGroup { get; set; }
|
||||
public string ReleaseHash { get; set; }
|
||||
public string Title { get; set; }
|
||||
public bool Discography { get; set; }
|
||||
public bool SceneSource { get; set; }
|
||||
public string AirDate { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
public string AlbumTitle { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public bool TemporarilyRejected { get; set; }
|
||||
public bool Rejected { get; set; }
|
||||
public IEnumerable<string> Rejections { get; set; }
|
||||
public DateTime PublishDate { get; set; }
|
||||
public string CommentUrl { get; set; }
|
||||
public string DownloadUrl { get; set; }
|
||||
public string InfoUrl { get; set; }
|
||||
public bool DownloadAllowed { get; set; }
|
||||
public int ReleaseWeight { get; set; }
|
||||
public int PreferredWordScore { get; set; }
|
||||
|
||||
public string MagnetUrl { get; set; }
|
||||
public string InfoHash { get; set; }
|
||||
public int? Seeders { get; set; }
|
||||
public int? Leechers { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
|
||||
// Sent when queuing an unknown release
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
|
||||
// [JsonIgnore]
|
||||
public int? ArtistId { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
|
||||
// [JsonIgnore]
|
||||
public int? AlbumId { get; set; }
|
||||
}
|
||||
|
||||
public static class ReleaseResourceMapper
|
||||
{
|
||||
public static ReleaseResource ToResource(this DownloadDecision model)
|
||||
{
|
||||
var releaseInfo = model.RemoteAlbum.Release;
|
||||
var parsedAlbumInfo = model.RemoteAlbum.ParsedAlbumInfo;
|
||||
var remoteAlbum = model.RemoteAlbum;
|
||||
var torrentInfo = (model.RemoteAlbum.Release as TorrentInfo) ?? new TorrentInfo();
|
||||
|
||||
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?)
|
||||
return new ReleaseResource
|
||||
{
|
||||
Guid = releaseInfo.Guid,
|
||||
Quality = parsedAlbumInfo.Quality,
|
||||
|
||||
//QualityWeight
|
||||
Age = releaseInfo.Age,
|
||||
AgeHours = releaseInfo.AgeHours,
|
||||
AgeMinutes = releaseInfo.AgeMinutes,
|
||||
Size = releaseInfo.Size,
|
||||
IndexerId = releaseInfo.IndexerId,
|
||||
Indexer = releaseInfo.Indexer,
|
||||
ReleaseGroup = parsedAlbumInfo.ReleaseGroup,
|
||||
ReleaseHash = parsedAlbumInfo.ReleaseHash,
|
||||
Title = releaseInfo.Title,
|
||||
ArtistName = parsedAlbumInfo.ArtistName,
|
||||
AlbumTitle = parsedAlbumInfo.AlbumTitle,
|
||||
Discography = parsedAlbumInfo.Discography,
|
||||
Approved = model.Approved,
|
||||
TemporarilyRejected = model.TemporarilyRejected,
|
||||
Rejected = model.Rejected,
|
||||
Rejections = model.Rejections.Select(r => r.Reason).ToList(),
|
||||
PublishDate = releaseInfo.PublishDate,
|
||||
CommentUrl = releaseInfo.CommentUrl,
|
||||
DownloadUrl = releaseInfo.DownloadUrl,
|
||||
InfoUrl = releaseInfo.InfoUrl,
|
||||
DownloadAllowed = remoteAlbum.DownloadAllowed,
|
||||
|
||||
//ReleaseWeight
|
||||
PreferredWordScore = remoteAlbum.PreferredWordScore,
|
||||
|
||||
MagnetUrl = torrentInfo.MagnetUrl,
|
||||
InfoHash = torrentInfo.InfoHash,
|
||||
Seeders = torrentInfo.Seeders,
|
||||
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
|
||||
Protocol = releaseInfo.DownloadProtocol,
|
||||
};
|
||||
}
|
||||
|
||||
public static ReleaseInfo ToModel(this ReleaseResource resource)
|
||||
{
|
||||
ReleaseInfo model;
|
||||
|
||||
if (resource.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
model = new TorrentInfo
|
||||
{
|
||||
MagnetUrl = resource.MagnetUrl,
|
||||
InfoHash = resource.InfoHash,
|
||||
Seeders = resource.Seeders,
|
||||
Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
model = new ReleaseInfo();
|
||||
}
|
||||
|
||||
model.Guid = resource.Guid;
|
||||
model.Title = resource.Title;
|
||||
model.Size = resource.Size;
|
||||
model.DownloadUrl = resource.DownloadUrl;
|
||||
model.InfoUrl = resource.InfoUrl;
|
||||
model.CommentUrl = resource.CommentUrl;
|
||||
model.IndexerId = resource.IndexerId;
|
||||
model.Indexer = resource.Indexer;
|
||||
model.DownloadProtocol = resource.Protocol;
|
||||
model.PublishDate = resource.PublishDate.ToUniversalTime();
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/Readarr.Api.V1/Logs/LogFileModule.cs
Normal file
42
src/Readarr.Api.V1/Logs/LogFileModule.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Readarr.Api.V1.Logs
|
||||
{
|
||||
public class LogFileModule : LogFileModuleBase
|
||||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public LogFileModule(IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider)
|
||||
: base(diskProvider, configFileProvider, "")
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetLogFiles()
|
||||
{
|
||||
return _diskProvider.GetFiles(_appFolderInfo.GetLogFolder(), SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
|
||||
protected override string GetLogFilePath(string filename)
|
||||
{
|
||||
return Path.Combine(_appFolderInfo.GetLogFolder(), filename);
|
||||
}
|
||||
|
||||
protected override string DownloadUrlRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
return "logfile";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/Readarr.Api.V1/Logs/LogFileModuleBase.cs
Normal file
77
src/Readarr.Api.V1/Logs/LogFileModuleBase.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Logs
|
||||
{
|
||||
public abstract class LogFileModuleBase : ReadarrRestModule<LogFileResource>
|
||||
{
|
||||
protected const string LOGFILE_ROUTE = @"/(?<filename>[-.a-zA-Z0-9]+?\.txt)";
|
||||
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public LogFileModuleBase(IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider,
|
||||
string route)
|
||||
: base("log/file" + route)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_configFileProvider = configFileProvider;
|
||||
GetResourceAll = GetLogFilesResponse;
|
||||
|
||||
Get(LOGFILE_ROUTE, options => GetLogFileResponse(options.filename));
|
||||
}
|
||||
|
||||
private List<LogFileResource> GetLogFilesResponse()
|
||||
{
|
||||
var result = new List<LogFileResource>();
|
||||
|
||||
var files = GetLogFiles().ToList();
|
||||
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
var file = files[i];
|
||||
var filename = Path.GetFileName(file);
|
||||
|
||||
result.Add(new LogFileResource
|
||||
{
|
||||
Id = i + 1,
|
||||
Filename = filename,
|
||||
LastWriteTime = _diskProvider.FileGetLastWrite(file),
|
||||
ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, Resource, filename),
|
||||
DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename)
|
||||
});
|
||||
}
|
||||
|
||||
return result.OrderByDescending(l => l.LastWriteTime).ToList();
|
||||
}
|
||||
|
||||
private object GetLogFileResponse(string filename)
|
||||
{
|
||||
LogManager.Flush();
|
||||
|
||||
var filePath = GetLogFilePath(filename);
|
||||
|
||||
if (!_diskProvider.FileExists(filePath))
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
var data = _diskProvider.ReadAllText(filePath);
|
||||
|
||||
return new TextResponse(data);
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<string> GetLogFiles();
|
||||
protected abstract string GetLogFilePath(string filename);
|
||||
|
||||
protected abstract string DownloadUrlRoot { get; }
|
||||
}
|
||||
}
|
||||
13
src/Readarr.Api.V1/Logs/LogFileResource.cs
Normal file
13
src/Readarr.Api.V1/Logs/LogFileResource.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Logs
|
||||
{
|
||||
public class LogFileResource : RestResource
|
||||
{
|
||||
public string Filename { get; set; }
|
||||
public DateTime LastWriteTime { get; set; }
|
||||
public string ContentsUrl { get; set; }
|
||||
public string DownloadUrl { get; set; }
|
||||
}
|
||||
}
|
||||
63
src/Readarr.Api.V1/Logs/LogModule.cs
Normal file
63
src/Readarr.Api.V1/Logs/LogModule.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Logs
|
||||
{
|
||||
public class LogModule : ReadarrRestModule<LogResource>
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
|
||||
public LogModule(ILogService logService)
|
||||
{
|
||||
_logService = logService;
|
||||
GetResourcePaged = GetLogs;
|
||||
}
|
||||
|
||||
private PagingResource<LogResource> GetLogs(PagingResource<LogResource> pagingResource)
|
||||
{
|
||||
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>();
|
||||
|
||||
if (pageSpec.SortKey == "time")
|
||||
{
|
||||
pageSpec.SortKey = "id";
|
||||
}
|
||||
|
||||
var levelFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "level");
|
||||
|
||||
if (levelFilter != null)
|
||||
{
|
||||
switch (levelFilter.Value)
|
||||
{
|
||||
case "fatal":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal");
|
||||
break;
|
||||
case "error":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error");
|
||||
break;
|
||||
case "warn":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn");
|
||||
break;
|
||||
case "info":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info");
|
||||
break;
|
||||
case "debug":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug");
|
||||
break;
|
||||
case "trace":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource);
|
||||
|
||||
if (pageSpec.SortKey == "id")
|
||||
{
|
||||
response.SortKey = "time";
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Readarr.Api.V1/Logs/LogResource.cs
Normal file
39
src/Readarr.Api.V1/Logs/LogResource.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Logs
|
||||
{
|
||||
public class LogResource : RestResource
|
||||
{
|
||||
public DateTime Time { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public string ExceptionType { get; set; }
|
||||
public string Level { get; set; }
|
||||
public string Logger { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string Method { get; set; }
|
||||
}
|
||||
|
||||
public static class LogResourceMapper
|
||||
{
|
||||
public static LogResource ToResource(this Log model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new LogResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Time = model.Time,
|
||||
Exception = model.Exception,
|
||||
ExceptionType = model.ExceptionType,
|
||||
Level = model.Level.ToLowerInvariant(),
|
||||
Logger = model.Logger,
|
||||
Message = model.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Readarr.Api.V1/Logs/UpdateLogFileModule.cs
Normal file
51
src/Readarr.Api.V1/Logs/UpdateLogFileModule.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Readarr.Api.V1.Logs
|
||||
{
|
||||
public class UpdateLogFileModule : LogFileModuleBase
|
||||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public UpdateLogFileModule(IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider)
|
||||
: base(diskProvider, configFileProvider, "/update")
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetLogFiles()
|
||||
{
|
||||
if (!_diskProvider.FolderExists(_appFolderInfo.GetUpdateLogFolder()))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
return _diskProvider.GetFiles(_appFolderInfo.GetUpdateLogFolder(), SearchOption.TopDirectoryOnly)
|
||||
.Where(f => Regex.IsMatch(Path.GetFileName(f), LOGFILE_ROUTE.TrimStart('/'), RegexOptions.IgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override string GetLogFilePath(string filename)
|
||||
{
|
||||
return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), filename);
|
||||
}
|
||||
|
||||
protected override string DownloadUrlRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
return "updatelogfile";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Readarr.Api.V1/ManualImport/ManualImportModule.cs
Normal file
90
src/Readarr.Api.V1/ManualImport/ManualImportModule.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NLog;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.ManualImport
|
||||
{
|
||||
public class ManualImportModule : ReadarrRestModule<ManualImportResource>
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IReleaseService _releaseService;
|
||||
private readonly IManualImportService _manualImportService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManualImportModule(IManualImportService manualImportService,
|
||||
IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IReleaseService releaseService,
|
||||
Logger logger)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_albumService = albumService;
|
||||
_releaseService = releaseService;
|
||||
_manualImportService = manualImportService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceAll = GetMediaFiles;
|
||||
|
||||
Put("/", options =>
|
||||
{
|
||||
var resource = Request.Body.FromJson<List<ManualImportResource>>();
|
||||
return ResponseWithCode(UpdateImportItems(resource), HttpStatusCode.Accepted);
|
||||
});
|
||||
}
|
||||
|
||||
private List<ManualImportResource> GetMediaFiles()
|
||||
{
|
||||
var folder = (string)Request.Query.folder;
|
||||
var downloadId = (string)Request.Query.downloadId;
|
||||
var filter = Request.GetBooleanQueryParameter("filterExistingFiles", true) ? FilterFilesType.Matched : FilterFilesType.None;
|
||||
var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true);
|
||||
|
||||
return _manualImportService.GetMediaFiles(folder, downloadId, filter, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList();
|
||||
}
|
||||
|
||||
private ManualImportResource AddQualityWeight(ManualImportResource item)
|
||||
{
|
||||
if (item.Quality != null)
|
||||
{
|
||||
item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight;
|
||||
item.QualityWeight += item.Quality.Revision.Real * 10;
|
||||
item.QualityWeight += item.Quality.Revision.Version;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<ManualImportResource> UpdateImportItems(List<ManualImportResource> resources)
|
||||
{
|
||||
var items = new List<ManualImportItem>();
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
items.Add(new ManualImportItem
|
||||
{
|
||||
Id = resource.Id,
|
||||
Path = resource.Path,
|
||||
Name = resource.Name,
|
||||
Size = resource.Size,
|
||||
Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id),
|
||||
Album = resource.Album == null ? null : _albumService.GetAlbum(resource.Album.Id),
|
||||
Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId),
|
||||
Quality = resource.Quality,
|
||||
DownloadId = resource.DownloadId,
|
||||
AdditionalFile = resource.AdditionalFile,
|
||||
ReplaceExistingFiles = resource.ReplaceExistingFiles,
|
||||
DisableReleaseSwitching = resource.DisableReleaseSwitching
|
||||
});
|
||||
}
|
||||
|
||||
return _manualImportService.UpdateItems(items).Select(x => x.ToResource()).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/Readarr.Api.V1/ManualImport/ManualImportResource.cs
Normal file
69
src/Readarr.Api.V1/ManualImport/ManualImportResource.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.Albums;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Api.V1.Tracks;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.ManualImport
|
||||
{
|
||||
public class ManualImportResource : RestResource
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public string Name { get; set; }
|
||||
public long Size { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
public AlbumResource Album { get; set; }
|
||||
public int AlbumReleaseId { get; set; }
|
||||
public List<TrackResource> Tracks { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public ParsedTrackInfo AudioTags { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool ReplaceExistingFiles { get; set; }
|
||||
public bool DisableReleaseSwitching { get; set; }
|
||||
}
|
||||
|
||||
public static class ManualImportResourceMapper
|
||||
{
|
||||
public static ManualImportResource ToResource(this ManualImportItem model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ManualImportResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Path = model.Path,
|
||||
Name = model.Name,
|
||||
Size = model.Size,
|
||||
Artist = model.Artist.ToResource(),
|
||||
Album = model.Album.ToResource(),
|
||||
AlbumReleaseId = model.Release?.Id ?? 0,
|
||||
Tracks = model.Tracks.ToResource(),
|
||||
Quality = model.Quality,
|
||||
|
||||
//QualityWeight
|
||||
DownloadId = model.DownloadId,
|
||||
Rejections = model.Rejections,
|
||||
AudioTags = model.Tags,
|
||||
AdditionalFile = model.AdditionalFile,
|
||||
ReplaceExistingFiles = model.ReplaceExistingFiles,
|
||||
DisableReleaseSwitching = model.DisableReleaseSwitching
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ManualImportResource> ToResource(this IEnumerable<ManualImportItem> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/Readarr.Api.V1/MediaCovers/MediaCoverModule.cs
Normal file
71
src/Readarr.Api.V1/MediaCovers/MediaCoverModule.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.MediaCovers
|
||||
{
|
||||
public class MediaCoverModule : ReadarrV1Module
|
||||
{
|
||||
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 readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||
: base("MediaCover")
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
|
||||
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)
|
||||
{
|
||||
var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", artistId.ToString(), filename);
|
||||
|
||||
if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0)
|
||||
{
|
||||
// Return the full sized image if someone requests a non-existing resized one.
|
||||
// TODO: This code can be removed later once everyone had the update for a while.
|
||||
var basefilePath = RegexResizedImage.Replace(filePath, "");
|
||||
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
filePath = basefilePath;
|
||||
}
|
||||
|
||||
return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath));
|
||||
}
|
||||
|
||||
private object GetAlbumMediaCover(int albumId, string filename)
|
||||
{
|
||||
var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Albums", albumId.ToString(), filename);
|
||||
|
||||
if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0)
|
||||
{
|
||||
// Return the full sized image if someone requests a non-existing resized one.
|
||||
// TODO: This code can be removed later once everyone had the update for a while.
|
||||
var basefilePath = RegexResizedImage.Replace(filePath, "");
|
||||
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
filePath = basefilePath;
|
||||
}
|
||||
|
||||
return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Readarr.Api.V1/Metadata/MetadataModule.cs
Normal file
24
src/Readarr.Api.V1/Metadata/MetadataModule.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using NzbDrone.Core.Extras.Metadata;
|
||||
|
||||
namespace Readarr.Api.V1.Metadata
|
||||
{
|
||||
public class MetadataModule : ProviderModuleBase<MetadataResource, IMetadata, MetadataDefinition>
|
||||
{
|
||||
public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper();
|
||||
|
||||
public MetadataModule(IMetadataFactory metadataFactory)
|
||||
: base(metadataFactory, "metadata", ResourceMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Validate(MetadataDefinition definition, bool includeWarnings)
|
||||
{
|
||||
if (!definition.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Readarr.Api.V1/Metadata/MetadataResource.cs
Normal file
40
src/Readarr.Api.V1/Metadata/MetadataResource.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using NzbDrone.Core.Extras.Metadata;
|
||||
|
||||
namespace Readarr.Api.V1.Metadata
|
||||
{
|
||||
public class MetadataResource : ProviderResource
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
}
|
||||
|
||||
public class MetadataResourceMapper : ProviderResourceMapper<MetadataResource, MetadataDefinition>
|
||||
{
|
||||
public override MetadataResource ToResource(MetadataDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resource = base.ToResource(definition);
|
||||
|
||||
resource.Enable = definition.Enable;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public override MetadataDefinition ToModel(MetadataResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var definition = base.ToModel(resource);
|
||||
|
||||
definition.Enable = resource.Enable;
|
||||
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Readarr.Api.V1/Notifications/NotificationModule.cs
Normal file
24
src/Readarr.Api.V1/Notifications/NotificationModule.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using NzbDrone.Core.Notifications;
|
||||
|
||||
namespace Readarr.Api.V1.Notifications
|
||||
{
|
||||
public class NotificationModule : ProviderModuleBase<NotificationResource, INotification, NotificationDefinition>
|
||||
{
|
||||
public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
|
||||
|
||||
public NotificationModule(NotificationFactory notificationFactory)
|
||||
: base(notificationFactory, "notification", ResourceMapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Validate(NotificationDefinition definition, bool includeWarnings)
|
||||
{
|
||||
if (!definition.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Readarr.Api.V1/Notifications/NotificationResource.cs
Normal file
90
src/Readarr.Api.V1/Notifications/NotificationResource.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using NzbDrone.Core.Notifications;
|
||||
|
||||
namespace Readarr.Api.V1.Notifications
|
||||
{
|
||||
public class NotificationResource : ProviderResource
|
||||
{
|
||||
public string Link { get; set; }
|
||||
public bool OnGrab { get; set; }
|
||||
public bool OnReleaseImport { get; set; }
|
||||
public bool OnUpgrade { get; set; }
|
||||
public bool OnRename { get; set; }
|
||||
public bool OnHealthIssue { get; set; }
|
||||
public bool OnDownloadFailure { get; set; }
|
||||
public bool OnImportFailure { get; set; }
|
||||
public bool OnTrackRetag { get; set; }
|
||||
public bool SupportsOnGrab { get; set; }
|
||||
public bool SupportsOnReleaseImport { get; set; }
|
||||
public bool SupportsOnUpgrade { get; set; }
|
||||
public bool SupportsOnRename { get; set; }
|
||||
public bool SupportsOnHealthIssue { get; set; }
|
||||
public bool IncludeHealthWarnings { get; set; }
|
||||
public bool SupportsOnDownloadFailure { get; set; }
|
||||
public bool SupportsOnImportFailure { get; set; }
|
||||
public bool SupportsOnTrackRetag { get; set; }
|
||||
public string TestCommand { get; set; }
|
||||
}
|
||||
|
||||
public class NotificationResourceMapper : ProviderResourceMapper<NotificationResource, NotificationDefinition>
|
||||
{
|
||||
public override NotificationResource ToResource(NotificationDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return default(NotificationResource);
|
||||
}
|
||||
|
||||
var resource = base.ToResource(definition);
|
||||
|
||||
resource.OnGrab = definition.OnGrab;
|
||||
resource.OnReleaseImport = definition.OnReleaseImport;
|
||||
resource.OnUpgrade = definition.OnUpgrade;
|
||||
resource.OnRename = definition.OnRename;
|
||||
resource.OnHealthIssue = definition.OnHealthIssue;
|
||||
resource.OnDownloadFailure = definition.OnDownloadFailure;
|
||||
resource.OnImportFailure = definition.OnImportFailure;
|
||||
resource.OnTrackRetag = definition.OnTrackRetag;
|
||||
resource.SupportsOnGrab = definition.SupportsOnGrab;
|
||||
resource.SupportsOnReleaseImport = definition.SupportsOnReleaseImport;
|
||||
resource.SupportsOnUpgrade = definition.SupportsOnUpgrade;
|
||||
resource.SupportsOnRename = definition.SupportsOnRename;
|
||||
resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue;
|
||||
resource.IncludeHealthWarnings = definition.IncludeHealthWarnings;
|
||||
resource.SupportsOnDownloadFailure = definition.SupportsOnDownloadFailure;
|
||||
resource.SupportsOnImportFailure = definition.SupportsOnImportFailure;
|
||||
resource.SupportsOnTrackRetag = definition.SupportsOnTrackRetag;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public override NotificationDefinition ToModel(NotificationResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return default(NotificationDefinition);
|
||||
}
|
||||
|
||||
var definition = base.ToModel(resource);
|
||||
|
||||
definition.OnGrab = resource.OnGrab;
|
||||
definition.OnReleaseImport = resource.OnReleaseImport;
|
||||
definition.OnUpgrade = resource.OnUpgrade;
|
||||
definition.OnRename = resource.OnRename;
|
||||
definition.OnHealthIssue = resource.OnHealthIssue;
|
||||
definition.OnDownloadFailure = resource.OnDownloadFailure;
|
||||
definition.OnImportFailure = resource.OnImportFailure;
|
||||
definition.OnTrackRetag = resource.OnTrackRetag;
|
||||
definition.SupportsOnGrab = resource.SupportsOnGrab;
|
||||
definition.SupportsOnReleaseImport = resource.SupportsOnReleaseImport;
|
||||
definition.SupportsOnUpgrade = resource.SupportsOnUpgrade;
|
||||
definition.SupportsOnRename = resource.SupportsOnRename;
|
||||
definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue;
|
||||
definition.IncludeHealthWarnings = resource.IncludeHealthWarnings;
|
||||
definition.SupportsOnDownloadFailure = resource.SupportsOnDownloadFailure;
|
||||
definition.SupportsOnImportFailure = resource.SupportsOnImportFailure;
|
||||
definition.SupportsOnTrackRetag = resource.SupportsOnTrackRetag;
|
||||
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Readarr.Api.V1/Parse/ParseModule.cs
Normal file
51
src/Readarr.Api.V1/Parse/ParseModule.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using NzbDrone.Core.Parser;
|
||||
using Readarr.Api.V1.Albums;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Parse
|
||||
{
|
||||
public class ParseModule : ReadarrRestModule<ParseResource>
|
||||
{
|
||||
private readonly IParsingService _parsingService;
|
||||
|
||||
public ParseModule(IParsingService parsingService)
|
||||
{
|
||||
_parsingService = parsingService;
|
||||
|
||||
GetResourceSingle = Parse;
|
||||
}
|
||||
|
||||
private ParseResource Parse()
|
||||
{
|
||||
var title = Request.Query.Title.Value as string;
|
||||
var parsedAlbumInfo = Parser.ParseAlbumTitle(title);
|
||||
|
||||
if (parsedAlbumInfo == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var remoteAlbum = _parsingService.Map(parsedAlbumInfo);
|
||||
|
||||
if (remoteAlbum != null)
|
||||
{
|
||||
return new ParseResource
|
||||
{
|
||||
Title = title,
|
||||
ParsedAlbumInfo = remoteAlbum.ParsedAlbumInfo,
|
||||
Artist = remoteAlbum.Artist.ToResource(),
|
||||
Albums = remoteAlbum.Albums.ToResource()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ParseResource
|
||||
{
|
||||
Title = title,
|
||||
ParsedAlbumInfo = parsedAlbumInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Readarr.Api.V1/Parse/ParseResource.cs
Normal file
16
src/Readarr.Api.V1/Parse/ParseResource.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Readarr.Api.V1.Albums;
|
||||
using Readarr.Api.V1.Artist;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Parse
|
||||
{
|
||||
public class ParseResource : RestResource
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
public List<AlbumResource> Albums { get; set; }
|
||||
}
|
||||
}
|
||||
86
src/Readarr.Api.V1/Profiles/Delay/DelayProfileModule.cs
Normal file
86
src/Readarr.Api.V1/Profiles/Delay/DelayProfileModule.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.REST;
|
||||
using Readarr.Http.Validation;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Delay
|
||||
{
|
||||
public class DelayProfileModule : ReadarrRestModule<DelayProfileResource>
|
||||
{
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
|
||||
public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator)
|
||||
{
|
||||
_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).EmptyCollection<DelayProfileResource, int>().When(d => d.Id == 1);
|
||||
SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator);
|
||||
SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0);
|
||||
SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0);
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((delayProfile, context) =>
|
||||
{
|
||||
if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent)
|
||||
{
|
||||
context.AddFailure("Either Usenet or Torrent should be enabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int Create(DelayProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _delayProfileService.Add(model);
|
||||
|
||||
return model.Id;
|
||||
}
|
||||
|
||||
private void DeleteProfile(int id)
|
||||
{
|
||||
if (id == 1)
|
||||
{
|
||||
throw new MethodNotAllowedException("Cannot delete global delay profile");
|
||||
}
|
||||
|
||||
_delayProfileService.Delete(id);
|
||||
}
|
||||
|
||||
private void Update(DelayProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
_delayProfileService.Update(model);
|
||||
}
|
||||
|
||||
private DelayProfileResource GetById(int id)
|
||||
{
|
||||
return _delayProfileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<DelayProfileResource> GetAll()
|
||||
{
|
||||
return _delayProfileService.All().ToResource();
|
||||
}
|
||||
|
||||
private object Reorder(int id)
|
||||
{
|
||||
ValidateId(id);
|
||||
|
||||
var afterIdQuery = Request.Query.After;
|
||||
int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null;
|
||||
|
||||
return _delayProfileService.Reorder(id, afterId).ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/Readarr.Api.V1/Profiles/Delay/DelayProfileResource.cs
Normal file
69
src/Readarr.Api.V1/Profiles/Delay/DelayProfileResource.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Delay
|
||||
{
|
||||
public class DelayProfileResource : RestResource
|
||||
{
|
||||
public bool EnableUsenet { get; set; }
|
||||
public bool EnableTorrent { get; set; }
|
||||
public DownloadProtocol PreferredProtocol { get; set; }
|
||||
public int UsenetDelay { get; set; }
|
||||
public int TorrentDelay { get; set; }
|
||||
public int Order { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
}
|
||||
|
||||
public static class DelayProfileResourceMapper
|
||||
{
|
||||
public static DelayProfileResource ToResource(this DelayProfile model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DelayProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
EnableUsenet = model.EnableUsenet,
|
||||
EnableTorrent = model.EnableTorrent,
|
||||
PreferredProtocol = model.PreferredProtocol,
|
||||
UsenetDelay = model.UsenetDelay,
|
||||
TorrentDelay = model.TorrentDelay,
|
||||
Order = model.Order,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static DelayProfile ToModel(this DelayProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DelayProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
EnableUsenet = resource.EnableUsenet,
|
||||
EnableTorrent = resource.EnableTorrent,
|
||||
PreferredProtocol = resource.PreferredProtocol,
|
||||
UsenetDelay = resource.UsenetDelay,
|
||||
TorrentDelay = resource.TorrentDelay,
|
||||
Order = resource.Order,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<DelayProfileResource> ToResource(this IEnumerable<DelayProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Metadata
|
||||
{
|
||||
public class MetadataProfileModule : ReadarrRestModule<MetadataProfileResource>
|
||||
{
|
||||
private readonly IMetadataProfileService _profileService;
|
||||
|
||||
public MetadataProfileModule(IMetadataProfileService profileService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
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.SecondaryAlbumTypes).MustHaveAllowedSecondaryType();
|
||||
SharedValidator.RuleFor(c => c.ReleaseStatuses).MustHaveAllowedReleaseStatus();
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetById;
|
||||
UpdateResource = Update;
|
||||
CreateResource = Create;
|
||||
DeleteResource = DeleteProfile;
|
||||
}
|
||||
|
||||
private int Create(MetadataProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
return model.Id;
|
||||
}
|
||||
|
||||
private void DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
}
|
||||
|
||||
private void Update(MetadataProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
}
|
||||
|
||||
private MetadataProfileResource GetById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<MetadataProfileResource> GetAll()
|
||||
{
|
||||
var profiles = _profileService.All().ToResource();
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileResource.cs
Normal file
159
src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileResource.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Metadata
|
||||
{
|
||||
public class MetadataProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<ProfilePrimaryAlbumTypeItemResource> PrimaryAlbumTypes { get; set; }
|
||||
public List<ProfileSecondaryAlbumTypeItemResource> SecondaryAlbumTypes { get; set; }
|
||||
public List<ProfileReleaseStatusItemResource> ReleaseStatuses { get; set; }
|
||||
}
|
||||
|
||||
public class ProfilePrimaryAlbumTypeItemResource : RestResource
|
||||
{
|
||||
public NzbDrone.Core.Music.PrimaryAlbumType AlbumType { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileSecondaryAlbumTypeItemResource : RestResource
|
||||
{
|
||||
public NzbDrone.Core.Music.SecondaryAlbumType AlbumType { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileReleaseStatusItemResource : RestResource
|
||||
{
|
||||
public NzbDrone.Core.Music.ReleaseStatus ReleaseStatus { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
public static class MetadataProfileResourceMapper
|
||||
{
|
||||
public static MetadataProfileResource ToResource(this MetadataProfile model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MetadataProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
PrimaryAlbumTypes = model.PrimaryAlbumTypes.ConvertAll(ToResource),
|
||||
SecondaryAlbumTypes = model.SecondaryAlbumTypes.ConvertAll(ToResource),
|
||||
ReleaseStatuses = model.ReleaseStatuses.ConvertAll(ToResource)
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfilePrimaryAlbumTypeItemResource ToResource(this ProfilePrimaryAlbumTypeItem model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProfilePrimaryAlbumTypeItemResource
|
||||
{
|
||||
AlbumType = model.PrimaryAlbumType,
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileSecondaryAlbumTypeItemResource ToResource(this ProfileSecondaryAlbumTypeItem model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProfileSecondaryAlbumTypeItemResource
|
||||
{
|
||||
AlbumType = model.SecondaryAlbumType,
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileReleaseStatusItemResource ToResource(this ProfileReleaseStatusItem model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProfileReleaseStatusItemResource
|
||||
{
|
||||
ReleaseStatus = model.ReleaseStatus,
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static MetadataProfile ToModel(this MetadataProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MetadataProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
PrimaryAlbumTypes = resource.PrimaryAlbumTypes.ConvertAll(ToModel),
|
||||
SecondaryAlbumTypes = resource.SecondaryAlbumTypes.ConvertAll(ToModel),
|
||||
ReleaseStatuses = resource.ReleaseStatuses.ConvertAll(ToModel)
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfilePrimaryAlbumTypeItem ToModel(this ProfilePrimaryAlbumTypeItemResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProfilePrimaryAlbumTypeItem
|
||||
{
|
||||
PrimaryAlbumType = (NzbDrone.Core.Music.PrimaryAlbumType)resource.AlbumType.Id,
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileSecondaryAlbumTypeItem ToModel(this ProfileSecondaryAlbumTypeItemResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProfileSecondaryAlbumTypeItem
|
||||
{
|
||||
SecondaryAlbumType = (NzbDrone.Core.Music.SecondaryAlbumType)resource.AlbumType.Id,
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileReleaseStatusItem ToModel(this ProfileReleaseStatusItemResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProfileReleaseStatusItem
|
||||
{
|
||||
ReleaseStatus = (NzbDrone.Core.Music.ReleaseStatus)resource.ReleaseStatus.Id,
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static List<MetadataProfileResource> ToResource(this IEnumerable<MetadataProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Metadata
|
||||
{
|
||||
public class MetadataProfileSchemaModule : ReadarrRestModule<MetadataProfileResource>
|
||||
{
|
||||
public MetadataProfileSchemaModule()
|
||||
: base("/metadataprofile/schema")
|
||||
{
|
||||
GetResourceSingle = GetAll;
|
||||
}
|
||||
|
||||
private MetadataProfileResource GetAll()
|
||||
{
|
||||
var orderedPrimTypes = NzbDrone.Core.Music.PrimaryAlbumType.All
|
||||
.OrderByDescending(l => l.Id)
|
||||
.ToList();
|
||||
|
||||
var orderedSecTypes = NzbDrone.Core.Music.SecondaryAlbumType.All
|
||||
.OrderByDescending(l => l.Id)
|
||||
.ToList();
|
||||
|
||||
var orderedRelStatuses = NzbDrone.Core.Music.ReleaseStatus.All
|
||||
.OrderByDescending(l => l.Id)
|
||||
.ToList();
|
||||
|
||||
var primTypes = orderedPrimTypes
|
||||
.Select(v => new ProfilePrimaryAlbumTypeItem { PrimaryAlbumType = v, Allowed = false })
|
||||
.ToList();
|
||||
|
||||
var secTypes = orderedSecTypes
|
||||
.Select(v => new ProfileSecondaryAlbumTypeItem { SecondaryAlbumType = v, Allowed = false })
|
||||
.ToList();
|
||||
|
||||
var relStatuses = orderedRelStatuses
|
||||
.Select(v => new ProfileReleaseStatusItem { ReleaseStatus = v, Allowed = false })
|
||||
.ToList();
|
||||
|
||||
var profile = new MetadataProfile
|
||||
{
|
||||
PrimaryAlbumTypes = primTypes,
|
||||
SecondaryAlbumTypes = secTypes,
|
||||
ReleaseStatuses = relStatuses
|
||||
};
|
||||
|
||||
return profile.ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/Readarr.Api.V1/Profiles/Metadata/MetadataValidator.cs
Normal file
106
src/Readarr.Api.V1/Profiles/Metadata/MetadataValidator.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Metadata
|
||||
{
|
||||
public static class MetadataValidation
|
||||
{
|
||||
public static IRuleBuilderOptions<T, IList<ProfilePrimaryAlbumTypeItemResource>> MustHaveAllowedPrimaryType<T>(this IRuleBuilder<T, IList<ProfilePrimaryAlbumTypeItemResource>> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
|
||||
return ruleBuilder.SetValidator(new PrimaryTypeValidator<T>());
|
||||
}
|
||||
|
||||
public static IRuleBuilderOptions<T, IList<ProfileSecondaryAlbumTypeItemResource>> MustHaveAllowedSecondaryType<T>(this IRuleBuilder<T, IList<ProfileSecondaryAlbumTypeItemResource>> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
|
||||
return ruleBuilder.SetValidator(new SecondaryTypeValidator<T>());
|
||||
}
|
||||
|
||||
public static IRuleBuilderOptions<T, IList<ProfileReleaseStatusItemResource>> MustHaveAllowedReleaseStatus<T>(this IRuleBuilder<T, IList<ProfileReleaseStatusItemResource>> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
|
||||
return ruleBuilder.SetValidator(new ReleaseStatusValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class PrimaryTypeValidator<T> : PropertyValidator
|
||||
{
|
||||
public PrimaryTypeValidator()
|
||||
: base("Must have at least one allowed primary type")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var list = context.PropertyValue as IList<ProfilePrimaryAlbumTypeItemResource>;
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!list.Any(c => c.Allowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class SecondaryTypeValidator<T> : PropertyValidator
|
||||
{
|
||||
public SecondaryTypeValidator()
|
||||
: base("Must have at least one allowed secondary type")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var list = context.PropertyValue as IList<ProfileSecondaryAlbumTypeItemResource>;
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!list.Any(c => c.Allowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleaseStatusValidator<T> : PropertyValidator
|
||||
{
|
||||
public ReleaseStatusValidator()
|
||||
: base("Must have at least one allowed release status")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var list = context.PropertyValue as IList<ProfileReleaseStatusItemResource>;
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!list.Any(c => c.Allowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Quality
|
||||
{
|
||||
public static class QualityCutoffValidator
|
||||
{
|
||||
public static IRuleBuilderOptions<T, int> ValidCutoff<T>(this IRuleBuilder<T, int> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.SetValidator(new ValidCutoffValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidCutoffValidator<T> : PropertyValidator
|
||||
{
|
||||
public ValidCutoffValidator()
|
||||
: base("Cutoff must be an allowed quality or group")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
int cutoff = (int)context.PropertyValue;
|
||||
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||
var items = instance.Items as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
QualityProfileQualityItemResource cutoffItem = items.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff);
|
||||
|
||||
if (cutoffItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cutoffItem.Allowed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/Readarr.Api.V1/Profiles/Quality/QualityItemsValidator.cs
Normal file
189
src/Readarr.Api.V1/Profiles/Quality/QualityItemsValidator.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Quality
|
||||
{
|
||||
public static class QualityItemsValidator
|
||||
{
|
||||
public static IRuleBuilderOptions<T, IList<QualityProfileQualityItemResource>> ValidItems<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
ruleBuilder.SetValidator(new AllowedValidator<T>());
|
||||
ruleBuilder.SetValidator(new QualityNameValidator<T>());
|
||||
ruleBuilder.SetValidator(new GroupItemValidator<T>());
|
||||
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
|
||||
return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class AllowedValidator<T> : PropertyValidator
|
||||
{
|
||||
public AllowedValidator()
|
||||
: base("Must contain at least one allowed quality")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!list.Any(c => c.Allowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupItemValidator<T> : PropertyValidator
|
||||
{
|
||||
public GroupItemValidator()
|
||||
: base("Groups must contain multiple qualities")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Count <= 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class QualityNameValidator<T> : PropertyValidator
|
||||
{
|
||||
public QualityNameValidator()
|
||||
: base("Individual qualities should not be named")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroupNameValidator<T> : PropertyValidator
|
||||
{
|
||||
public ItemGroupNameValidator()
|
||||
: base("Groups must have a name")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroupIdValidator<T> : PropertyValidator
|
||||
{
|
||||
public ItemGroupIdValidator()
|
||||
: base("Groups must have an ID")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Quality == null && i.Id == 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class UniqueIdValidator<T> : PropertyValidator
|
||||
{
|
||||
public UniqueIdValidator()
|
||||
: base("Groups must have a unique ID")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class UniqueQualityIdValidator<T> : PropertyValidator
|
||||
{
|
||||
public UniqueQualityIdValidator()
|
||||
: base("Qualities can only be used once")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
var qualityIds = new HashSet<int>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Id > 0)
|
||||
{
|
||||
foreach (var quality in item.Items)
|
||||
{
|
||||
if (qualityIds.Contains(quality.Quality.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
qualityIds.Add(quality.Quality.Id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (qualityIds.Contains(item.Quality.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
qualityIds.Add(item.Quality.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/Readarr.Api.V1/Profiles/Quality/QualityProfileModule.cs
Normal file
55
src/Readarr.Api.V1/Profiles/Quality/QualityProfileModule.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Quality
|
||||
{
|
||||
public class ProfileModule : ReadarrRestModule<QualityProfileResource>
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public ProfileModule(IProfileService profileService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
|
||||
SharedValidator.RuleFor(c => c.Items).ValidItems();
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetById;
|
||||
UpdateResource = Update;
|
||||
CreateResource = Create;
|
||||
DeleteResource = DeleteProfile;
|
||||
}
|
||||
|
||||
private int Create(QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
return model.Id;
|
||||
}
|
||||
|
||||
private void DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
}
|
||||
|
||||
private void Update(QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
}
|
||||
|
||||
private QualityProfileResource GetById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<QualityProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Readarr.Api.V1/Profiles/Quality/QualityProfileResource.cs
Normal file
104
src/Readarr.Api.V1/Profiles/Quality/QualityProfileResource.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Quality
|
||||
{
|
||||
public class QualityProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
}
|
||||
|
||||
public class QualityProfileQualityItemResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
|
||||
public QualityProfileQualityItemResource()
|
||||
{
|
||||
Items = new List<QualityProfileQualityItemResource>();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
{
|
||||
public static QualityProfileResource ToResource(this QualityProfile model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QualityProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
UpgradeAllowed = model.UpgradeAllowed,
|
||||
Cutoff = model.Cutoff,
|
||||
Items = model.Items.ConvertAll(ToResource)
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfileQualityItemResource ToResource(this QualityProfileQualityItem model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QualityProfileQualityItemResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
Quality = model.Quality,
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfile ToModel(this QualityProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QualityProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
UpgradeAllowed = resource.UpgradeAllowed,
|
||||
Cutoff = resource.Cutoff,
|
||||
Items = resource.Items.ConvertAll(ToModel)
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfileQualityItem ToModel(this QualityProfileQualityItemResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QualityProfileQualityItem
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null,
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static List<QualityProfileResource> ToResource(this IEnumerable<QualityProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Quality
|
||||
{
|
||||
public class QualityProfileSchemaModule : ReadarrRestModule<QualityProfileResource>
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public QualityProfileSchemaModule(IProfileService profileService)
|
||||
: base("/qualityprofile/schema")
|
||||
{
|
||||
_profileService = profileService;
|
||||
GetResourceSingle = GetSchema;
|
||||
}
|
||||
|
||||
private QualityProfileResource GetSchema()
|
||||
{
|
||||
QualityProfile qualityProfile = _profileService.GetDefaultProfile(string.Empty);
|
||||
|
||||
return qualityProfile.ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/Readarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs
Normal file
57
src/Readarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Release
|
||||
{
|
||||
public class ReleaseProfileModule : ReadarrRestModule<ReleaseProfileResource>
|
||||
{
|
||||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
|
||||
public ReleaseProfileModule(IReleaseProfileService releaseProfileService)
|
||||
{
|
||||
_releaseProfileService = releaseProfileService;
|
||||
|
||||
GetResourceById = GetById;
|
||||
GetResourceAll = GetAll;
|
||||
CreateResource = Create;
|
||||
UpdateResource = Update;
|
||||
DeleteResource = DeleteById;
|
||||
|
||||
SharedValidator.RuleFor(r => r).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty())
|
||||
{
|
||||
context.AddFailure("Either 'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ReleaseProfileResource GetById(int id)
|
||||
{
|
||||
return _releaseProfileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<ReleaseProfileResource> GetAll()
|
||||
{
|
||||
return _releaseProfileService.All().ToResource();
|
||||
}
|
||||
|
||||
private int Create(ReleaseProfileResource resource)
|
||||
{
|
||||
return _releaseProfileService.Add(resource.ToModel()).Id;
|
||||
}
|
||||
|
||||
private void Update(ReleaseProfileResource resource)
|
||||
{
|
||||
_releaseProfileService.Update(resource.ToModel());
|
||||
}
|
||||
|
||||
private void DeleteById(int id)
|
||||
{
|
||||
_releaseProfileService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Profiles.Release
|
||||
{
|
||||
public class ReleaseProfileResource : RestResource
|
||||
{
|
||||
public string Required { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public List<KeyValuePair<string, int>> Preferred { get; set; }
|
||||
public bool IncludePreferredWhenRenaming { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public ReleaseProfileResource()
|
||||
{
|
||||
Tags = new HashSet<int>();
|
||||
}
|
||||
}
|
||||
|
||||
public static class RestrictionResourceMapper
|
||||
{
|
||||
public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ReleaseProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Required = model.Required,
|
||||
Ignored = model.Ignored,
|
||||
Preferred = model.Preferred,
|
||||
IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ReleaseProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Required = resource.Required,
|
||||
Ignored = resource.Ignored,
|
||||
Preferred = resource.Preferred,
|
||||
IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ReleaseProfileResource> ToResource(this IEnumerable<ReleaseProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/Readarr.Api.V1/ProviderModuleBase.cs
Normal file
213
src/Readarr.Api.V1/ProviderModuleBase.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1
|
||||
{
|
||||
public abstract class ProviderModuleBase<TProviderResource, TProvider, TProviderDefinition> : ReadarrRestModule<TProviderResource>
|
||||
where TProviderDefinition : ProviderDefinition, new()
|
||||
where TProvider : IProvider
|
||||
where TProviderResource : ProviderResource, new()
|
||||
{
|
||||
private readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory;
|
||||
private readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper;
|
||||
|
||||
protected ProviderModuleBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper)
|
||||
: base(resource)
|
||||
{
|
||||
_providerFactory = providerFactory;
|
||||
_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)));
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetProviderById;
|
||||
CreateResource = CreateProvider;
|
||||
UpdateResource = UpdateProvider;
|
||||
DeleteResource = DeleteProvider;
|
||||
|
||||
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.Implementation).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty();
|
||||
|
||||
PostValidator.RuleFor(c => c.Fields).NotNull();
|
||||
}
|
||||
|
||||
private TProviderResource GetProviderById(int id)
|
||||
{
|
||||
var definition = _providerFactory.Get(id);
|
||||
_providerFactory.SetProviderCharacteristics(definition);
|
||||
|
||||
return _resourceMapper.ToResource(definition);
|
||||
}
|
||||
|
||||
private List<TProviderResource> GetAll()
|
||||
{
|
||||
var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName);
|
||||
|
||||
var result = new List<TProviderResource>(providerDefinitions.Count());
|
||||
|
||||
foreach (var definition in providerDefinitions)
|
||||
{
|
||||
_providerFactory.SetProviderCharacteristics(definition);
|
||||
|
||||
result.Add(_resourceMapper.ToResource(definition));
|
||||
}
|
||||
|
||||
return result.OrderBy(p => p.Name).ToList();
|
||||
}
|
||||
|
||||
private int CreateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false);
|
||||
|
||||
if (providerDefinition.Enable)
|
||||
{
|
||||
Test(providerDefinition, false);
|
||||
}
|
||||
|
||||
providerDefinition = _providerFactory.Create(providerDefinition);
|
||||
|
||||
return providerDefinition.Id;
|
||||
}
|
||||
|
||||
private void UpdateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false);
|
||||
|
||||
if (providerDefinition.Enable)
|
||||
{
|
||||
Test(providerDefinition, false);
|
||||
}
|
||||
|
||||
_providerFactory.Update(providerDefinition);
|
||||
}
|
||||
|
||||
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true)
|
||||
{
|
||||
var definition = _resourceMapper.ToModel(providerResource);
|
||||
|
||||
if (validate)
|
||||
{
|
||||
Validate(definition, includeWarnings);
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
private void DeleteProvider(int id)
|
||||
{
|
||||
_providerFactory.Delete(id);
|
||||
}
|
||||
|
||||
private object GetTemplates()
|
||||
{
|
||||
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
|
||||
|
||||
var result = new List<TProviderResource>(defaultDefinitions.Count());
|
||||
|
||||
foreach (var providerDefinition in defaultDefinitions)
|
||||
{
|
||||
var providerResource = _resourceMapper.ToResource(providerDefinition);
|
||||
var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition);
|
||||
|
||||
providerResource.Presets = presetDefinitions.Select(v =>
|
||||
{
|
||||
var presetResource = _resourceMapper.ToResource(v);
|
||||
|
||||
return presetResource as ProviderResource;
|
||||
}).ToList();
|
||||
|
||||
result.Add(providerResource);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private object Test(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true);
|
||||
|
||||
Test(providerDefinition, true);
|
||||
|
||||
return "{}";
|
||||
}
|
||||
|
||||
private object TestAll()
|
||||
{
|
||||
var providerDefinitions = _providerFactory.All()
|
||||
.Where(c => c.Settings.Validate().IsValid && c.Enable)
|
||||
.ToList();
|
||||
var result = new List<ProviderTestAllResult>();
|
||||
|
||||
foreach (var definition in providerDefinitions)
|
||||
{
|
||||
var validationResult = _providerFactory.Test(definition);
|
||||
|
||||
result.Add(new ProviderTestAllResult
|
||||
{
|
||||
Id = definition.Id,
|
||||
ValidationFailures = validationResult.Errors.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
return ResponseWithCode(result, result.Any(c => !c.IsValid) ? HttpStatusCode.BadRequest : HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
private object RequestAction(string action, TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true, false);
|
||||
|
||||
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
|
||||
|
||||
var data = _providerFactory.RequestAction(providerDefinition, action, query);
|
||||
Response resp = data.ToJson();
|
||||
resp.ContentType = "application/json";
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = definition.Settings.Validate();
|
||||
|
||||
VerifyValidationResult(validationResult, includeWarnings);
|
||||
}
|
||||
|
||||
protected virtual void Test(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = _providerFactory.Test(definition);
|
||||
|
||||
VerifyValidationResult(validationResult, includeWarnings);
|
||||
}
|
||||
|
||||
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings)
|
||||
{
|
||||
var result = validationResult as NzbDroneValidationResult;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
result = new NzbDroneValidationResult(validationResult.Errors);
|
||||
}
|
||||
|
||||
if (includeWarnings && (!result.IsValid || result.HasWarnings))
|
||||
{
|
||||
throw new ValidationException(result.Failures);
|
||||
}
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
throw new ValidationException(result.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/Readarr.Api.V1/ProviderResource.cs
Normal file
72
src/Readarr.Api.V1/ProviderResource.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Reflection;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using Readarr.Http.ClientSchema;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1
|
||||
{
|
||||
public class ProviderResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<Field> Fields { get; set; }
|
||||
public string ImplementationName { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public ProviderMessage Message { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public List<ProviderResource> Presets { get; set; }
|
||||
}
|
||||
|
||||
public class ProviderResourceMapper<TProviderResource, TProviderDefinition>
|
||||
where TProviderResource : ProviderResource, new()
|
||||
where TProviderDefinition : ProviderDefinition, new()
|
||||
{
|
||||
public virtual TProviderResource ToResource(TProviderDefinition definition)
|
||||
{
|
||||
return new TProviderResource
|
||||
{
|
||||
Id = definition.Id,
|
||||
|
||||
Name = definition.Name,
|
||||
ImplementationName = definition.ImplementationName,
|
||||
Implementation = definition.Implementation,
|
||||
ConfigContract = definition.ConfigContract,
|
||||
Message = definition.Message,
|
||||
Tags = definition.Tags,
|
||||
Fields = SchemaBuilder.ToSchema(definition.Settings),
|
||||
|
||||
InfoLink = string.Format("https://github.com/Readarr/Readarr/wiki/Supported-{0}#{1}",
|
||||
typeof(TProviderResource).Name.Replace("Resource", "s"),
|
||||
definition.Implementation.ToLower())
|
||||
};
|
||||
}
|
||||
|
||||
public virtual TProviderDefinition ToModel(TProviderResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return default(TProviderDefinition);
|
||||
}
|
||||
|
||||
var definition = new TProviderDefinition
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Name = resource.Name,
|
||||
ImplementationName = resource.ImplementationName,
|
||||
Implementation = resource.Implementation,
|
||||
ConfigContract = resource.ConfigContract,
|
||||
Message = resource.Message,
|
||||
Tags = resource.Tags
|
||||
};
|
||||
|
||||
var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract);
|
||||
definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract);
|
||||
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Readarr.Api.V1/ProviderTestAllResult.cs
Normal file
18
src/Readarr.Api.V1/ProviderTestAllResult.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1
|
||||
{
|
||||
public class ProviderTestAllResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool IsValid => ValidationFailures.Empty();
|
||||
public List<ValidationFailure> ValidationFailures { get; set; }
|
||||
|
||||
public ProviderTestAllResult()
|
||||
{
|
||||
ValidationFailures = new List<ValidationFailure>();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Readarr.Api.V1/Qualities/QualityDefinitionModule.cs
Normal file
54
src/Readarr.Api.V1/Qualities/QualityDefinitionModule.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Qualities
|
||||
{
|
||||
public class QualityDefinitionModule : ReadarrRestModule<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Readarr.Api.V1/Qualities/QualityDefinitionResource.cs
Normal file
68
src/Readarr.Api.V1/Qualities/QualityDefinitionResource.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Qualities
|
||||
{
|
||||
public class QualityDefinitionResource : RestResource
|
||||
{
|
||||
public Quality Quality { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public int Weight { get; set; }
|
||||
|
||||
public double? MinSize { get; set; }
|
||||
public double? MaxSize { get; set; }
|
||||
}
|
||||
|
||||
public static class QualityDefinitionResourceMapper
|
||||
{
|
||||
public static QualityDefinitionResource ToResource(this QualityDefinition model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QualityDefinitionResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Quality = model.Quality,
|
||||
Title = model.Title,
|
||||
Weight = model.Weight,
|
||||
MinSize = model.MinSize,
|
||||
MaxSize = model.MaxSize
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityDefinition ToModel(this QualityDefinitionResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QualityDefinition
|
||||
{
|
||||
Id = resource.Id,
|
||||
Quality = resource.Quality,
|
||||
Title = resource.Title,
|
||||
Weight = resource.Weight,
|
||||
MinSize = resource.MinSize,
|
||||
MaxSize = resource.MaxSize
|
||||
};
|
||||
}
|
||||
|
||||
public static List<QualityDefinitionResource> ToResource(this IEnumerable<QualityDefinition> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<QualityDefinition> ToModel(this IEnumerable<QualityDefinitionResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/Readarr.Api.V1/Queue/QueueActionModule.cs
Normal file
185
src/Readarr.Api.V1/Queue/QueueActionModule.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Queue;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Queue
|
||||
{
|
||||
public class QueueActionModule : ReadarrRestModule<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Readarr.Api.V1/Queue/QueueBulkResource.cs
Normal file
9
src/Readarr.Api.V1/Queue/QueueBulkResource.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Readarr.Api.V1.Queue
|
||||
{
|
||||
public class QueueBulkResource
|
||||
{
|
||||
public List<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
68
src/Readarr.Api.V1/Queue/QueueDetailsModule.cs
Normal file
68
src/Readarr.Api.V1/Queue/QueueDetailsModule.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Queue;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Queue
|
||||
{
|
||||
public class QueueDetailsModule : ReadarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>,
|
||||
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
|
||||
{
|
||||
private readonly IQueueService _queueService;
|
||||
private readonly IPendingReleaseService _pendingReleaseService;
|
||||
|
||||
public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
|
||||
: base(broadcastSignalRMessage, "queue/details")
|
||||
{
|
||||
_queueService = queueService;
|
||||
_pendingReleaseService = pendingReleaseService;
|
||||
GetResourceAll = GetQueue;
|
||||
}
|
||||
|
||||
private List<QueueResource> GetQueue()
|
||||
{
|
||||
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
|
||||
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum", true);
|
||||
var queue = _queueService.GetQueue();
|
||||
var pending = _pendingReleaseService.GetPendingQueue();
|
||||
var fullQueue = queue.Concat(pending);
|
||||
|
||||
var artistIdQuery = Request.Query.ArtistId;
|
||||
var albumIdsQuery = Request.Query.AlbumIds;
|
||||
|
||||
if (artistIdQuery.HasValue)
|
||||
{
|
||||
return fullQueue.Where(q => q.Artist?.Id == (int)artistIdQuery).ToResource(includeArtist, includeAlbum);
|
||||
}
|
||||
|
||||
if (albumIdsQuery.HasValue)
|
||||
{
|
||||
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.ToResource(includeArtist, includeAlbum);
|
||||
}
|
||||
|
||||
public void Handle(QueueUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
|
||||
public void Handle(PendingReleasesUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/Readarr.Api.V1/Queue/QueueModule.cs
Normal file
156
src/Readarr.Api.V1/Queue/QueueModule.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Queue;
|
||||
using NzbDrone.SignalR;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.Extensions;
|
||||
|
||||
namespace Readarr.Api.V1.Queue
|
||||
{
|
||||
public class QueueModule : ReadarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>,
|
||||
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
|
||||
{
|
||||
private readonly IQueueService _queueService;
|
||||
private readonly IPendingReleaseService _pendingReleaseService;
|
||||
|
||||
private readonly QualityModelComparer _qualityComparer;
|
||||
|
||||
public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage,
|
||||
IQueueService queueService,
|
||||
IPendingReleaseService pendingReleaseService,
|
||||
QualityProfileService qualityProfileService)
|
||||
: base(broadcastSignalRMessage)
|
||||
{
|
||||
_queueService = queueService;
|
||||
_pendingReleaseService = pendingReleaseService;
|
||||
GetResourcePaged = GetQueue;
|
||||
|
||||
_qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty));
|
||||
}
|
||||
|
||||
private PagingResource<QueueResource> GetQueue(PagingResource<QueueResource> pagingResource)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, bool includeUnknownArtistItems)
|
||||
{
|
||||
var ascending = pagingSpec.SortDirection == SortDirection.Ascending;
|
||||
var orderByFunc = GetOrderByFunc(pagingSpec);
|
||||
|
||||
var queue = _queueService.GetQueue();
|
||||
var filteredQueue = includeUnknownArtistItems ? queue : queue.Where(q => q.Artist != null);
|
||||
var pending = _pendingReleaseService.GetPendingQueue();
|
||||
var fullQueue = filteredQueue.Concat(pending).ToList();
|
||||
IOrderedEnumerable<NzbDrone.Core.Queue.Queue> ordered;
|
||||
|
||||
if (pagingSpec.SortKey == "timeleft")
|
||||
{
|
||||
ordered = ascending
|
||||
? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer())
|
||||
: fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer());
|
||||
}
|
||||
else if (pagingSpec.SortKey == "estimatedCompletionTime")
|
||||
{
|
||||
ordered = ascending
|
||||
? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer())
|
||||
: fullQueue.OrderByDescending(q => q.EstimatedCompletionTime,
|
||||
new EstimatedCompletionTimeComparer());
|
||||
}
|
||||
else if (pagingSpec.SortKey == "protocol")
|
||||
{
|
||||
ordered = ascending
|
||||
? fullQueue.OrderBy(q => q.Protocol)
|
||||
: fullQueue.OrderByDescending(q => q.Protocol);
|
||||
}
|
||||
else if (pagingSpec.SortKey == "indexer")
|
||||
{
|
||||
ordered = ascending
|
||||
? fullQueue.OrderBy(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase)
|
||||
: fullQueue.OrderByDescending(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
else if (pagingSpec.SortKey == "downloadClient")
|
||||
{
|
||||
ordered = ascending
|
||||
? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase)
|
||||
: fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
else if (pagingSpec.SortKey == "quality")
|
||||
{
|
||||
ordered = ascending
|
||||
? fullQueue.OrderBy(q => q.Quality, _qualityComparer)
|
||||
: fullQueue.OrderByDescending(q => q.Quality, _qualityComparer);
|
||||
}
|
||||
else
|
||||
{
|
||||
ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc);
|
||||
}
|
||||
|
||||
ordered = ordered.ThenByDescending(q => q.Size == 0 ? 0 : 100 - (q.Sizeleft / q.Size * 100));
|
||||
|
||||
pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList();
|
||||
pagingSpec.TotalRecords = fullQueue.Count;
|
||||
|
||||
if (pagingSpec.Records.Empty() && pagingSpec.Page > 1)
|
||||
{
|
||||
pagingSpec.Page = (int)Math.Max(Math.Ceiling((decimal)(pagingSpec.TotalRecords / pagingSpec.PageSize)), 1);
|
||||
pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList();
|
||||
}
|
||||
|
||||
return pagingSpec;
|
||||
}
|
||||
|
||||
private Func<NzbDrone.Core.Queue.Queue, object> GetOrderByFunc(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec)
|
||||
{
|
||||
switch (pagingSpec.SortKey)
|
||||
{
|
||||
case "status":
|
||||
return q => q.Status;
|
||||
case "artist.sortName":
|
||||
return q => q.Artist?.SortName;
|
||||
case "title":
|
||||
return q => q.Title;
|
||||
case "album":
|
||||
return q => q.Album;
|
||||
case "album.title":
|
||||
return q => q.Album?.Title;
|
||||
case "album.releaseDate":
|
||||
return q => q.Album?.ReleaseDate;
|
||||
case "quality":
|
||||
return q => q.Quality;
|
||||
case "progress":
|
||||
// Avoid exploding if a download's size is 0
|
||||
return q => 100 - (q.Sizeleft / Math.Max(q.Size * 100, 1));
|
||||
default:
|
||||
return q => q.Timeleft;
|
||||
}
|
||||
}
|
||||
|
||||
private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeArtist, bool includeAlbum)
|
||||
{
|
||||
return queueItem.ToResource(includeArtist, includeAlbum);
|
||||
}
|
||||
|
||||
public void Handle(QueueUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
|
||||
public void Handle(PendingReleasesUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user