New: Use ASP.NET Core instead of Nancy

pull/906/head
ta264 3 years ago
parent d348232e0d
commit 58ddbcd77e

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

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

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

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace NzbDrone.Common.Serializer
{
@ -15,23 +16,25 @@ namespace NzbDrone.Common.Serializer
public static JsonSerializerOptions GetSerializerSettings()
{
var serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
var settings = new JsonSerializerOptions();
ApplySerializerSettings(settings);
return settings;
}
public static void ApplySerializerSettings(JsonSerializerOptions serializerSettings)
{
serializerSettings.AllowTrailingCommas = true;
serializerSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
serializerSettings.PropertyNameCaseInsensitive = true;
serializerSettings.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
serializerSettings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
serializerSettings.WriteIndented = true;
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
serializerSettings.Converters.Add(new STJVersionConverter());
serializerSettings.Converters.Add(new STJHttpUriConverter());
serializerSettings.Converters.Add(new STJTimeSpanConverter());
serializerSettings.Converters.Add(new STJUtcConverter());
return serializerSettings;
}
public static T Deserialize<T>(string json)
@ -84,5 +87,15 @@ namespace NzbDrone.Common.Serializer
JsonSerializer.Serialize(writer, (object)model, options);
}
}
public static Task SerializeAsync<TModel>(TModel model, Stream outputStream, JsonSerializerOptions options = null)
{
if (options == null)
{
options = SerializerSettings;
}
return JsonSerializer.SerializeAsync(outputStream, (object)model, options);
}
}
}

@ -1,9 +1,7 @@
using System.Collections.Generic;
using Nancy.Bootstrapper;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.SignalR;
using Readarr.Http;
namespace NzbDrone.Host
{
@ -28,8 +26,6 @@ namespace NzbDrone.Host
{
AutoRegisterImplementations<MessageHub>();
Container.Register<INancyBootstrapper, ReadarrBootstrapper>();
if (OsInfo.IsWindows)
{
Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>();

@ -4,7 +4,6 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.6.4" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using NzbDrone.Common.Composition;
namespace NzbDrone.Host
{
public class ControllerActivator : IControllerActivator
{
private readonly IContainer _container;
public ControllerActivator(IContainer container)
{
_container = container;
}
public object Create(ControllerContext context)
{
return _container.Resolve(context.ActionDescriptor.ControllerTypeInfo.AsType());
}
public void Release(ControllerContext context, object controller)
{
// Nothing to do
}
}
}

@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Builder;
namespace NzbDrone.Host.Middleware
{
public interface IAspNetCoreMiddleware
{
int Order { get; }
void Attach(IApplicationBuilder appBuilder);
}
}

@ -1,29 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Nancy.Bootstrapper;
using Nancy.Owin;
namespace NzbDrone.Host.Middleware
{
public class NancyMiddleware : IAspNetCoreMiddleware
{
private readonly INancyBootstrapper _nancyBootstrapper;
public int Order => 2;
public NancyMiddleware(INancyBootstrapper nancyBootstrapper)
{
_nancyBootstrapper = nancyBootstrapper;
}
public void Attach(IApplicationBuilder appBuilder)
{
var options = new NancyOptions
{
Bootstrapper = _nancyBootstrapper,
PerformPassThrough = context => context.Request.Path.StartsWith("/signalr")
};
appBuilder.UseOwin(x => x.UseNancy(options));
}
}
}

@ -1,70 +0,0 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Configuration;
using NzbDrone.SignalR;
namespace NzbDrone.Host.Middleware
{
public class SignalRMiddleware : IAspNetCoreMiddleware
{
private readonly IContainer _container;
private readonly Logger _logger;
private static string API_KEY;
private static string URL_BASE;
public int Order => 1;
public SignalRMiddleware(IContainer container,
IConfigFileProvider configFileProvider,
Logger logger)
{
_container = container;
_logger = logger;
API_KEY = configFileProvider.ApiKey;
URL_BASE = configFileProvider.UrlBase;
}
public void Attach(IApplicationBuilder appBuilder)
{
appBuilder.UseWebSockets();
appBuilder.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/signalr") &&
!context.Request.Path.Value.EndsWith("/negotiate") &&
(!context.Request.Query.ContainsKey("access_token") ||
context.Request.Query["access_token"] != API_KEY))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
return;
}
try
{
await next();
}
catch (OperationCanceledException e)
{
// Demote the exception to trace logging so users don't worry (as much).
_logger.Trace(e);
}
});
appBuilder.UseEndpoints(x =>
{
x.MapHub<MessageHub>(URL_BASE + "/signalr/messages");
});
// This is a side effect of haing multiple IoC containers, TinyIoC and whatever
// Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC
// TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac).
var hubContext = appBuilder.ApplicationServices.GetService<IHubContext<MessageHub>>();
_container.Register(hubContext);
}
}
}

@ -4,42 +4,59 @@ using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Host.AccessControl;
using NzbDrone.Host.Middleware;
using NzbDrone.SignalR;
using Readarr.Api.V1.System;
using Readarr.Http;
using Readarr.Http.Authentication;
using Readarr.Http.ErrorManagement;
using Readarr.Http.Frontend;
using Readarr.Http.Middleware;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
{
public class WebHostController : IHostController
{
private readonly IContainer _container;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigFileProvider _configFileProvider;
private readonly IFirewallAdapter _firewallAdapter;
private readonly IEnumerable<IAspNetCoreMiddleware> _middlewares;
private readonly ReadarrErrorPipeline _errorHandler;
private readonly Logger _logger;
private IWebHost _host;
public WebHostController(IRuntimeInfo runtimeInfo,
public WebHostController(IContainer container,
IRuntimeInfo runtimeInfo,
IConfigFileProvider configFileProvider,
IFirewallAdapter firewallAdapter,
IEnumerable<IAspNetCoreMiddleware> middlewares,
ReadarrErrorPipeline errorHandler,
Logger logger)
{
_container = container;
_runtimeInfo = runtimeInfo;
_configFileProvider = configFileProvider;
_firewallAdapter = firewallAdapter;
_middlewares = middlewares;
_errorHandler = errorHandler;
_logger = logger;
}
@ -103,24 +120,125 @@ namespace NzbDrone.Host
})
.ConfigureServices(services =>
{
// So that we can resolve containers with our TinyIoC services
services.AddSingleton(_container);
services.AddSingleton<IControllerActivator, ControllerActivator>();
// Bits used in our custom middleware
services.AddSingleton(_container.Resolve<ReadarrErrorPipeline>());
services.AddSingleton(_container.Resolve<ICacheableSpecification>());
// Used in authentication
services.AddSingleton(_container.Resolve<IAuthenticationService>());
services.AddRouting(options => options.LowercaseUrls = true);
services.AddResponseCompression();
services.AddCors(options =>
{
options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY,
builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
options.AddPolicy("AllowGet",
builder =>
builder.AllowAnyOrigin()
.WithMethods("GET", "OPTIONS")
.AllowAnyHeader());
});
services
.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddApplicationPart(typeof(SystemController).Assembly)
.AddApplicationPart(typeof(StaticResourceController).Assembly)
.AddJsonOptions(options =>
{
STJson.ApplySerializerSettings(options.JsonSerializerOptions);
});
services
.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
services.AddAuthorization(options =>
{
options.AddPolicy("UI", policy =>
{
policy.AuthenticationSchemes.Add(_configFileProvider.AuthenticationMethod.ToString());
policy.RequireAuthenticatedUser();
});
options.AddPolicy("SignalR", policy =>
{
policy.AuthenticationSchemes.Add("SignalR");
policy.RequireAuthenticatedUser();
});
// Require auth on everything except those marked [AllowAnonymous]
options.FallbackPolicy = new AuthorizationPolicyBuilder("API")
.RequireAuthenticatedUser()
.Build();
});
services.AddAppAuthentication(_configFileProvider);
})
.Configure(app =>
{
app.UseMiddleware<LoggingMiddleware>();
app.UsePathBase(new PathString(_configFileProvider.UrlBase));
app.UseExceptionHandler(new ExceptionHandlerOptions
{
AllowStatusCode404Response = true,
ExceptionHandler = _errorHandler.HandleException
});
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.Properties["host.AppName"] = BuildInfo.AppName;
app.UsePathBase(_configFileProvider.UrlBase);
foreach (var middleWare in _middlewares.OrderBy(c => c.Order))
app.UseMiddleware<VersionMiddleware>();
app.UseMiddleware<UrlBaseMiddleware>(_configFileProvider.UrlBase);
app.UseMiddleware<CacheHeaderMiddleware>();
app.UseMiddleware<IfModifiedMiddleware>();
app.Use((context, next) =>
{
_logger.Debug("Attaching {0} to host", middleWare.GetType().Name);
middleWare.Attach(app);
}
if (context.Request.Path.StartsWithSegments("/api/v1/command", StringComparison.CurrentCultureIgnoreCase))
{
context.Request.EnableBuffering();
}
return next();
});
app.UseWebSockets();
app.UseEndpoints(x =>
{
x.MapHub<MessageHub>("/signalr/messages").RequireAuthorization("SignalR");
x.MapControllers();
});
// This is a side effect of haing multiple IoC containers, TinyIoC and whatever
// Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC
// TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac).
_container.Register(app.ApplicationServices);
_container.Register(app.ApplicationServices.GetService<IHubContext<MessageHub>>());
_container.Register(app.ApplicationServices.GetService<IActionDescriptorCollectionProvider>());
_container.Register(app.ApplicationServices.GetService<EndpointDataSource>());
_container.Register(app.ApplicationServices.GetService<DfaGraphWriter>());
})
.UseContentRoot(Directory.GetCurrentDirectory())
.Build();

