New: Lidarr to Readarr

This commit is contained in:
Qstick
2020-02-29 15:51:29 -05:00
parent 7359c2a9fa
commit 3b7eb01918
565 changed files with 1669 additions and 4272 deletions

View 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; }
}
}

View 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);
}
}
}

View 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; }
}
}

View 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;
}
}
}
}

View 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));
}
}
}

View 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);
}
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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
};
}
}
}

View 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; }
}
}

View 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();
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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();
}
}
}

View 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
}
}

View 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();
}
}
}

View 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;
}
}
}
}

View 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));
}
}
}

View 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();
}
}
}

View 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
};
}
}
}

View 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);
}
}
}

View 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()
};
}
}
}

View 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");
}
}
}

View 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();
}
}
}

View 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);
}
}
}
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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
};
}
}
}

View 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);
}
}
}
}

View 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
};
}
}
}

View 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);
}
}
}

View 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,
};
}
}
}

View 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);
}
}
}

View 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,
};
}
}
}

View 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);
}
}
}

View 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,
};
}
}
}

View 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());
}
}
}
}

View 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; }
}
}

View 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
};
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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
};
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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
};
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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
});
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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
};
}
}
}

View 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);
}
}
}

View File

@@ -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();
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}
}

View 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;
}
}
}

View 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";
}
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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;
}
}
}

View 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
};
}
}
}

View 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";
}
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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));
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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
};
}
}
}
}

View 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; }
}
}

View 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();
}
}
}

View 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();
}
}
}

View File

@@ -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;
}
}
}

View 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();
}
}
}

View File

@@ -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();
}
}
}

View 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;
}
}
}

View File

@@ -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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View File

@@ -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();
}
}
}

View 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);
}
}
}

View File

@@ -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();
}
}
}

View 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);
}
}
}
}

View 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;
}
}
}

View 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>();
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Readarr.Api.V1.Queue
{
public class QueueBulkResource
{
public List<int> Ids { get; set; }
}
}

View 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);
}
}
}

View 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