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({ const promise = createAjaxRequest({
method: 'PUT', method: 'PUT',
url: '/qualityDefinition/update', url: '/qualityDefinition/update',
data: JSON.stringify(upatedDefinitions) data: JSON.stringify(upatedDefinitions),
contentType: 'application/json',
dataType: 'json'
}).request; }).request;
promise.done((data) => { promise.done((data) => {

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

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

@ -2,6 +2,7 @@ using System;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace NzbDrone.Common.Serializer namespace NzbDrone.Common.Serializer
{ {
@ -15,23 +16,25 @@ namespace NzbDrone.Common.Serializer
public static JsonSerializerOptions GetSerializerSettings() public static JsonSerializerOptions GetSerializerSettings()
{ {
var serializerSettings = new JsonSerializerOptions var settings = new JsonSerializerOptions();
{ ApplySerializerSettings(settings);
AllowTrailingCommas = true, return settings;
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }
PropertyNameCaseInsensitive = true,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, public static void ApplySerializerSettings(JsonSerializerOptions serializerSettings)
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, {
WriteIndented = true 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 JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
serializerSettings.Converters.Add(new STJVersionConverter()); serializerSettings.Converters.Add(new STJVersionConverter());
serializerSettings.Converters.Add(new STJHttpUriConverter()); serializerSettings.Converters.Add(new STJHttpUriConverter());
serializerSettings.Converters.Add(new STJTimeSpanConverter()); serializerSettings.Converters.Add(new STJTimeSpanConverter());
serializerSettings.Converters.Add(new STJUtcConverter()); serializerSettings.Converters.Add(new STJUtcConverter());
return serializerSettings;
} }
public static T Deserialize<T>(string json) public static T Deserialize<T>(string json)
@ -84,5 +87,15 @@ namespace NzbDrone.Common.Serializer
JsonSerializer.Serialize(writer, (object)model, options); 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 System.Collections.Generic;
using Nancy.Bootstrapper;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Readarr.Http;
namespace NzbDrone.Host namespace NzbDrone.Host
{ {
@ -28,8 +26,6 @@ namespace NzbDrone.Host
{ {
AutoRegisterImplementations<MessageHub>(); AutoRegisterImplementations<MessageHub>();
Container.Register<INancyBootstrapper, ReadarrBootstrapper>();
if (OsInfo.IsWindows) if (OsInfo.IsWindows)
{ {
Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>(); Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>();

@ -4,7 +4,6 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.6.4" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.6.4" />
</ItemGroup> </ItemGroup>
<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.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; 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.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NLog; using NLog;
using NLog.Extensions.Logging; using NLog.Extensions.Logging;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Host.AccessControl; 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; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host namespace NzbDrone.Host
{ {
public class WebHostController : IHostController public class WebHostController : IHostController
{ {
private readonly IContainer _container;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IFirewallAdapter _firewallAdapter; private readonly IFirewallAdapter _firewallAdapter;
private readonly IEnumerable<IAspNetCoreMiddleware> _middlewares; private readonly ReadarrErrorPipeline _errorHandler;
private readonly Logger _logger; private readonly Logger _logger;
private IWebHost _host; private IWebHost _host;
public WebHostController(IRuntimeInfo runtimeInfo, public WebHostController(IContainer container,
IRuntimeInfo runtimeInfo,
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
IFirewallAdapter firewallAdapter, IFirewallAdapter firewallAdapter,
IEnumerable<IAspNetCoreMiddleware> middlewares, ReadarrErrorPipeline errorHandler,
Logger logger) Logger logger)
{ {
_container = container;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_firewallAdapter = firewallAdapter; _firewallAdapter = firewallAdapter;
_middlewares = middlewares; _errorHandler = errorHandler;
_logger = logger; _logger = logger;
} }
@ -103,24 +120,125 @@ namespace NzbDrone.Host
}) })
.ConfigureServices(services => .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 services
.AddSignalR() .AddSignalR()
.AddJsonProtocol(options => .AddJsonProtocol(options =>
{ {
options.PayloadSerializerOptions = STJson.GetSerializerSettings(); 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 => .Configure(app =>
{ {
app.UseMiddleware<LoggingMiddleware>();
app.UsePathBase(new PathString(_configFileProvider.UrlBase));
app.UseExceptionHandler(new ExceptionHandlerOptions
{
AllowStatusCode404Response = true,
ExceptionHandler = _errorHandler.HandleException
});
app.UseRouting(); app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.Properties["host.AppName"] = BuildInfo.AppName; 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); if (context.Request.Path.StartsWithSegments("/api/v1/command", StringComparison.CurrentCultureIgnoreCase))
middleWare.Attach(app); {
} 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()) .UseContentRoot(Directory.GetCurrentDirectory())
.Build(); .Build();

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

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

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

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

@ -73,9 +73,9 @@ namespace NzbDrone.Integration.Test.Client
// cache control header gets reordered on net core // cache control header gets reordered on net core
var headers = response.Headers; var headers = response.Headers;
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim()) ((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 == "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") private RestRequest BuildGet(string route = "author")
{ {
var request = new RestRequest(route, Method.GET); var request = new RestRequest(route, Method.GET);
request.AddHeader("Origin", "http://a.different.domain");
request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); request.AddHeader(AccessControlHeaders.RequestMethod, "POST");
return request; return request;
@ -19,6 +20,8 @@ namespace NzbDrone.Integration.Test
private RestRequest BuildOptions(string route = "author") private RestRequest BuildOptions(string route = "author")
{ {
var request = new RestRequest(route, Method.OPTIONS); var request = new RestRequest(route, Method.OPTIONS);
request.AddHeader("Origin", "http://a.different.domain");
request.AddHeader(AccessControlHeaders.RequestMethod, "POST");
return request; return request;
} }

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

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

@ -4,14 +4,12 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using NLog; using NLog;
using NLog.Config; using NLog.Config;
using NLog.Targets; using NLog.Targets;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.MediaFiles.BookImport.Manual; using NzbDrone.Core.MediaFiles.BookImport.Manual;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Integration.Test.Client; 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) public string GetTempDirectory(params string[] args)
{ {
var path = Path.Combine(TempDirectory, Path.Combine(args)); var path = Path.Combine(TempDirectory, Path.Combine(args));

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

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

@ -1,31 +1,29 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Books.Commands; using NzbDrone.Core.Books.Commands;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using Readarr.Http.Extensions; using Readarr.Http;
namespace Readarr.Api.V1.Author namespace Readarr.Api.V1.Author
{ {
public class AuthorEditorModule : ReadarrV1Module [V1ApiController("author/editor")]
public class AuthorEditorController : Controller
{ {
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
public AuthorEditorModule(IAuthorService authorService, IManageCommandQueue commandQueueManager) public AuthorEditorController(IAuthorService authorService, IManageCommandQueue commandQueueManager)
: base("/author/editor")
{ {
_authorService = authorService; _authorService = authorService;
_commandQueueManager = commandQueueManager; _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 authorsToUpdate = _authorService.GetAuthors(resource.AuthorIds);
var authorsToMove = new List<BulkMoveAuthor>(); var authorsToMove = new List<BulkMoveAuthor>();
@ -85,15 +83,12 @@ namespace Readarr.Api.V1.Author
}); });
} }
return ResponseWithCode(_authorService.UpdateAuthors(authorsToUpdate, !resource.MoveFiles) return Accepted(_authorService.UpdateAuthors(authorsToUpdate, !resource.MoveFiles).ToResource());
.ToResource(),
HttpStatusCode.Accepted);
} }
private object DeleteAuthor() [HttpDelete]
public object DeleteAuthor([FromBody] AuthorEditorResource resource)
{ {
var resource = Request.Body.FromJson<AuthorEditorResource>();
foreach (var authorId in resource.AuthorIds) foreach (var authorId in resource.AuthorIds)
{ {
_authorService.DeleteAuthor(authorId, false); _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.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using Readarr.Http; using Readarr.Http;
namespace Readarr.Api.V1.Author namespace Readarr.Api.V1.Author
{ {
public class AuthorLookupModule : ReadarrRestModule<AuthorResource> [V1ApiController("author/lookup")]
public class AuthorLookupController : Controller
{ {
private readonly ISearchForNewAuthor _searchProxy; private readonly ISearchForNewAuthor _searchProxy;
public AuthorLookupModule(ISearchForNewAuthor searchProxy) public AuthorLookupController(ISearchForNewAuthor searchProxy)
: base("/author/lookup")
{ {
_searchProxy = 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(); 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.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
@ -9,14 +8,17 @@ using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.Extensions; using Readarr.Http.REST;
using BadRequestException = NzbDrone.Core.Exceptions.BadRequestException;
using HttpStatusCode = System.Net.HttpStatusCode; using HttpStatusCode = System.Net.HttpStatusCode;
namespace Readarr.Api.V1.BookFiles namespace Readarr.Api.V1.BookFiles
{ {
public class BookFileModule : ReadarrRestModuleWithSignalR<BookFileResource, BookFile>, [V1ApiController]
public class BookFileController : RestControllerWithSignalR<BookFileResource, BookFile>,
IHandle<BookFileAddedEvent>, IHandle<BookFileAddedEvent>,
IHandle<BookFileDeletedEvent> IHandle<BookFileDeletedEvent>
{ {
@ -27,7 +29,7 @@ namespace Readarr.Api.V1.BookFiles
private readonly IBookService _bookService; private readonly IBookService _bookService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
public BookFileModule(IBroadcastSignalRMessage signalRBroadcaster, public BookFileController(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IDeleteMediaFiles mediaFileDeletionService, IDeleteMediaFiles mediaFileDeletionService,
IAudioTagService audioTagService, IAudioTagService audioTagService,
@ -42,14 +44,6 @@ namespace Readarr.Api.V1.BookFiles
_authorService = authorService; _authorService = authorService;
_bookService = bookService; _bookService = bookService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
GetResourceById = GetBookFile;
GetResourceAll = GetBookFiles;
UpdateResource = SetQuality;
DeleteResource = DeleteBookFile;
Put("/editor", trackFiles => SetQuality());
Delete("/bulk", trackFiles => DeleteBookFiles());
} }
private BookFileResource MapToResource(BookFile bookFile) 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)); var resource = MapToResource(_mediaFileService.Get(id));
resource.AudioTags = _audioTagService.ReadTags(resource.Path); resource.AudioTags = _audioTagService.ReadTags(resource.Path);
return resource; 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; if (!authorId.HasValue && !bookFileIds.Any() && !bookIds.Any() && !unmapped.HasValue)
var bookFileIdsQuery = Request.Query.TrackFileIds;
var bookIdQuery = Request.Query.BookId;
var unmappedQuery = Request.Query.Unmapped;
if (!authorIdQuery.HasValue && !bookFileIdsQuery.HasValue && !bookIdQuery.HasValue && !unmappedQuery.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(); var files = _mediaFileService.GetUnmappedFiles();
return files.ConvertAll(f => MapToResource(f)); 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.Value);
var author = _authorService.GetAuthor(authorId);
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>(); var result = new List<BookFileResource>();
foreach (var bookId in bookIds) foreach (var bookId in bookIds)
{ {
@ -117,28 +100,24 @@ namespace Readarr.Api.V1.BookFiles
} }
else 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 // trackfiles will come back with the author already populated
var bookFiles = _mediaFileService.Get(bookFileIds); var bookFiles = _mediaFileService.Get(bookFileIds);
return bookFiles.ConvertAll(e => MapToResource(e)); return bookFiles.ConvertAll(e => MapToResource(e));
} }
} }
private void SetQuality(BookFileResource bookFileResource) [RestPutById]
public ActionResult<BookFileResource> SetQuality(BookFileResource bookFileResource)
{ {
var bookFile = _mediaFileService.Get(bookFileResource.Id); var bookFile = _mediaFileService.Get(bookFileResource.Id);
bookFile.Quality = bookFileResource.Quality; bookFile.Quality = bookFileResource.Quality;
_mediaFileService.Update(bookFile); _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); var bookFiles = _mediaFileService.Get(resource.BookFileIds);
foreach (var bookFile in bookFiles) foreach (var bookFile in bookFiles)
@ -151,11 +130,11 @@ namespace Readarr.Api.V1.BookFiles
_mediaFileService.Update(bookFiles); _mediaFileService.Update(bookFiles);
return ResponseWithCode(bookFiles.ConvertAll(f => f.ToResource(bookFiles.First().Author.Value, _upgradableSpecification)), return Accepted(bookFiles.ConvertAll(f => f.ToResource(bookFiles.First().Author.Value, _upgradableSpecification)));
Nancy.HttpStatusCode.Accepted);
} }
private void DeleteBookFile(int id) [RestDeleteById]
public void DeleteBookFile(int id)
{ {
var bookFile = _mediaFileService.Get(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 bookFiles = _mediaFileService.Get(resource.BookFileIds);
var author = bookFiles.First().Author.Value; var author = bookFiles.First().Author.Value;
@ -185,14 +164,16 @@ namespace Readarr.Api.V1.BookFiles
_mediaFileDeletionService.DeleteTrackFile(author, bookFile); _mediaFileDeletionService.DeleteTrackFile(author, bookFile);
} }
return new object(); return Ok();
} }
[NonAction]
public void Handle(BookFileAddedEvent message) public void Handle(BookFileAddedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.BookFile)); BroadcastResourceChange(ModelAction.Updated, MapToResource(message.BookFile));
} }
[NonAction]
public void Handle(BookFileDeletedEvent message) public void Handle(BookFileDeletedEvent message)
{ {
BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.BookFile)); BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.BookFile));

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

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

@ -1,17 +1,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.AuthorStats; using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Readarr.Api.V1.Author; using Readarr.Api.V1.Author;
using Readarr.Http; using Readarr.Http.REST;
namespace Readarr.Api.V1.Books 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 IBookService _bookService;
protected readonly ISeriesBookLinkService _seriesBookLinkService; protected readonly ISeriesBookLinkService _seriesBookLinkService;
@ -19,7 +18,7 @@ namespace Readarr.Api.V1.Books
protected readonly IUpgradableSpecification _qualityUpgradableSpecification; protected readonly IUpgradableSpecification _qualityUpgradableSpecification;
protected readonly IMapCoversToLocal _coverMapper; protected readonly IMapCoversToLocal _coverMapper;
protected BookModuleWithSignalR(IBookService bookService, protected BookControllerWithSignalR(IBookService bookService,
ISeriesBookLinkService seriesBookLinkService, ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService, IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
@ -32,29 +31,9 @@ namespace Readarr.Api.V1.Books
_authorStatisticsService = authorStatisticsService; _authorStatisticsService = authorStatisticsService;
_coverMapper = coverMapper; _coverMapper = coverMapper;
_qualityUpgradableSpecification = qualityUpgradableSpecification; _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 book = _bookService.GetBook(id);
var resource = MapToResource(book, true); var resource = MapToResource(book, true);

@ -1,26 +1,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using Readarr.Http; using Readarr.Http;
namespace Readarr.Api.V1.Books namespace Readarr.Api.V1.Books
{ {
public class BookLookupModule : ReadarrRestModule<BookResource> [V1ApiController("book/lookup")]
public class BookLookupController : Controller
{ {
private readonly ISearchForNewBook _searchProxy; private readonly ISearchForNewBook _searchProxy;
public BookLookupModule(ISearchForNewBook searchProxy) public BookLookupController(ISearchForNewBook searchProxy)
: base("/book/lookup")
{ {
_searchProxy = 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(); return MapToResource(searchResults).ToList();
} }

@ -6,7 +6,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using Readarr.Api.V1.Author; using Readarr.Api.V1.Author;
using Readarr.Api.V1.BookFiles;
using Readarr.Http.REST; using Readarr.Http.REST;
namespace Readarr.Api.V1.Books 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.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.REST; using Readarr.Http.REST;
namespace Readarr.Api.V1.Books namespace Readarr.Api.V1.Books
{ {
public class RetagBookModule : ReadarrRestModule<RetagBookResource> [V1ApiController("retag")]
public class RetagBookController : Controller
{ {
private readonly IAudioTagService _audioTagService; private readonly IAudioTagService _audioTagService;
public RetagBookModule(IAudioTagService audioTagService) public RetagBookController(IAudioTagService audioTagService)
: base("retag")
{ {
_audioTagService = 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.Value).Where(x => x.Changes.Any()).ToResource();
return _audioTagService.GetRetagPreviewsByBook(bookId).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.Value).Where(x => x.Changes.Any()).ToResource();
return _audioTagService.GetRetagPreviewsByAuthor(authorId).Where(x => x.Changes.Any()).ToResource();
} }
else else
{ {

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

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

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

@ -0,0 +1,48 @@
using System.Linq;
using System.Reflection;
using 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 NzbDrone.Core.Configuration;
using Readarr.Http;
namespace Readarr.Api.V1.Config 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) : base(configService)
{ {
} }

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

@ -1,12 +1,14 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using Readarr.Http;
using Readarr.Http.Validation; using Readarr.Http.Validation;
namespace Readarr.Api.V1.Config 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) : base(configService)
{ {
SharedValidator.RuleFor(c => c.MinimumAge) SharedValidator.RuleFor(c => c.MinimumAge)

@ -3,12 +3,14 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using Readarr.Http;
namespace Readarr.Api.V1.Config 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) : base(configService)
{ {
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0); SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);

@ -2,12 +2,14 @@ using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using Readarr.Http;
namespace Readarr.Api.V1.Config 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) : base(configService)
{ {
SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace()); SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace());

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

@ -1,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 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) : 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 NzbDrone.Core.DiskSpace;
using Readarr.Http; using Readarr.Http;
namespace Readarr.Api.V1.DiskSpace namespace Readarr.Api.V1.DiskSpace
{ {
public class DiskSpaceModule : ReadarrRestModule<DiskSpaceResource> [V1ApiController("diskspace")]
public class DiskSpaceController : Controller
{ {
private readonly IDiskSpaceService _diskSpaceService; private readonly IDiskSpaceService _diskSpaceService;
public DiskSpaceModule(IDiskSpaceService diskSpaceService) public DiskSpaceController(IDiskSpaceService diskSpaceService)
: base("diskspace")
{ {
_diskSpaceService = diskSpaceService; _diskSpaceService = diskSpaceService;
GetResourceAll = GetFreeSpace;
} }
[HttpGet]
public List<DiskSpaceResource> GetFreeSpace() public List<DiskSpaceResource> GetFreeSpace()
{ {
return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource);

@ -1,12 +1,14 @@
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using Readarr.Http;
namespace Readarr.Api.V1.DownloadClient 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 static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper();
public DownloadClientModule(IDownloadClientFactory downloadClientFactory) public DownloadClientController(IDownloadClientFactory downloadClientFactory)
: base(downloadClientFactory, "downloadclient", ResourceMapper) : base(downloadClientFactory, "downloadclient", ResourceMapper)
{ {
} }

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

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

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
@ -10,28 +10,23 @@ using Readarr.Api.V1.Author;
using Readarr.Api.V1.Books; using Readarr.Api.V1.Books;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.Extensions; using Readarr.Http.Extensions;
using Readarr.Http.REST;
namespace Readarr.Api.V1.History namespace Readarr.Api.V1.History
{ {
public class HistoryModule : ReadarrRestModule<HistoryResource> [V1ApiController]
public class HistoryController : Controller
{ {
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IFailedDownloadService _failedDownloadService; private readonly IFailedDownloadService _failedDownloadService;
public HistoryModule(IHistoryService historyService, public HistoryController(IHistoryService historyService,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IFailedDownloadService failedDownloadService) IFailedDownloadService failedDownloadService)
{ {
_historyService = historyService; _historyService = historyService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
_failedDownloadService = failedDownloadService; _failedDownloadService = failedDownloadService;
GetResourcePaged = GetHistory;
Get("/since", x => GetHistorySince());
Get("/author", x => GetAuthorHistory());
Post("/failed", x => MarkAsFailed());
} }
protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeAuthor, bool includeBook) protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeAuthor, bool includeBook)
@ -56,11 +51,11 @@ namespace Readarr.Api.V1.History
return resource; 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 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 eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType");
var bookIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "bookId"); 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); 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(); 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; if (bookId.HasValue)
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)
{ {
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); return _historyService.GetByBook(bookId.Value, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
}
if (queryBookId.HasValue)
{
int bookId = Convert.ToInt32(queryBookId.Value);
return _historyService.GetByBook(bookId, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
} }
return _historyService.GetByAuthor(authorId, 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); _failedDownloadService.MarkAsFailed(id);
return new object(); return new object();
} }

@ -1,14 +1,16 @@
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using Readarr.Http;
namespace Readarr.Api.V1.ImportLists 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 static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
public ImportListModule(ImportListFactory importListFactory, public ImportListController(ImportListFactory importListFactory,
QualityProfileExistsValidator qualityProfileExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator) MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(importListFactory, "importlist", ResourceMapper) : base(importListFactory, "importlist", ResourceMapper)

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

@ -1,12 +1,14 @@
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using Readarr.Http;
namespace Readarr.Api.V1.Indexers 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 static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper();
public IndexerModule(IndexerFactory indexerFactory) public IndexerController(IndexerFactory indexerFactory)
: base(indexerFactory, "indexer", ResourceMapper) : base(indexerFactory, "indexer", ResourceMapper)
{ {
} }

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

@ -1,11 +1,17 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using Readarr.Http; using Readarr.Http.REST;
namespace Readarr.Api.V1.Indexers 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) protected virtual List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions)
{ {
var result = new List<ReleaseResource>(); var result = new List<ReleaseResource>();

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

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

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

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

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

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

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

@ -1,12 +1,14 @@
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
using Readarr.Http;
namespace Readarr.Api.V1.Metadata 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 static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper();
public MetadataModule(IMetadataFactory metadataFactory) public MetadataController(IMetadataFactory metadataFactory)
: base(metadataFactory, "metadata", ResourceMapper) : base(metadataFactory, "metadata", ResourceMapper)
{ {
} }

@ -1,12 +1,14 @@
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using Readarr.Http;
namespace Readarr.Api.V1.Notifications 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 static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
public NotificationModule(NotificationFactory notificationFactory) public NotificationController(NotificationFactory notificationFactory)
: base(notificationFactory, "notification", ResourceMapper) : base(notificationFactory, "notification", ResourceMapper)
{ {
} }

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

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

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

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

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

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

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

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

@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using 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;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Queue; using NzbDrone.Core.Queue;
using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.Extensions; using Readarr.Http.Extensions;
using Readarr.Http.REST;
namespace Readarr.Api.V1.Queue 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> IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
{ {
private readonly IQueueService _queueService; private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService; private readonly IPendingReleaseService _pendingReleaseService;
private readonly QualityModelComparer _qualityComparer; private readonly QualityModelComparer _qualityComparer;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IFailedDownloadService _failedDownloadService;
private readonly IIgnoredDownloadService _ignoredDownloadService;
private readonly IProvideDownloadClient _downloadClientProvider;
public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage,
IQueueService queueService, IQueueService queueService,
IPendingReleaseService pendingReleaseService, IPendingReleaseService pendingReleaseService,
QualityProfileService qualityProfileService) QualityProfileService qualityProfileService,
ITrackedDownloadService trackedDownloadService,
IFailedDownloadService failedDownloadService,
IIgnoredDownloadService ignoredDownloadService,
IProvideDownloadClient downloadClientProvider)
: base(broadcastSignalRMessage) : base(broadcastSignalRMessage)
{ {
_queueService = queueService; _queueService = queueService;
_pendingReleaseService = pendingReleaseService; _pendingReleaseService = pendingReleaseService;
GetResourcePaged = GetQueue; _trackedDownloadService = trackedDownloadService;
_failedDownloadService = failedDownloadService;
_ignoredDownloadService = ignoredDownloadService;
_downloadClientProvider = downloadClientProvider;
_qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty)); _qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty));
} }
private PagingResource<QueueResource> GetQueue(PagingResource<QueueResource> pagingResource) public override QueueResource GetResourceById(int id)
{ {
throw new NotImplementedException();
}
[RestDeleteById]
public void RemoveAction(int id, bool removeFromClient = true, bool blacklist = false, bool skipReDownload = false)
{
var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload);
if (trackedDownload != null)
{
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
}
}
[HttpDelete]
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 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) 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) private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeAuthor, bool includeBook)
{ {
return queueItem.ToResource(includeAuthor, includeBook); return queueItem.ToResource(includeAuthor, includeBook);
} }
[NonAction]
public void Handle(QueueUpdatedEvent message) public void Handle(QueueUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);
} }
[NonAction]
public void Handle(PendingReleasesUpdatedEvent message) public void Handle(PendingReleasesUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

@ -1,65 +1,63 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Queue; using NzbDrone.Core.Queue;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.Extensions; using Readarr.Http.REST;
namespace Readarr.Api.V1.Queue 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> IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
{ {
private readonly IQueueService _queueService; private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService; private readonly IPendingReleaseService _pendingReleaseService;
public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
: base(broadcastSignalRMessage, "queue/details") : base(broadcastSignalRMessage)
{ {
_queueService = queueService; _queueService = queueService;
_pendingReleaseService = pendingReleaseService; _pendingReleaseService = pendingReleaseService;
GetResourceAll = GetQueue;
} }
private List<QueueResource> GetQueue() public override QueueResource GetResourceById(int id)
{
throw new NotImplementedException();
}
[HttpGet]
public List<QueueResource> GetQueue(int? 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 queue = _queueService.GetQueue();
var pending = _pendingReleaseService.GetPendingQueue(); var pending = _pendingReleaseService.GetPendingQueue();
var fullQueue = queue.Concat(pending); var fullQueue = queue.Concat(pending);
var authorIdQuery = Request.Query.AuthorId; if (authorId.HasValue)
var bookIdsQuery = Request.Query.BookIds;
if (authorIdQuery.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.Where(q => q.Book != null && bookIds.Contains(q.Book.Id)).ToResource(includeAuthor, includeBook);
} }
return fullQueue.ToResource(includeAuthor, includeBook); return fullQueue.ToResource(includeAuthor, includeBook);
} }
[NonAction]
public void Handle(QueueUpdatedEvent message) public void Handle(QueueUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);
} }
[NonAction]
public void Handle(PendingReleasesUpdatedEvent message) public void Handle(PendingReleasesUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Sync); BroadcastResourceChange(ModelAction.Sync);

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

@ -9,11 +9,9 @@
<ProjectReference Include="..\NzbDrone.SignalR\Readarr.SignalR.csproj" /> <ProjectReference Include="..\NzbDrone.SignalR\Readarr.SignalR.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageReference Include="FluentValidation" Version="8.6.2" /> <PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Ical.Net" Version="4.1.11" /> <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="NLog" Version="4.7.2" />
<PackageReference Include="System.IO.Abstractions" Version="12.0.4" /> <PackageReference Include="System.IO.Abstractions" Version="12.0.4" />
</ItemGroup> </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 FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using NzbDrone.Http.REST.Attributes;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.REST;
namespace Readarr.Api.V1.RemotePathMappings namespace Readarr.Api.V1.RemotePathMappings
{ {
public class RemotePathMappingModule : ReadarrRestModule<RemotePathMappingResource> [V1ApiController]
public class RemotePathMappingController : RestController<RemotePathMappingResource>
{ {
private readonly IRemotePathMappingService _remotePathMappingService; private readonly IRemotePathMappingService _remotePathMappingService;
public RemotePathMappingModule(IRemotePathMappingService remotePathMappingService, public RemotePathMappingController(IRemotePathMappingService remotePathMappingService,
PathExistsValidator pathExistsValidator, PathExistsValidator pathExistsValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator) MappedNetworkDriveValidator mappedNetworkDriveValidator)
{ {
_remotePathMappingService = remotePathMappingService; _remotePathMappingService = remotePathMappingService;
GetResourceAll = GetMappings;
GetResourceById = GetMappingById;
CreateResource = CreateMapping;
DeleteResource = DeleteMapping;
UpdateResource = UpdateMapping;
SharedValidator.RuleFor(c => c.Host) SharedValidator.RuleFor(c => c.Host)
.NotEmpty(); .NotEmpty();
@ -36,33 +34,37 @@ namespace Readarr.Api.V1.RemotePathMappings
.SetValidator(pathExistsValidator); .SetValidator(pathExistsValidator);
} }
private RemotePathMappingResource GetMappingById(int id) public override RemotePathMappingResource GetResourceById(int id)
{ {
return _remotePathMappingService.Get(id).ToResource(); return _remotePathMappingService.Get(id).ToResource();
} }
private int CreateMapping(RemotePathMappingResource resource) [RestPostById]
public ActionResult<RemotePathMappingResource> CreateMapping(RemotePathMappingResource resource)
{ {
var model = resource.ToModel(); var model = resource.ToModel();
return _remotePathMappingService.Add(model).Id; return Created(_remotePathMappingService.Add(model).Id);
} }
private List<RemotePathMappingResource> GetMappings() [HttpGet]
public List<RemotePathMappingResource> GetMappings()
{ {
return _remotePathMappingService.All().ToResource(); return _remotePathMappingService.All().ToResource();
} }
private void DeleteMapping(int id) [RestDeleteById]
public void DeleteMapping(int id)
{ {
_remotePathMappingService.Remove(id); _remotePathMappingService.Remove(id);
} }
private void UpdateMapping(RemotePathMappingResource resource) [RestPutById]
public ActionResult<RemotePathMappingResource> UpdateMapping(RemotePathMappingResource resource)
{ {
var mapping = resource.ToModel(); var mapping = resource.ToModel();
_remotePathMappingService.Update(mapping); return Accepted(_remotePathMappingService.Update(mapping));
} }
} }
} }

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

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

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

@ -1,52 +1,60 @@
using System.IO;
using System.Threading.Tasks; 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.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using Readarr.Http;
using Readarr.Http.Validation;
namespace Readarr.Api.V1.System namespace Readarr.Api.V1.System
{ {
public class SystemModule : ReadarrV1Module [V1ApiController]
public class SystemController : Controller
{ {
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IPlatformInfo _platformInfo; private readonly IPlatformInfo _platformInfo;
private readonly IOsInfo _osInfo; private readonly IOsInfo _osInfo;
private readonly IRouteCacheProvider _routeCacheProvider;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IMainDatabase _database; private readonly IMainDatabase _database;
private readonly ILifecycleService _lifecycleService; private readonly ILifecycleService _lifecycleService;
private readonly IDeploymentInfoProvider _deploymentInfoProvider; private readonly IDeploymentInfoProvider _deploymentInfoProvider;
private readonly EndpointDataSource _endpointData;
private readonly DfaGraphWriter _graphWriter;
private readonly DuplicateEndpointDetector _detector;
public SystemModule(IAppFolderInfo appFolderInfo, public SystemController(IAppFolderInfo appFolderInfo,
IRuntimeInfo runtimeInfo, IRuntimeInfo runtimeInfo,
IPlatformInfo platformInfo, IPlatformInfo platformInfo,
IOsInfo osInfo, IOsInfo osInfo,
IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider,
IConfigFileProvider configFileProvider, IMainDatabase database,
IMainDatabase database, ILifecycleService lifecycleService,
ILifecycleService lifecycleService, IDeploymentInfoProvider deploymentInfoProvider,
IDeploymentInfoProvider deploymentInfoProvider) EndpointDataSource endpoints,
: base("system") DfaGraphWriter graphWriter,
DuplicateEndpointDetector detector)
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_platformInfo = platformInfo; _platformInfo = platformInfo;
_osInfo = osInfo; _osInfo = osInfo;
_routeCacheProvider = routeCacheProvider;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_database = database; _database = database;
_lifecycleService = lifecycleService; _lifecycleService = lifecycleService;
_deploymentInfoProvider = deploymentInfoProvider; _deploymentInfoProvider = deploymentInfoProvider;
Get("/status", x => GetStatus()); _endpointData = endpoints;
Get("/routes", x => GetRoutes()); _graphWriter = graphWriter;
Post("/shutdown", x => Shutdown()); _detector = detector;
Post("/restart", x => Restart());
} }
private object GetStatus() [HttpGet("status")]
public object GetStatus()
{ {
return new return new
{ {
@ -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()); Task.Factory.StartNew(() => _lifecycleService.Shutdown());
return new { ShuttingDown = true }; return new { ShuttingDown = true };
} }
private object Restart() [HttpPost("restart")]
public object Restart()
{ {
Task.Factory.StartNew(() => _lifecycleService.Restart()); Task.Factory.StartNew(() => _lifecycleService.Restart());
return new { Restarting = true }; return new { Restarting = true };

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

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

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

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

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

@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.AuthorStats; using NzbDrone.Core.AuthorStats;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
@ -11,21 +12,23 @@ using Readarr.Http.Extensions;
namespace Readarr.Api.V1.Wanted 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, ISeriesBookLinkService seriesBookLinkService,
IAuthorStatisticsService authorStatisticsService, IAuthorStatisticsService authorStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster) 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> var pagingSpec = new PagingSpec<Book>
{ {
Page = pagingResource.Page, Page = pagingResource.Page,
@ -34,7 +37,6 @@ namespace Readarr.Api.V1.Wanted
SortDirection = pagingResource.SortDirection SortDirection = pagingResource.SortDirection
}; };
var includeAuthor = Request.GetBooleanQueryParameter("includeAuthor");
var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored");
if (monitoredFilter != null && monitoredFilter.Value == "false") if (monitoredFilter != null && monitoredFilter.Value == "false")
@ -46,9 +48,7 @@ namespace Readarr.Api.V1.Wanted
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true); pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true);
} }
var resource = ApplyToPage(_bookService.BooksWithoutFiles, pagingSpec, v => MapToResource(v, includeAuthor)); return pagingSpec.ApplyToPage(_bookService.BooksWithoutFiles, v => MapToResource(v, includeAuthor));
return resource;
} }
} }
} }

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

Loading…
Cancel
Save