@ -34,8 +34,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Order(0)]
public void add_author_without_profileid_should_return_badrequest()
{
IgnoreOnMonoVersions("5.12", "5.14");
EnsureNoAuthor("14586394", "Andrew Hunter Murray");
var author = Author.Lookup("readarr:43765115").Single();
@ -49,8 +47,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Order(0)]
public void add_author_without_path_should_return_badrequest()
{
IgnoreOnMonoVersions("5.12", "5.14");
EnsureNoAuthor("14586394", "Andrew Hunter Murray");
var author = Author.Lookup("readarr:43765115").Single();
@ -109,8 +105,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void get_author_by_unknown_id_should_return_404()
{
IgnoreOnMonoVersions("5.12", "5.14");
var result = Author.InvalidGet(1000000);
}

@ -11,8 +11,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Order(0)]
public void add_downloadclient_without_name_should_return_badrequest()
{
IgnoreOnMonoVersions("5.12", "5.14");
EnsureNoDownloadClient();
var schema = DownloadClients.Schema().First(v => v.Implementation == "UsenetBlackhole");
@ -28,8 +26,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Order(0)]
public void add_downloadclient_without_nzbfolder_should_return_badrequest()
{
IgnoreOnMonoVersions("5.12", "5.14");
EnsureNoDownloadClient();
var schema = DownloadClients.Schema().First(v => v.Implementation == "UsenetBlackhole");
@ -45,8 +41,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Order(0)]
public void add_downloadclient_without_watchfolder_should_return_badrequest()
{
IgnoreOnMonoVersions("5.12", "5.14");
EnsureNoDownloadClient();
var schema = DownloadClients.Schema().First(v => v.Implementation == "UsenetBlackhole");
@ -101,8 +95,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void get_downloadclient_by_unknown_id_should_return_404()
{
IgnoreOnMonoVersions("5.12", "5.14");
var result = DownloadClients.InvalidGet(1000000);
}

@ -35,8 +35,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void should_get_bad_request_if_standard_format_is_empty()
{
IgnoreOnMonoVersions("5.12", "5.14");
var config = NamingConfig.GetSingle();
config.RenameBooks = true;
config.StandardBookFormat = "";
@ -48,8 +46,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void should_get_bad_request_if_standard_format_doesnt_contain_track_number_and_title()
{
IgnoreOnMonoVersions("5.12", "5.14");
var config = NamingConfig.GetSingle();
config.RenameBooks = true;
config.StandardBookFormat = "{track:00}";
@ -61,8 +57,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void should_not_require_format_when_rename_tracks_is_false()
{
IgnoreOnMonoVersions("5.12", "5.14");
var config = NamingConfig.GetSingle();
config.RenameBooks = false;
config.StandardBookFormat = "";
@ -74,8 +68,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void should_require_format_when_rename_tracks_is_true()
{
IgnoreOnMonoVersions("5.12", "5.14");
var config = NamingConfig.GetSingle();
config.RenameBooks = true;
config.StandardBookFormat = "";
@ -87,8 +79,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void should_get_bad_request_if_author_folder_format_does_not_contain_author_name()
{
IgnoreOnMonoVersions("5.12", "5.14");
var config = NamingConfig.GetSingle();
config.RenameBooks = true;
config.AuthorFolderFormat = "This and That";

@ -42,8 +42,6 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test]
public void invalid_path_should_return_bad_request()
{
IgnoreOnMonoVersions("5.12", "5.14");
var rootFolder = new RootFolderResource
{
Path = "invalid_path"

@ -73,9 +73,9 @@ namespace NzbDrone.Integration.Test.Client
// cache control header gets reordered on net core
var headers = response.Headers;
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim())
.Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim()));
.Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim()));
headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache");
headers.Single(c => c.Name == "Expires").Value.Should().Be("0");
headers.Single(c => c.Name == "Expires").Value.Should().Be("-1");
}
}

@ -11,6 +11,7 @@ namespace NzbDrone.Integration.Test
private RestRequest BuildGet(string route = "author")
{
var request = new RestRequest(route, Method.GET);
request.AddHeader("Origin", "http://a.different.domain");
request.AddHeader(AccessControlHeaders.RequestMethod, "POST");
return request;
@ -19,6 +20,8 @@ namespace NzbDrone.Integration.Test
private RestRequest BuildOptions(string route = "author")
{
var request = new RestRequest(route, Method.OPTIONS);
request.AddHeader("Origin", "http://a.different.domain");
request.AddHeader(AccessControlHeaders.RequestMethod, "POST");
return request;
}

@ -1,4 +1,4 @@
using System.Net;
using System.Net;
using FluentAssertions;
using NUnit.Framework;
using RestSharp;
@ -33,8 +33,6 @@ namespace NzbDrone.Integration.Test
[TestCase("application/junk")]
public void should_get_unacceptable_with_accept_header(string header)
{
IgnoreOnMonoVersions("5.12", "5.14");
var request = new RestRequest("system/status")
{
RequestFormat = DataFormat.None

@ -11,8 +11,6 @@ namespace NzbDrone.Integration.Test
[Test]
public void should_log_on_error()
{
IgnoreOnMonoVersions("5.12", "5.14");
var config = HostConfig.Get(1);
config.LogLevel = "Trace";
HostConfig.Put(config);

@ -4,14 +4,12 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.SignalR.Client;
using NLog;
using NLog.Config;
using NLog.Targets;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.MediaFiles.BookImport.Manual;
using NzbDrone.Core.Qualities;
using NzbDrone.Integration.Test.Client;
@ -159,22 +157,6 @@ namespace NzbDrone.Integration.Test
}
}
protected void IgnoreOnMonoVersions(params string[] version_strings)
{
if (!PlatformInfo.IsMono)
{
return;
}
var current = PlatformInfo.GetVersion();
var versions = version_strings.Select(x => new Version(x)).ToList();
if (versions.Any(x => x.Major == current.Major && x.Minor == current.Minor))
{
throw new IgnoreException($"Ignored on mono {PlatformInfo.GetVersion()}");
}
}
public string GetTempDirectory(params string[] args)
{
var path = Path.Combine(TempDirectory, Path.Combine(args));

@ -1,11 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using FluentAssertions;
using Mono.Posix;
using Mono.Unix;
using NUnit.Framework;
using NzbDrone.Mono.Disk;

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books;
@ -15,14 +16,17 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR;
using Readarr.Api.V1.Books;
using Readarr.Http;
using Readarr.Http.Extensions;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Author
{
public class AuthorModule : ReadarrRestModuleWithSignalR<AuthorResource, NzbDrone.Core.Books.Author>,
[V1ApiController]
public class AuthorController : RestControllerWithSignalR<AuthorResource, NzbDrone.Core.Books.Author>,
IHandle<BookImportedEvent>,
IHandle<BookEditedEvent>,
IHandle<BookFileDeletedEvent>,
@ -40,7 +44,7 @@ namespace Readarr.Api.V1.Author
private readonly IManageCommandQueue _commandQueueManager;
private readonly IRootFolderService _rootFolderService;
public AuthorModule(IBroadcastSignalRMessage signalRBroadcaster,
public AuthorController(IBroadcastSignalRMessage signalRBroadcaster,
IAuthorService authorService,
IBookService bookService,
IAddAuthorService addAuthorService,
@ -67,12 +71,6 @@ namespace Readarr.Api.V1.Author
_commandQueueManager = commandQueueManager;
_rootFolderService = rootFolderService;
GetResourceAll = AllAuthors;
GetResourceById = GetAuthor;
CreateResource = AddAuthor;
UpdateResource = UpdateAuthor;
DeleteResource = DeleteAuthor;
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId));
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId));
@ -97,7 +95,7 @@ namespace Readarr.Api.V1.Author
PutValidator.RuleFor(s => s.Path).IsValidPath();
}
private AuthorResource GetAuthor(int id)
public override AuthorResource GetResourceById(int id)
{
var author = _authorService.GetAuthor(id);
return GetAuthorResource(author);
@ -121,7 +119,8 @@ namespace Readarr.Api.V1.Author
return resource;
}
private List<AuthorResource> AllAuthors()
[HttpGet]
public List<AuthorResource> AllAuthors()
{
var authorStats = _authorStatisticsService.AuthorStatistics();
var authorResources = _authorService.GetAllAuthors().ToResource();
@ -134,14 +133,16 @@ namespace Readarr.Api.V1.Author
return authorResources;
}
private int AddAuthor(AuthorResource authorResource)
[RestPostById]
public ActionResult<AuthorResource> AddAuthor(AuthorResource authorResource)
{
var author = _addAuthorService.AddAuthor(authorResource.ToModel());
return author.Id;
return Created(author.Id);
}
private void UpdateAuthor(AuthorResource authorResource)
[RestPutById]
public ActionResult<AuthorResource> UpdateAuthor(AuthorResource authorResource)
{
var moveFiles = Request.GetBooleanQueryParameter("moveFiles");
var author = _authorService.GetAuthor(authorResource.Id);
@ -165,13 +166,15 @@ namespace Readarr.Api.V1.Author
_authorService.UpdateAuthor(model);
BroadcastResourceChange(ModelAction.Updated, authorResource);
return Accepted(authorResource.Id);
}
private void DeleteAuthor(int id)
[RestDeleteById]
public void DeleteAuthor(int id)
{
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
_authorService.DeleteAuthor(id, deleteFiles, addImportListExclusion);
}
@ -240,16 +243,19 @@ namespace Readarr.Api.V1.Author
resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path);
}
[NonAction]
public void Handle(BookImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, GetAuthorResource(message.Author));
}
[NonAction]
public void Handle(BookEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, GetAuthorResource(message.Book.Author.Value));
}
[NonAction]
public void Handle(BookFileDeletedEvent message)
{
if (message.Reason == DeleteMediaFileReason.Upgrade)
@ -260,26 +266,31 @@ namespace Readarr.Api.V1.Author
BroadcastResourceChange(ModelAction.Updated, GetAuthorResource(message.BookFile.Author.Value));
}
[NonAction]
public void Handle(AuthorUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, GetAuthorResource(message.Author));
}
[NonAction]
public void Handle(AuthorEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, GetAuthorResource(message.Author));
}
[NonAction]
public void Handle(AuthorDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.Author.ToResource());
}
[NonAction]
public void Handle(AuthorRenamedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Author.Id);
}
[NonAction]
public void Handle(MediaCoversUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, GetAuthorResource(message.Author));

@ -1,31 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Books.Commands;
using NzbDrone.Core.Messaging.Commands;
using Readarr.Http.Extensions;
using Readarr.Http;
namespace Readarr.Api.V1.Author
{
public class AuthorEditorModule : ReadarrV1Module
[V1ApiController("author/editor")]
public class AuthorEditorController : Controller
{
private readonly IAuthorService _authorService;
private readonly IManageCommandQueue _commandQueueManager;
public AuthorEditorModule(IAuthorService authorService, IManageCommandQueue commandQueueManager)
: base("/author/editor")
public AuthorEditorController(IAuthorService authorService, IManageCommandQueue commandQueueManager)
{
_authorService = authorService;
_commandQueueManager = commandQueueManager;
Put("/", author => SaveAll());
Delete("/", author => DeleteAuthor());
}
private object SaveAll()
[HttpPut]
public IActionResult SaveAll([FromBody] AuthorEditorResource resource)
{
var resource = Request.Body.FromJson<AuthorEditorResource>();
var authorsToUpdate = _authorService.GetAuthors(resource.AuthorIds);
var authorsToMove = new List<BulkMoveAuthor>();
@ -85,15 +83,12 @@ namespace Readarr.Api.V1.Author
});
}
return ResponseWithCode(_authorService.UpdateAuthors(authorsToUpdate, !resource.MoveFiles)
.ToResource(),
HttpStatusCode.Accepted);
return Accepted(_authorService.UpdateAuthors(authorsToUpdate, !resource.MoveFiles).ToResource());
}
private object DeleteAuthor()
[HttpDelete]
public object DeleteAuthor([FromBody] AuthorEditorResource resource)
{
var resource = Request.Body.FromJson<AuthorEditorResource>();
foreach (var authorId in resource.AuthorIds)
{
_authorService.DeleteAuthor(authorId, false);

@ -1,28 +0,0 @@
using System.Collections.Generic;
using Nancy;
using NzbDrone.Core.Books;
using Readarr.Http;
using Readarr.Http.Extensions;
namespace Readarr.Api.V1.Author
{
public class AuthorImportModule : ReadarrRestModule<AuthorResource>
{
private readonly IAddAuthorService _addAuthorService;
public AuthorImportModule(IAddAuthorService addAuthorService)
: base("/author/import")
{
_addAuthorService = addAuthorService;
Post("/", x => Import());
}
private object Import()
{
var resource = Request.Body.FromJson<List<AuthorResource>>();
var newAuthors = resource.ToModel();
return _addAuthorService.AddAuthors(newAuthors).ToResource();
}
}
}

@ -1,26 +1,26 @@
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource;
using Readarr.Http;
namespace Readarr.Api.V1.Author
{
public class AuthorLookupModule : ReadarrRestModule<AuthorResource>
[V1ApiController("author/lookup")]
public class AuthorLookupController : Controller
{
private readonly ISearchForNewAuthor _searchProxy;
public AuthorLookupModule(ISearchForNewAuthor searchProxy)
: base("/author/lookup")
public AuthorLookupController(ISearchForNewAuthor searchProxy)
{
_searchProxy = searchProxy;
Get("/", x => Search());
}
private object Search()
[HttpGet]
public object Search([FromQuery] string term)
{
var searchResults = _searchProxy.SearchForNewAuthor((string)Request.Query.term);
var searchResults = _searchProxy.SearchForNewAuthor(term);
return MapToResource(searchResults).ToList();
}

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

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

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications;
@ -9,14 +8,17 @@ using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR;
using Readarr.Http;
using Readarr.Http.Extensions;
using Readarr.Http.REST;
using BadRequestException = NzbDrone.Core.Exceptions.BadRequestException;
using HttpStatusCode = System.Net.HttpStatusCode;
namespace Readarr.Api.V1.BookFiles
{
public class BookFileModule : ReadarrRestModuleWithSignalR<BookFileResource, BookFile>,
[V1ApiController]
public class BookFileController : RestControllerWithSignalR<BookFileResource, BookFile>,
IHandle<BookFileAddedEvent>,
IHandle<BookFileDeletedEvent>
{
@ -27,7 +29,7 @@ namespace Readarr.Api.V1.BookFiles
private readonly IBookService _bookService;
private readonly IUpgradableSpecification _upgradableSpecification;
public BookFileModule(IBroadcastSignalRMessage signalRBroadcaster,
public BookFileController(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService,
IDeleteMediaFiles mediaFileDeletionService,
IAudioTagService audioTagService,
@ -42,14 +44,6 @@ namespace Readarr.Api.V1.BookFiles
_authorService = authorService;
_bookService = bookService;
_upgradableSpecification = upgradableSpecification;
GetResourceById = GetBookFile;
GetResourceAll = GetBookFiles;
UpdateResource = SetQuality;
DeleteResource = DeleteBookFile;
Put("/editor", trackFiles => SetQuality());
Delete("/bulk", trackFiles => DeleteBookFiles());
}
private BookFileResource MapToResource(BookFile bookFile)
@ -64,47 +58,36 @@ namespace Readarr.Api.V1.BookFiles
}
}
private BookFileResource GetBookFile(int id)
public override BookFileResource GetResourceById(int id)
{
var resource = MapToResource(_mediaFileService.Get(id));
resource.AudioTags = _audioTagService.ReadTags(resource.Path);
return resource;
}
private List<BookFileResource> GetBookFiles()
[HttpGet]
public List<BookFileResource> GetBookFiles(int? authorId, [FromQuery]List<int> bookFileIds, [FromQuery(Name="bookId")]List<int> bookIds, bool? unmapped)
{
var authorIdQuery = Request.Query.AuthorId;
var bookFileIdsQuery = Request.Query.TrackFileIds;
var bookIdQuery = Request.Query.BookId;
var unmappedQuery = Request.Query.Unmapped;
if (!authorIdQuery.HasValue && !bookFileIdsQuery.HasValue && !bookIdQuery.HasValue && !unmappedQuery.HasValue)
if (!authorId.HasValue && !bookFileIds.Any() && !bookIds.Any() && !unmapped.HasValue)
{
throw new Readarr.Http.REST.BadRequestException("authorId, bookId, bookFileIds or unmapped must be provided");
throw new BadRequestException("authorId, bookId, bookFileIds or unmapped must be provided");
}
if (unmappedQuery.HasValue && Convert.ToBoolean(unmappedQuery.Value))
if (unmapped.HasValue && unmapped.Value)
{
var files = _mediaFileService.GetUnmappedFiles();
return files.ConvertAll(f => MapToResource(f));
}
if (authorIdQuery.HasValue && !bookIdQuery.HasValue)
if (authorId.HasValue && !bookIds.Any())
{
int authorId = Convert.ToInt32(authorIdQuery.Value);
var author = _authorService.GetAuthor(authorId);
var author = _authorService.GetAuthor(authorId.Value);
return _mediaFileService.GetFilesByAuthor(authorId).ConvertAll(f => f.ToResource(author, _upgradableSpecification));
return _mediaFileService.GetFilesByAuthor(authorId.Value).ConvertAll(f => f.ToResource(author, _upgradableSpecification));
}
if (bookIdQuery.HasValue)
if (bookIds.Any())
{
string bookIdValue = bookIdQuery.Value.ToString();
var bookIds = bookIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
var result = new List<BookFileResource>();
foreach (var bookId in bookIds)
{
@ -117,28 +100,24 @@ namespace Readarr.Api.V1.BookFiles
}
else
{
string bookFileIdsValue = bookFileIdsQuery.Value.ToString();
var bookFileIds = bookFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
// trackfiles will come back with the author already populated
var bookFiles = _mediaFileService.Get(bookFileIds);
return bookFiles.ConvertAll(e => MapToResource(e));
}
}
private void SetQuality(BookFileResource bookFileResource)
[RestPutById]
public ActionResult<BookFileResource> SetQuality(BookFileResource bookFileResource)
{
var bookFile = _mediaFileService.Get(bookFileResource.Id);
bookFile.Quality = bookFileResource.Quality;
_mediaFileService.Update(bookFile);
return Accepted(bookFile.Id);
}
private object SetQuality()
[HttpPut("editor")]
public IActionResult SetQuality([FromBody] BookFileListResource resource)
{
var resource = Request.Body.FromJson<BookFileListResource>();
var bookFiles = _mediaFileService.Get(resource.BookFileIds);
foreach (var bookFile in bookFiles)
@ -151,11 +130,11 @@ namespace Readarr.Api.V1.BookFiles
_mediaFileService.Update(bookFiles);
return ResponseWithCode(bookFiles.ConvertAll(f => f.ToResource(bookFiles.First().Author.Value, _upgradableSpecification)),
Nancy.HttpStatusCode.Accepted);
return Accepted(bookFiles.ConvertAll(f => f.ToResource(bookFiles.First().Author.Value, _upgradableSpecification)));
}
private void DeleteBookFile(int id)
[RestDeleteById]
public void DeleteBookFile(int id)
{
var bookFile = _mediaFileService.Get(id);
@ -174,9 +153,9 @@ namespace Readarr.Api.V1.BookFiles
}
}
private object DeleteBookFiles()
[HttpDelete("bulk")]
public IActionResult DeleteBookFiles([FromBody] BookFileListResource resource)
{
var resource = Request.Body.FromJson<BookFileListResource>();
var bookFiles = _mediaFileService.Get(resource.BookFileIds);
var author = bookFiles.First().Author.Value;
@ -185,14 +164,16 @@ namespace Readarr.Api.V1.BookFiles
_mediaFileDeletionService.DeleteTrackFile(author, bookFile);
}
return new object();
return Ok();
}
[NonAction]
public void Handle(BookFileAddedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.BookFile));
}
[NonAction]
public void Handle(BookFileDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.BookFile));

@ -1,27 +1,26 @@
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Books;
using Readarr.Http.Extensions;
using Readarr.Http;
namespace Readarr.Api.V1.Bookshelf
{
public class BookshelfModule : ReadarrV1Module
[V1ApiController]
public class BookshelfController : Controller
{
private readonly IAuthorService _authorService;
private readonly IBookMonitoredService _bookMonitoredService;
public BookshelfModule(IAuthorService authorService, IBookMonitoredService bookMonitoredService)
: base("/bookshelf")
public BookshelfController(IAuthorService authorService, IBookMonitoredService bookMonitoredService)
{
_authorService = authorService;
_bookMonitoredService = bookMonitoredService;
Post("/", author => UpdateAll());
}
private object UpdateAll()
[HttpPost]
public IActionResult UpdateAll([FromBody] BookshelfResource request)
{
//Read from request
var request = Request.Body.FromJson<BookshelfResource>();
var authorToUpdate = _authorService.GetAuthors(request.Authors.Select(s => s.Id));
foreach (var s in request.Authors)
@ -41,7 +40,7 @@ namespace Readarr.Api.V1.Bookshelf
_bookMonitoredService.SetBookMonitoredStatus(author, request.MonitoringOptions);
}
return ResponseWithCode("ok", HttpStatusCode.Accepted);
return Accepted();
}
}
}

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books;
@ -16,12 +16,15 @@ using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR;
using Readarr.Http;
using Readarr.Http.Extensions;
namespace Readarr.Api.V1.Books
{
public class BookModule : BookModuleWithSignalR,
[V1ApiController]
public class BookController : BookControllerWithSignalR,
IHandle<BookGrabbedEvent>,
IHandle<BookEditedEvent>,
IHandle<BookUpdatedEvent>,
@ -33,7 +36,7 @@ namespace Readarr.Api.V1.Books
protected readonly IEditionService _editionService;
protected readonly IAddBookService _addBookService;
public BookModule(IAuthorService authorService,
public BookController(IAuthorService authorService,
IBookService bookService,
IAddBookService addBookService,
IEditionService editionService,
@ -51,12 +54,6 @@ namespace Readarr.Api.V1.Books
_editionService = editionService;
_addBookService = addBookService;
GetResourceAll = GetBooks;
CreateResource = AddBook;
UpdateResource = UpdateBook;
DeleteResource = DeleteBook;
Put("/monitor", x => SetBooksMonitored());
PostValidator.RuleFor(s => s.ForeignBookId).NotEmpty();
PostValidator.RuleFor(s => s.Author.QualityProfileId).SetValidator(qualityProfileExistsValidator);
PostValidator.RuleFor(s => s.Author.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
@ -64,14 +61,13 @@ namespace Readarr.Api.V1.Books
PostValidator.RuleFor(s => s.Author.ForeignAuthorId).NotEmpty();
}
private List<BookResource> GetBooks()
[HttpGet]
public List<BookResource> GetBooks([FromQuery]int? authorId,
[FromQuery]List<int> bookIds,
[FromQuery]string titleSlug,
[FromQuery]bool includeAllAuthorBooks = false)
{
var authorIdQuery = Request.Query.AuthorId;
var bookIdsQuery = Request.Query.BookIds;
var slugQuery = Request.Query.TitleSlug;
var includeAllAuthorBooksQuery = Request.Query.IncludeAllAuthorBooks;
if (!Request.Query.AuthorId.HasValue && !bookIdsQuery.HasValue && !slugQuery.HasValue)
if (!authorId.HasValue && !bookIds.Any() && titleSlug.IsNullOrWhiteSpace())
{
var books = _bookService.GetAllBooks();
@ -94,13 +90,12 @@ namespace Readarr.Api.V1.Books
return MapToResource(books, false);
}
if (authorIdQuery.HasValue)
if (authorId.HasValue)
{
int authorId = Convert.ToInt32(authorIdQuery.Value);
var books = _bookService.GetBooksByAuthor(authorId);
var books = _bookService.GetBooksByAuthor(authorId.Value);
var author = _authorService.GetAuthor(authorId);
var editions = _editionService.GetEditionsByAuthor(authorId)
var author = _authorService.GetAuthor(authorId.Value);
var editions = _editionService.GetEditionsByAuthor(authorId.Value)
.GroupBy(x => x.BookId)
.ToDictionary(x => x.Key, y => y.ToList());
@ -120,10 +115,8 @@ namespace Readarr.Api.V1.Books
return MapToResource(books, false);
}
if (slugQuery.HasValue)
if (titleSlug.IsNotNullOrWhiteSpace())
{
string titleSlug = slugQuery.Value.ToString();
var book = _bookService.FindBySlug(titleSlug);
if (book == null)
@ -131,7 +124,7 @@ namespace Readarr.Api.V1.Books
return MapToResource(new List<Book>(), false);
}
if (includeAllAuthorBooksQuery.HasValue && Convert.ToBoolean(includeAllAuthorBooksQuery.Value))
if (includeAllAuthorBooks)
{
return MapToResource(_bookService.GetBooksByAuthor(book.AuthorId), false);
}
@ -141,23 +134,19 @@ namespace Readarr.Api.V1.Books
}
}
string bookIdsValue = bookIdsQuery.Value.ToString();
var bookIds = bookIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
return MapToResource(_bookService.GetBooks(bookIds), false);
}
private int AddBook(BookResource bookResource)
[RestPostById]
public ActionResult<BookResource> AddBook(BookResource bookResource)
{
var book = _addBookService.AddBook(bookResource.ToModel());
return book.Id;
return Created(book.Id);
}
private void UpdateBook(BookResource bookResource)
[RestPutById]
public ActionResult<BookResource> UpdateBook(BookResource bookResource)
{
var book = _bookService.GetBook(bookResource.Id);
@ -167,9 +156,12 @@ namespace Readarr.Api.V1.Books
_editionService.UpdateMany(model.Editions.Value);
BroadcastResourceChange(ModelAction.Updated, model.Id);
return Accepted(model.Id);
}
private void DeleteBook(int id)
[RestDeleteById]
public void DeleteBook(int id)
{
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
@ -177,15 +169,15 @@ namespace Readarr.Api.V1.Books
_bookService.DeleteBook(id, deleteFiles, addImportListExclusion);
}
private object SetBooksMonitored()
[HttpPut("monitor")]
public IActionResult SetBooksMonitored([FromBody]BooksMonitoredResource resource)
{
var resource = Request.Body.FromJson<BooksMonitoredResource>();
_bookService.SetMonitored(resource.BookIds, resource.Monitored);
return ResponseWithCode(MapToResource(_bookService.GetBooks(resource.BookIds), false), HttpStatusCode.Accepted);
return Accepted(MapToResource(_bookService.GetBooks(resource.BookIds), false));
}
[NonAction]
public void Handle(BookGrabbedEvent message)
{
foreach (var book in message.Book.Books)
@ -197,31 +189,37 @@ namespace Readarr.Api.V1.Books
}
}
[NonAction]
public void Handle(BookEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Book, true));
}
[NonAction]
public void Handle(BookUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Book, true));
}
[NonAction]
public void Handle(BookDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.Book.ToResource());
}
[NonAction]
public void Handle(BookImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Book, true));
}
[NonAction]
public void Handle(TrackImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.BookInfo.Book.ToResource());
}
[NonAction]
public void Handle(BookFileDeletedEvent message)
{
if (message.Reason == DeleteMediaFileReason.Upgrade)

@ -1,17 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaCover;
using NzbDrone.SignalR;
using Readarr.Api.V1.Author;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Books
{
public abstract class BookModuleWithSignalR : ReadarrRestModuleWithSignalR<BookResource, Book>
public abstract class BookControllerWithSignalR : RestControllerWithSignalR<BookResource, Book>
{
protected readonly IBookService _bookService;
protected readonly ISeriesBookLinkService _seriesBookLinkService;
@ -19,7 +18,7 @@ namespace Readarr.Api.V1.Books
protected readonly IUpgradableSpecification _qualityUpgradableSpecification;
protected readonly IMapCoversToLocal _coverMapper;
protected BookModuleWithSignalR(IBookService bookService,
protected BookControllerWithSignalR(IBookService bookService,
ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper,
@ -32,29 +31,9 @@ namespace Readarr.Api.V1.Books
_authorStatisticsService = authorStatisticsService;
_coverMapper = coverMapper;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
GetResourceById = GetBook;
}
protected BookModuleWithSignalR(IBookService bookService,
ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster,
string resource)
: base(signalRBroadcaster, resource)
{
_bookService = bookService;
_seriesBookLinkService = seriesBookLinkService;
_authorStatisticsService = authorStatisticsService;
_coverMapper = coverMapper;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
GetResourceById = GetBook;
}
protected BookResource GetBook(int id)
public override BookResource GetResourceById(int id)
{
var book = _bookService.GetBook(id);
var resource = MapToResource(book, true);

@ -1,26 +1,26 @@
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource;
using Readarr.Http;
namespace Readarr.Api.V1.Books
{
public class BookLookupModule : ReadarrRestModule<BookResource>
[V1ApiController("book/lookup")]
public class BookLookupController : Controller
{
private readonly ISearchForNewBook _searchProxy;
public BookLookupModule(ISearchForNewBook searchProxy)
: base("/book/lookup")
public BookLookupController(ISearchForNewBook searchProxy)
{
_searchProxy = searchProxy;
Get("/", x => Search());
}
private object Search()
[HttpGet]
public object Search(string term)
{
var searchResults = _searchProxy.SearchForNewBook((string)Request.Query.term, null);
var searchResults = _searchProxy.SearchForNewBook(term, null);
return MapToResource(searchResults).ToList();
}

@ -6,7 +6,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.MediaCover;
using Readarr.Api.V1.Author;
using Readarr.Api.V1.BookFiles;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Books

@ -0,0 +1,29 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles;
using Readarr.Http;
namespace Readarr.Api.V1.Books
{
[V1ApiController("rename")]
public class RenameBookController : Controller
{
private readonly IRenameBookFileService _renameBookFileService;
public RenameBookController(IRenameBookFileService renameBookFileService)
{
_renameBookFileService = renameBookFileService;
}
[HttpGet]
public List<RenameBookResource> GetBookFiles(int authorId, int? bookId)
{
if (bookId.HasValue)
{
return _renameBookFileService.GetRenamePreviews(authorId, bookId.Value).ToResource();
}
return _renameBookFileService.GetRenamePreviews(authorId).ToResource();
}
}
}

@ -1,42 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Core.MediaFiles;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Books
{
public class RenameBookModule : ReadarrRestModule<RenameBookResource>
{
private readonly IRenameBookFileService _renameBookFileService;
public RenameBookModule(IRenameBookFileService renameBookFileService)
: base("rename")
{
_renameBookFileService = renameBookFileService;
GetResourceAll = GetBookFiles;
}
private List<RenameBookResource> GetBookFiles()
{
int authorId;
if (Request.Query.AuthorId.HasValue)
{
authorId = (int)Request.Query.AuthorId;
}
else
{
throw new BadRequestException("authorId is missing");
}
if (Request.Query.bookId.HasValue)
{
var bookId = (int)Request.Query.bookId;
return _renameBookFileService.GetRenamePreviews(authorId, bookId).ToResource();
}
return _renameBookFileService.GetRenamePreviews(authorId).ToResource();
}
}
}

@ -1,34 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Books
{
public class RetagBookModule : ReadarrRestModule<RetagBookResource>
[V1ApiController("retag")]
public class RetagBookController : Controller
{
private readonly IAudioTagService _audioTagService;
public RetagBookModule(IAudioTagService audioTagService)
: base("retag")
public RetagBookController(IAudioTagService audioTagService)
{
_audioTagService = audioTagService;
GetResourceAll = GetBooks;
}
private List<RetagBookResource> GetBooks()
[HttpGet]
public List<RetagBookResource> GetBooks(int? authorId, int? bookId)
{
if (Request.Query.bookId.HasValue)
if (bookId.HasValue)
{
var bookId = (int)Request.Query.bookId;
return _audioTagService.GetRetagPreviewsByBook(bookId).Where(x => x.Changes.Any()).ToResource();
return _audioTagService.GetRetagPreviewsByBook(bookId.Value).Where(x => x.Changes.Any()).ToResource();
}
else if (Request.Query.AuthorId.HasValue)
else if (authorId.HasValue)
{
var authorId = (int)Request.Query.AuthorId;
return _audioTagService.GetRetagPreviewsByAuthor(authorId).Where(x => x.Changes.Any()).ToResource();
return _audioTagService.GetRetagPreviewsByAuthor(authorId.Value).Where(x => x.Changes.Any()).ToResource();
}
else
{

@ -1,53 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaCover;
using NzbDrone.SignalR;
using Readarr.Api.V1.Books;
using Readarr.Http;
using Readarr.Http.Extensions;
namespace Readarr.Api.V1.Calendar
{
public class CalendarModule : BookModuleWithSignalR
[V1ApiController]
public class CalendarController : BookControllerWithSignalR
{
public CalendarModule(IBookService bookService,
public CalendarController(IBookService bookService,
ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "calendar")
: base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{
GetResourceAll = GetCalendar;
}
private List<BookResource> GetCalendar()
[HttpGet]
public List<BookResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeAuthor = false)
{
var start = DateTime.Today;
var end = DateTime.Today.AddDays(2);
var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored");
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
//TODO: Add Book Image support to BookModuleWithSignalR
//TODO: Add Book Image support to BookControllerWithSignalR
var includeBookImages = Request.GetBooleanQueryParameter("includeBookImages");
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 startUse = start ?? DateTime.Today;
var endUse = end ?? DateTime.Today.AddDays(2);
var resources = MapToResource(_bookService.BooksBetweenDates(start, end, includeUnmonitored), includeAuthor);
var resources = MapToResource(_bookService.BooksBetweenDates(startUse, endUse, unmonitored), includeAuthor);
return resources.OrderBy(e => e.ReleaseDate).ToList();
}

@ -5,60 +5,38 @@ using Ical.Net;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Nancy;
using Nancy.Responses;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Tags;
using Readarr.Http.Extensions;
using Readarr.Http;
namespace Readarr.Api.V1.Calendar
{
public class CalendarFeedModule : ReadarrV1FeedModule
[V1FeedController("calendar")]
public class CalendarFeedController : Controller
{
private readonly IBookService _bookService;
private readonly IAuthorService _authorService;
private readonly ITagService _tagService;
public CalendarFeedModule(IBookService bookService, IAuthorService authorService, ITagService tagService)
: base("calendar")
public CalendarFeedController(IBookService bookService, IAuthorService authorService, ITagService tagService)
{
_bookService = bookService;
_authorService = authorService;
_tagService = tagService;
Get("/Readarr.ics", options => GetCalendarFeed());
}
private object GetCalendarFeed()
[HttpGet("Readarr.ics")]
public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tagList = "", bool unmonitored = false)
{
var pastDays = 7;
var futureDays = 28;
var start = DateTime.Today.AddDays(-pastDays);
var 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)
if (tagList.IsNotNullOrWhiteSpace())
{
var tagInput = (string)queryTags.Value.ToString();
tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
tags.AddRange(tagList.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
}
var books = _bookService.BooksBetweenDates(start, end, unmonitored);
@ -95,7 +73,7 @@ namespace Readarr.Api.V1.Calendar
var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext());
var icalendar = serializer.SerializeToString(calendar);
return new TextResponse(icalendar, "text/calendar");
return Content(icalendar, "text/calendar");
}
}
}

@ -1,27 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common;
using NzbDrone.Common.Serializer;
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.Http.REST.Attributes;
using NzbDrone.SignalR;
using Readarr.Http;
using Readarr.Http.Extensions;
using Readarr.Http.REST;
using Readarr.Http.Validation;
namespace Readarr.Api.V1.Commands
{
public class CommandModule : ReadarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
[V1ApiController]
public class CommandController : RestControllerWithSignalR<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,
public CommandController(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster,
IServiceFactory serviceFactory)
: base(signalRBroadcaster)
@ -29,48 +34,52 @@ namespace Readarr.Api.V1.Commands
_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)
public override CommandResource GetResourceById(int id)
{
return _commandQueueManager.Get(id).ToResource();
}
private int StartCommand(CommandResource commandResource)
[RestPostById]
public ActionResult<CommandResource> 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);
Request.Body.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(Request.Body);
var body = reader.ReadToEnd();
dynamic command = STJson.Deserialize(body, commandType);
command.Trigger = CommandTrigger.Manual;
command.SuppressMessages = !command.SendUpdatesToClient;
command.SendUpdatesToClient = true;
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
return trackedCommand.Id;
return Created(trackedCommand.Id);
}
private List<CommandResource> GetStartedCommands()
[HttpGet]
public List<CommandResource> GetStartedCommands()
{
return _commandQueueManager.All().ToResource();
}
private void CancelCommand(int id)
[RestDeleteById]
public void CancelCommand(int id)
{
_commandQueueManager.Cancel(id);
}
[NonAction]
public void Handle(CommandUpdatedEvent message)
{
if (message.Command.Body.SendUpdatesToClient)

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

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

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

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

@ -3,12 +3,14 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using Readarr.Http;
namespace Readarr.Api.V1.Config
{
public class MediaManagementConfigModule : ReadarrConfigModule<MediaManagementConfigResource>
[V1ApiController("config/mediamanagement")]
public class MediaManagementConfigController : ConfigController<MediaManagementConfigResource>
{
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
public MediaManagementConfigController(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
: base(configService)
{
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);

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

@ -2,49 +2,44 @@ using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Nancy.ModelBinding;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Organizer;
using NzbDrone.Http.REST.Attributes;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Config
{
public class NamingConfigModule : ReadarrRestModule<NamingConfigResource>
[V1ApiController("config/naming")]
public class NamingConfigController : RestController<NamingConfigResource>
{
private readonly INamingConfigService _namingConfigService;
private readonly IFilenameSampleService _filenameSampleService;
private readonly IFilenameValidationService _filenameValidationService;
private readonly IBuildFileNames _filenameBuilder;
public NamingConfigModule(INamingConfigService namingConfigService,
public NamingConfigController(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.StandardBookFormat).ValidBookFormat();
SharedValidator.RuleFor(c => c.AuthorFolderFormat).ValidAuthorFolderFormat();
}
private void UpdateNamingConfig(NamingConfigResource resource)
public override NamingConfigResource GetResourceById(int id)
{
var nameSpec = resource.ToModel();
ValidateFormatResult(nameSpec);
_namingConfigService.Save(nameSpec);
return GetNamingConfig();
}
private NamingConfigResource GetNamingConfig()
[HttpGet]
public NamingConfigResource GetNamingConfig()
{
var nameSpec = _namingConfigService.GetConfig();
var resource = nameSpec.ToResource();
@ -58,12 +53,19 @@ namespace Readarr.Api.V1.Config
return resource;
}
private NamingConfigResource GetNamingConfig(int id)
[RestPutById]
public ActionResult<NamingConfigResource> UpdateNamingConfig(NamingConfigResource resource)
{
return GetNamingConfig();
var nameSpec = resource.ToModel();
ValidateFormatResult(nameSpec);
_namingConfigService.Save(nameSpec);
return Accepted(resource.Id);
}
private object GetExamples(NamingConfigResource config)
[HttpGet("examples")]
public object GetExamples([FromQuery]NamingConfigResource config)
{
if (config.Id == 0)
{

@ -1,53 +0,0 @@
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);
}
}
}

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

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

@ -1,49 +0,0 @@
using System.Collections.Generic;
using 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);
}
}
}

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

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

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

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

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download;
@ -10,28 +10,23 @@ using Readarr.Api.V1.Author;
using Readarr.Api.V1.Books;
using Readarr.Http;
using Readarr.Http.Extensions;
using Readarr.Http.REST;
namespace Readarr.Api.V1.History
{
public class HistoryModule : ReadarrRestModule<HistoryResource>
[V1ApiController]
public class HistoryController : Controller
{
private readonly IHistoryService _historyService;
private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IFailedDownloadService _failedDownloadService;
public HistoryModule(IHistoryService historyService,
public HistoryController(IHistoryService historyService,
IUpgradableSpecification upgradableSpecification,
IFailedDownloadService failedDownloadService)
{
_historyService = historyService;
_upgradableSpecification = upgradableSpecification;
_failedDownloadService = failedDownloadService;
GetResourcePaged = GetHistory;
Get("/since", x => GetHistorySince());
Get("/author", x => GetAuthorHistory());
Post("/failed", x => MarkAsFailed());
}
protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeAuthor, bool includeBook)
@ -56,11 +51,11 @@ namespace Readarr.Api.V1.History
return resource;
}
private PagingResource<HistoryResource> GetHistory(PagingResource<HistoryResource> pagingResource)
[HttpGet]
public PagingResource<HistoryResource> GetHistory(bool includeAuthor = false, bool includeBook = false)
{
var pagingResource = Request.ReadPagingResourceFromRequest<HistoryResource>();
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending);
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var includeBook = Request.GetBooleanQueryParameter("includeBook");
var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType");
var bookIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "bookId");
@ -84,66 +79,29 @@ namespace Readarr.Api.V1.History
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
}
return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeAuthor, includeBook));
return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeAuthor, includeBook));
}
private List<HistoryResource> GetHistorySince()
[HttpGet("since")]
public List<HistoryResource> GetHistorySince(DateTime date, HistoryEventType? eventType = null, bool includeAuthor = false, bool includeBook = false)
{
var queryDate = Request.Query.Date;
var queryEventType = Request.Query.EventType;
if (!queryDate.HasValue)
{
throw new BadRequestException("date is missing");
}
DateTime date = DateTime.Parse(queryDate.Value);
HistoryEventType? eventType = null;
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var includeBook = Request.GetBooleanQueryParameter("includeBook");
if (queryEventType.HasValue)
{
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
}
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
}
private List<HistoryResource> GetAuthorHistory()
[HttpGet("author")]
public List<HistoryResource> GetAuthorHistory(int authorId, int? bookId = null, HistoryEventType? eventType = null, bool includeAuthor = false, bool includeBook = false)
{
var queryAuthorId = Request.Query.AuthorId;
var queryBookId = Request.Query.BookId;
var queryEventType = Request.Query.EventType;
if (!queryAuthorId.HasValue)
{
throw new BadRequestException("authorId is missing");
}
int authorId = Convert.ToInt32(queryAuthorId.Value);
HistoryEventType? eventType = null;
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var includeBook = Request.GetBooleanQueryParameter("includeBook");
if (queryEventType.HasValue)
if (bookId.HasValue)
{
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
}
if (queryBookId.HasValue)
{
int bookId = Convert.ToInt32(queryBookId.Value);
return _historyService.GetByBook(bookId, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
return _historyService.GetByBook(bookId.Value, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
}
return _historyService.GetByAuthor(authorId, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
}
private object MarkAsFailed()
[HttpPost("failed")]
public object MarkAsFailed([FromBody] int id)
{
var id = (int)Request.Form.Id;
_failedDownloadService.MarkAsFailed(id);
return new object();
}

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

@ -1,54 +1,57 @@
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.Validation;
using NzbDrone.Http.REST.Attributes;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.ImportLists
{
public class ImportListExclusionModule : ReadarrRestModule<ImportListExclusionResource>
[V1ApiController]
public class ImportListExclusionController : RestController<ImportListExclusionResource>
{
private readonly IImportListExclusionService _importListExclusionService;
public ImportListExclusionModule(IImportListExclusionService importListExclusionService,
public ImportListExclusionController(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.AuthorName).NotEmpty();
}
private ImportListExclusionResource GetImportListExclusion(int id)
public override ImportListExclusionResource GetResourceById(int id)
{
return _importListExclusionService.Get(id).ToResource();
}
private List<ImportListExclusionResource> GetImportListExclusions()
[HttpGet]
public List<ImportListExclusionResource> GetImportListExclusions()
{
return _importListExclusionService.All().ToResource();
}
private int AddImportListExclusion(ImportListExclusionResource resource)
[RestPostById]
public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportListExclusionResource resource)
{
var customFilter = _importListExclusionService.Add(resource.ToModel());
return customFilter.Id;
return Created(customFilter.Id);
}
private void UpdateImportListExclusion(ImportListExclusionResource resource)
[RestPutById]
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion(ImportListExclusionResource resource)
{
_importListExclusionService.Update(resource.ToModel());
return Accepted(resource.Id);
}
private void DeleteImportListExclusionResource(int id)
[RestDeleteById]
public void DeleteImportListExclusionResource(int id)
{
_importListExclusionService.Delete(id);
}

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

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@ -14,11 +15,13 @@ using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
using Readarr.Http;
using HttpStatusCode = System.Net.HttpStatusCode;
namespace Readarr.Api.V1.Indexers
{
public class ReleaseModule : ReleaseModuleBase
[V1ApiController]
public class ReleaseController : ReleaseControllerBase
{
private readonly IFetchAndParseRss _rssFetcherAndParser;
private readonly ISearchForNzb _nzbSearchService;
@ -32,7 +35,7 @@ namespace Readarr.Api.V1.Indexers
private readonly ICached<RemoteBook> _remoteBookCache;
public ReleaseModule(IFetchAndParseRss rssFetcherAndParser,
public ReleaseController(IFetchAndParseRss rssFetcherAndParser,
ISearchForNzb nzbSearchService,
IMakeDownloadDecision downloadDecisionMaker,
IPrioritizeDownloadDecision prioritizeDownloadDecision,
@ -53,17 +56,17 @@ namespace Readarr.Api.V1.Indexers
_parsingService = parsingService;
_logger = logger;
GetResourceAll = GetReleases;
Post("/", x => DownloadRelease(ReadResourceFromRequest()));
PostValidator.RuleFor(s => s.IndexerId).ValidId();
PostValidator.RuleFor(s => s.Guid).NotEmpty();
_remoteBookCache = cacheManager.GetCache<RemoteBook>(GetType(), "remoteBooks");
}
private object DownloadRelease(ReleaseResource release)
[HttpPost]
public ActionResult<ReleaseResource> Create(ReleaseResource release)
{
ValidateResource(release);
var remoteBook = _remoteBookCache.Find(GetCacheKey(release));
if (remoteBook == null)
@ -129,19 +132,20 @@ namespace Readarr.Api.V1.Indexers
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed");
}
return release;
return Ok(release);
}
private List<ReleaseResource> GetReleases()
[HttpGet]
public List<ReleaseResource> GetReleases(int? bookId, int? authorId)
{
if (Request.Query.bookId.HasValue)
if (bookId.HasValue)
{
return GetBookReleases(Request.Query.bookId);
return GetBookReleases(int.Parse(Request.Query["bookId"]));
}
if (Request.Query.authorId.HasValue)
if (authorId.HasValue)
{
return GetAuthorReleases(Request.Query.authorId);
return GetAuthorReleases(int.Parse(Request.Query["authorId"]));
}
return GetRss();

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

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
@ -9,17 +10,19 @@ using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using Readarr.Http;
namespace Readarr.Api.V1.Indexers
{
internal class ReleasePushModule : ReleaseModuleBase
[V1ApiController("release/push")]
public class ReleasePushController : ReleaseControllerBase
{
private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger;
public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker,
public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker,
IProcessDownloadDecisions downloadDecisionProcessor,
IIndexerFactory indexerFactory,
Logger logger)
@ -29,18 +32,19 @@ namespace Readarr.Api.V1.Indexers
_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)
[HttpPost]
public ActionResult<ReleaseResource> Create(ReleaseResource release)
{
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
ValidateResource(release);
var info = release.ToModel();
info.Guid = "PUSH-" + info.DownloadUrl;

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

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

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

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

@ -1,18 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Microsoft.AspNetCore.Mvc;
using NLog;
using NzbDrone.Core.Books;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.BookImport.Manual;
using NzbDrone.Core.Qualities;
using Readarr.Http;
using Readarr.Http.Extensions;
namespace Readarr.Api.V1.ManualImport
{
public class ManualImportModule : ReadarrRestModule<ManualImportResource>
[V1ApiController]
public class ManualImportController : Controller
{
private readonly IAuthorService _authorService;
private readonly IBookService _bookService;
@ -20,7 +19,7 @@ namespace Readarr.Api.V1.ManualImport
private readonly IManualImportService _manualImportService;
private readonly Logger _logger;
public ManualImportModule(IManualImportService manualImportService,
public ManualImportController(IManualImportService manualImportService,
IAuthorService authorService,
IEditionService editionService,
IBookService bookService,
@ -31,31 +30,25 @@ namespace Readarr.Api.V1.ManualImport
_editionService = editionService;
_manualImportService = manualImportService;
_logger = logger;
}
GetResourceAll = GetMediaFiles;
Put("/", options =>
{
var resource = Request.Body.FromJson<List<ManualImportResource>>();
return ResponseWithCode(UpdateImportItems(resource), HttpStatusCode.Accepted);
});
[HttpPut]
public IActionResult UpdateItems(List<ManualImportResource> resource)
{
return Accepted(UpdateImportItems(resource));
}
private List<ManualImportResource> GetMediaFiles()
[HttpGet]
public List<ManualImportResource> GetMediaFiles(string folder, string downloadId, int? authorId, bool filterExistingFiles = true, bool replaceExistingFiles = true)
{
var folder = (string)Request.Query.folder;
var downloadId = (string)Request.Query.downloadId;
NzbDrone.Core.Books.Author author = null;
var authorIdQuery = Request.GetNullableIntegerQueryParameter("authorId", null);
if (authorIdQuery.HasValue && authorIdQuery.Value > 0)
if (authorId > 0)
{
author = _authorService.GetAuthor(Convert.ToInt32(authorIdQuery.Value));
author = _authorService.GetAuthor(authorId.Value);
}
var filter = Request.GetBooleanQueryParameter("filterExistingFiles", true) ? FilterFilesType.Matched : FilterFilesType.None;
var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true);
var filter = filterExistingFiles ? FilterFilesType.Matched : FilterFilesType.None;
return _manualImportService.GetMediaFiles(folder, downloadId, author, filter, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList();
}

@ -1,34 +1,32 @@
using System.IO;
using System.Text.RegularExpressions;
using Nancy;
using Nancy.Responses;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Readarr.Http;
namespace Readarr.Api.V1.MediaCovers
{
public class MediaCoverModule : ReadarrV1Module
[V1ApiController]
public class MediaCoverController : Controller
{
private const string MEDIA_COVER_AUTHOR_ROUTE = @"/Author/(?<authorId>\d+)/(?<filename>(.+)\.(jpg|png|gif))";
private const string MEDIA_COVER_BOOK_ROUTE = @"/Book/(?<authorId>\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;
private readonly IContentTypeProvider _mimeTypeProvider;
public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
: base("MediaCover")
public MediaCoverController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
Get(MEDIA_COVER_AUTHOR_ROUTE, options => GetAuthorMediaCover(options.authorId, options.filename));
Get(MEDIA_COVER_BOOK_ROUTE, options => GetBookMediaCover(options.authorId, options.filename));
_mimeTypeProvider = new FileExtensionContentTypeProvider();
}
private object GetAuthorMediaCover(int authorId, string filename)
[HttpGet(@"author/{authorId:int}/{filename:regex((.+)\.(jpg|png|gif))}")]
public IActionResult GetAuthorMediaCover(int authorId, string filename)
{
var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", authorId.ToString(), filename);
@ -39,16 +37,17 @@ namespace Readarr.Api.V1.MediaCovers
var basefilePath = RegexResizedImage.Replace(filePath, "");
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
{
return new NotFoundResponse();
return NotFound();
}
filePath = basefilePath;
}
return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath));
return PhysicalFile(filePath, GetContentType(filePath));
}
private object GetBookMediaCover(int bookId, string filename)
[HttpGet(@"book/{bookId:int}/{filename:regex((.+)\.(jpg|png|gif))}")]
public IActionResult GetBookMediaCover(int bookId, string filename)
{
var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Books", bookId.ToString(), filename);
@ -59,13 +58,23 @@ namespace Readarr.Api.V1.MediaCovers
var basefilePath = RegexResizedImage.Replace(filePath, "");
if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath))
{
return new NotFoundResponse();
return NotFound();
}
filePath = basefilePath;
}
return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath));
return PhysicalFile(filePath, GetContentType(filePath));
}
private string GetContentType(string filePath)
{
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
{
contentType = "application/octet-stream";
}
return contentType;
}
}
}

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

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

@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Parser;
using Readarr.Api.V1.Author;
using Readarr.Api.V1.Books;
@ -5,20 +6,19 @@ using Readarr.Http;
namespace Readarr.Api.V1.Parse
{
public class ParseModule : ReadarrRestModule<ParseResource>
[V1ApiController]
public class ParseController : Controller
{
private readonly IParsingService _parsingService;
public ParseModule(IParsingService parsingService)
public ParseController(IParsingService parsingService)
{
_parsingService = parsingService;
GetResourceSingle = Parse;
}
private ParseResource Parse()
[HttpGet]
public ParseResource Parse(string title)
{
var title = Request.Query.Title.Value as string;
var parsedBookInfo = Parser.ParseBookTitle(title);
if (parsedBookInfo == null)

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

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

@ -1,17 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Metadata;
using Readarr.Http;
namespace Readarr.Api.V1.Profiles.Metadata
{
public class MetadataProfileSchemaModule : ReadarrRestModule<MetadataProfileResource>
[V1ApiController("metadataprofile/schema")]
public class MetadataProfileSchemaController : Controller
{
public MetadataProfileSchemaModule()
: base("/metadataprofile/schema")
{
GetResourceSingle = GetAll;
}
private MetadataProfileResource GetAll()
[HttpGet]
public MetadataProfileResource GetAll()
{
var profile = new MetadataProfile
{

@ -1,53 +1,57 @@
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Http.REST.Attributes;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Profiles.Quality
{
public class ProfileModule : ReadarrRestModule<QualityProfileResource>
[V1ApiController]
public class QualityProfileController : RestController<QualityProfileResource>
{
private readonly IProfileService _profileService;
public ProfileModule(IProfileService profileService)
public QualityProfileController(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)
[RestPostById]
public ActionResult<QualityProfileResource> Create(QualityProfileResource resource)
{
var model = resource.ToModel();
model = _profileService.Add(model);
return model.Id;
return Created(model.Id);
}
private void DeleteProfile(int id)
[RestDeleteById]
public void DeleteProfile(int id)
{
_profileService.Delete(id);
}
private void Update(QualityProfileResource resource)
[RestPutById]
public ActionResult<QualityProfileResource> Update(QualityProfileResource resource)
{
var model = resource.ToModel();
_profileService.Update(model);
return Accepted(model.Id);
}
private QualityProfileResource GetById(int id)
public override QualityProfileResource GetResourceById(int id)
{
return _profileService.Get(id).ToResource();
}
private List<QualityProfileResource> GetAll()
[HttpGet]
public List<QualityProfileResource> GetAll()
{
return _profileService.All().ToResource();
}

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

@ -1,28 +1,26 @@
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Http.REST.Attributes;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Profiles.Release
{
public class ReleaseProfileModule : ReadarrRestModule<ReleaseProfileResource>
[V1ApiController]
public class ReleaseProfileController : RestController<ReleaseProfileResource>
{
private readonly IReleaseProfileService _releaseProfileService;
private readonly IIndexerFactory _indexerFactory;
public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory)
public ReleaseProfileController(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory)
{
_releaseProfileService = releaseProfileService;
_indexerFactory = indexerFactory;
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())
@ -37,27 +35,32 @@ namespace Readarr.Api.V1.Profiles.Release
});
}
private ReleaseProfileResource GetById(int id)
public override ReleaseProfileResource GetResourceById(int id)
{
return _releaseProfileService.Get(id).ToResource();
}
private List<ReleaseProfileResource> GetAll()
[HttpGet]
public List<ReleaseProfileResource> GetAll()
{
return _releaseProfileService.All().ToResource();
}
private int Create(ReleaseProfileResource resource)
[RestPostById]
public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource)
{
return _releaseProfileService.Add(resource.ToModel()).Id;
return Created(_releaseProfileService.Add(resource.ToModel()).Id);
}
private void Update(ReleaseProfileResource resource)
[RestPutById]
public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource)
{
_releaseProfileService.Update(resource.ToModel());
return Accepted(resource.Id);
}
private void DeleteById(int id)
[RestDeleteById]
public void DeleteById(int id)
{
_releaseProfileService.Delete(id);
}

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

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

@ -1,54 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using 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);
}
}
}

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

@ -1,184 +0,0 @@
using System.Collections.Generic;
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.RemoteBook);
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.RemoteBook);
}
return new object();
}
private object Remove(int id)
{
var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true);
var blacklist = Request.GetBooleanQueryParameter("blacklist");
var skipReDownload = Request.GetBooleanQueryParameter("skipredownload");
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
}
return new object();
}
private object Remove()
{
var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true);
var blacklist = Request.GetBooleanQueryParameter("blacklist");
var skipReDownload = Request.GetBooleanQueryParameter("skipredownload");
var resource = Request.Body.FromJson<QueueBulkResource>();
var trackedDownloadIds = new List<string>();
foreach (var id in resource.Ids)
{
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
}
}
_trackedDownloadService.StopTracking(trackedDownloadIds);
return new object();
}
private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist, bool skipReDownload)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease != null)
{
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
return null;
}
var trackedDownload = GetTrackedDownload(id);
if (trackedDownload == null)
{
throw new NotFoundException();
}
if (removeFromClient)
{
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
if (downloadClient == null)
{
throw new BadRequestException();
}
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true);
}
if (blacklist)
{
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipReDownload);
}
if (!removeFromClient && !blacklist)
{
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
{
return null;
}
}
return trackedDownload;
}
private TrackedDownload GetTrackedDownload(int queueId)
{
var queueItem = _queueService.Find(queueId);
if (queueItem == null)
{
throw new NotFoundException();
}
var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId);
if (trackedDownload == null)
{
throw new NotFoundException();
}
return trackedDownload;
}
}
}

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

@ -1,65 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
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;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Queue
{
public class QueueDetailsModule : ReadarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>,
[V1ApiController("queue/details")]
public class QueueDetailsController : RestControllerWithSignalR<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")
public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
: base(broadcastSignalRMessage)
{
_queueService = queueService;
_pendingReleaseService = pendingReleaseService;
GetResourceAll = GetQueue;
}
private List<QueueResource> GetQueue()
public override QueueResource GetResourceById(int id)
{
throw new NotImplementedException();
}
[HttpGet]
public List<QueueResource> GetQueue(int? authorId, [FromQuery]List<int> bookIds, bool includeAuthor = false, bool includeBook = true)
{
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var includeBook = Request.GetBooleanQueryParameter("includeBook", true);
var queue = _queueService.GetQueue();
var pending = _pendingReleaseService.GetPendingQueue();
var fullQueue = queue.Concat(pending);
var authorIdQuery = Request.Query.AuthorId;
var bookIdsQuery = Request.Query.BookIds;
if (authorIdQuery.HasValue)
if (authorId.HasValue)
{
return fullQueue.Where(q => q.Author?.Id == (int)authorIdQuery).ToResource(includeAuthor, includeBook);
return fullQueue.Where(q => q.Author?.Id == authorId.Value).ToResource(includeAuthor, includeBook);
}
if (bookIdsQuery.HasValue)
if (bookIds.Any())
{
string bookIdsValue = bookIdsQuery.Value.ToString();
var bookIds = bookIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
return fullQueue.Where(q => q.Book != null && bookIds.Contains(q.Book.Id)).ToResource(includeAuthor, includeBook);
}
return fullQueue.ToResource(includeAuthor, includeBook);
}
[NonAction]
public void Handle(QueueUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Sync);
}
[NonAction]
public void Handle(PendingReleasesUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Sync);

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

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

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

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

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

@ -2,24 +2,27 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Books.Calibre;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.RootFolders
{
public class RootFolderModule : ReadarrRestModuleWithSignalR<RootFolderResource, RootFolder>
[V1ApiController]
public class RootFolderController : RestControllerWithSignalR<RootFolderResource, RootFolder>
{
private readonly IRootFolderService _rootFolderService;
private readonly ICalibreProxy _calibreProxy;
public RootFolderModule(IRootFolderService rootFolderService,
public RootFolderController(IRootFolderService rootFolderService,
ICalibreProxy calibreProxy,
IBroadcastSignalRMessage signalRBroadcaster,
RootFolderValidator rootFolderValidator,
@ -35,12 +38,6 @@ namespace Readarr.Api.V1.RootFolders
_rootFolderService = rootFolderService;
_calibreProxy = calibreProxy;
GetResourceAll = GetRootFolders;
GetResourceById = GetRootFolder;
CreateResource = CreateRootFolder;
UpdateResource = UpdateRootFolder;
DeleteResource = DeleteFolder;
SharedValidator.RuleFor(c => c.Path)
.Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath()
@ -95,12 +92,13 @@ namespace Readarr.Api.V1.RootFolders
return HttpUri.CombinePath(HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase), settings.Library);
}
private RootFolderResource GetRootFolder(int id)
public override RootFolderResource GetResourceById(int id)
{
return _rootFolderService.Get(id).ToResource();
}
private int CreateRootFolder(RootFolderResource rootFolderResource)
[RestPostById]
public ActionResult<RootFolderResource> CreateRootFolder(RootFolderResource rootFolderResource)
{
var model = rootFolderResource.ToModel();
@ -109,10 +107,11 @@ namespace Readarr.Api.V1.RootFolders
_calibreProxy.Test(model.CalibreSettings);
}
return _rootFolderService.Add(model).Id;
return Created(_rootFolderService.Add(model).Id);
}
private void UpdateRootFolder(RootFolderResource rootFolderResource)
[RestPutById]
public ActionResult<RootFolderResource> UpdateRootFolder(RootFolderResource rootFolderResource)
{
var model = rootFolderResource.ToModel();
@ -127,14 +126,18 @@ namespace Readarr.Api.V1.RootFolders
}
_rootFolderService.Update(model);
return Accepted(model.Id);
}
private List<RootFolderResource> GetRootFolders()
[HttpGet]
public List<RootFolderResource> GetRootFolders()
{
return _rootFolderService.AllWithSpaceStats().ToResource();
}
private void DeleteFolder(int id)
[RestDeleteById]
public void DeleteFolder(int id)
{
_rootFolderService.Remove(id);
}

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

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Books;
using Readarr.Http;
namespace Readarr.Api.V1.Series
{
[V1ApiController]
public class SeriesController : Controller
{
protected readonly ISeriesService _seriesService;
public SeriesController(ISeriesService seriesService)
{
_seriesService = seriesService;
}
[HttpGet]
public List<SeriesResource> GetSeries(int authorId)
{
return _seriesService.GetByAuthorId(authorId).ToResource();
}
}
}

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using Nancy;
using NzbDrone.Core.Books;
using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Series
{
public class SeriesModule : ReadarrRestModule<SeriesResource>
{
protected readonly ISeriesService _seriesService;
public SeriesModule(ISeriesService seriesService)
{
_seriesService = seriesService;
GetResourceAll = GetSeries;
}
private List<SeriesResource> GetSeries()
{
var authorIdQuery = Request.Query.AuthorId;
if (!authorIdQuery.HasValue)
{
throw new BadRequestException("authorId must be provided");
}
int authorId = Convert.ToInt32(authorIdQuery.Value);
return _seriesService.GetByAuthorId(authorId).ToResource();
}
}
}

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

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

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

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

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

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

@ -1,4 +1,5 @@
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
@ -11,25 +12,27 @@ using Readarr.Http.Extensions;
namespace Readarr.Api.V1.Wanted
{
public class CutoffModule : BookModuleWithSignalR
[V1ApiController("wanted/cutoff")]
public class CutoffController : BookControllerWithSignalR
{
private readonly IBookCutoffService _bookCutoffService;
public CutoffModule(IBookCutoffService bookCutoffService,
public CutoffController(IBookCutoffService bookCutoffService,
IBookService bookService,
ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "wanted/cutoff")
: base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{
_bookCutoffService = bookCutoffService;
GetResourcePaged = GetCutoffUnmetBooks;
}
private PagingResource<BookResource> GetCutoffUnmetBooks(PagingResource<BookResource> pagingResource)
[HttpGet]
public PagingResource<BookResource> GetCutoffUnmetBooks(bool includeAuthor = false)
{
var pagingResource = Request.ReadPagingResourceFromRequest<BookResource>();
var pagingSpec = new PagingSpec<Book>
{
Page = pagingResource.Page,
@ -38,7 +41,6 @@ namespace Readarr.Api.V1.Wanted
SortDirection = pagingResource.SortDirection
};
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored");
if (filter != null && filter.Value == "false")
@ -50,9 +52,7 @@ namespace Readarr.Api.V1.Wanted
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true);
}
var resource = ApplyToPage(_bookCutoffService.BooksWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeAuthor));
return resource;
return pagingSpec.ApplyToPage(_bookCutoffService.BooksWhereCutoffUnmet, v => MapToResource(v, includeAuthor));
}
}
}

@ -1,4 +1,5 @@
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
@ -11,21 +12,23 @@ using Readarr.Http.Extensions;
namespace Readarr.Api.V1.Wanted
{
public class MissingModule : BookModuleWithSignalR
[V1ApiController("wanted/missing")]
public class MissingController : BookControllerWithSignalR
{
public MissingModule(IBookService bookService,
public MissingController(IBookService bookService,
ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "wanted/missing")
: base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{
GetResourcePaged = GetMissingBooks;
}
private PagingResource<BookResource> GetMissingBooks(PagingResource<BookResource> pagingResource)
[HttpGet]
public PagingResource<BookResource> GetMissingBooks(bool includeAuthor = false)
{
var pagingResource = Request.ReadPagingResourceFromRequest<BookResource>();
var pagingSpec = new PagingSpec<Book>
{
Page = pagingResource.Page,
@ -34,7 +37,6 @@ namespace Readarr.Api.V1.Wanted
SortDirection = pagingResource.SortDirection
};
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored");
if (monitoredFilter != null && monitoredFilter.Value == "false")
@ -46,9 +48,7 @@ namespace Readarr.Api.V1.Wanted
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true);
}
var resource = ApplyToPage(_bookService.BooksWithoutFiles, pagingSpec, v => MapToResource(v, includeAuthor));
return resource;
return pagingSpec.ApplyToPage(_bookService.BooksWithoutFiles, v => MapToResource(v, includeAuthor));
}
}
}

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

Loading…
Cancel
Save