New: Use ASP.NET Core instead of Nancy

Co-Authored-By: ta264 <ta264@users.noreply.github.com>
pull/5116/head
Qstick 3 years ago committed by Mark McDowall
parent b83bb2cade
commit 1169741c54

@ -128,7 +128,7 @@ class HistoryRow extends Component {
); );
} }
if (name === 'episodeTitle') { if (name === 'episodes.title') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeTitleLink <EpisodeTitleLink

@ -188,7 +188,7 @@ class QueueRow extends Component {
); );
} }
if (name === 'episode.title') { if (name === 'episodes.title') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
{ {
@ -206,7 +206,7 @@ class QueueRow extends Component {
); );
} }
if (name === 'episode.airDateUtc') { if (name === 'episodes.airDateUtc') {
if (episode) { if (episode) {
return ( return (
<RelativeDateCellConnector <RelativeDateCellConnector

@ -24,9 +24,7 @@ const requiresRestartKeys = [
'enableSsl', 'enableSsl',
'sslPort', 'sslPort',
'sslCertHash', 'sslCertHash',
'authenticationMethod', 'sslCertPassword'
'username',
'password'
]; ];
class GeneralSettings extends Component { class GeneralSettings extends Component {

@ -85,7 +85,6 @@ class SecuritySettings extends Component {
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText="Require Username and Password to access Sonarr" helpText="Require Username and Password to access Sonarr"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange} onChange={onInputChange}
{...authenticationMethod} {...authenticationMethod}
/> />
@ -99,7 +98,6 @@ class SecuritySettings extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="username" name="username"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange} onChange={onInputChange}
{...username} {...username}
/> />
@ -114,7 +112,6 @@ class SecuritySettings extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.PASSWORD} type={inputTypes.PASSWORD}
name="password" name="password"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange} onChange={onInputChange}
{...password} {...password}
/> />

@ -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) => {

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

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

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

@ -47,7 +47,7 @@ export const defaultState = {
isVisible: true isVisible: true
}, },
{ {
name: 'episodeTitle', name: 'episodes.title',
label: 'Episode Title', label: 'Episode Title',
isVisible: true isVisible: true
}, },

@ -75,13 +75,13 @@ export const defaultState = {
isVisible: true isVisible: true
}, },
{ {
name: 'episode.title', name: 'episodes.title',
label: 'Episode Title', label: 'Episode Title',
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'episode.airDateUtc', name: 'episodes.airDateUtc',
label: 'Episode Air Date', label: 'Episode Air Date',
isSortable: true, isSortable: true,
isVisible: false isVisible: false
@ -406,6 +406,7 @@ export const actionHandlers = handleThunks({
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}`, url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}`,
method: 'DELETE', method: 'DELETE',
dataType: 'json', dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({ ids }) data: JSON.stringify({ ids })
}).request; }).request;

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

@ -78,11 +78,9 @@ export const actionHandlers = handleThunks({
} = payload; } = payload;
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/failed', url: `/history/failed/${historyId}`,
method: 'POST', method: 'POST',
data: { dataType: 'json'
id: historyId
}
}).request; }).request;
promise.done(() => { promise.done(() => {

@ -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) => {

@ -21,7 +21,7 @@ export const defaultState = {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
pageSize: 20, pageSize: 20,
sortKey: 'airDateUtc', sortKey: 'episodes.airDateUtc',
sortDirection: sortDirections.DESCENDING, sortDirection: sortDirections.DESCENDING,
error: null, error: null,
items: [], items: [],
@ -39,12 +39,12 @@ export const defaultState = {
isVisible: true isVisible: true
}, },
{ {
name: 'episodeTitle', name: 'episodes.title',
label: 'Episode Title', label: 'Episode Title',
isVisible: true isVisible: true
}, },
{ {
name: 'airDateUtc', name: 'episodes.airDateUtc',
label: 'Air Date', label: 'Air Date',
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@ -94,7 +94,7 @@ export const defaultState = {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
pageSize: 20, pageSize: 20,
sortKey: 'airDateUtc', sortKey: 'episodes.airDateUtc',
sortDirection: sortDirections.DESCENDING, sortDirection: sortDirections.DESCENDING,
items: [], items: [],
@ -111,12 +111,12 @@ export const defaultState = {
isVisible: true isVisible: true
}, },
{ {
name: 'episodeTitle', name: 'episodes.episodeTitle',
label: 'Episode Title', label: 'Episode Title',
isVisible: true isVisible: true
}, },
{ {
name: 'airDateUtc', name: 'episodes.airDateUtc',
label: 'Air Date', label: 'Air Date',
isSortable: true, isSortable: true,
isVisible: true isVisible: true

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

@ -83,7 +83,7 @@ function CutoffUnmetRow(props) {
); );
} }
if (name === 'episodeTitle') { if (name === 'episodes.title') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeTitleLink <EpisodeTitleLink
@ -97,7 +97,7 @@ function CutoffUnmetRow(props) {
); );
} }
if (name === 'airDateUtc') { if (name === 'episodes.airDateUtc') {
return ( return (
<RelativeDateCellConnector <RelativeDateCellConnector
key={name} key={name}

@ -86,7 +86,7 @@ function MissingRow(props) {
); );
} }
if (name === 'episodeTitle') { if (name === 'episodes.title') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeTitleLink <EpisodeTitleLink
@ -100,7 +100,7 @@ function MissingRow(props) {
); );
} }
if (name === 'airDateUtc') { if (name === 'episodes.airDateUtc') {
return ( return (
<RelativeDateCellConnector <RelativeDateCellConnector
key={name} key={name}

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -17,7 +17,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles
[Test] [Test]
public void should_deserialize_releaseprofile_v3_ignored_null() public void should_deserialize_releaseprofile_v3_ignored_null()
{ {
var resource = Json.Deserialize<ReleaseProfileResource>("{ \"ignored\": null, \"required\": null }"); var resource = STJson.Deserialize<ReleaseProfileResource>("{ \"ignored\": null, \"required\": null }");
var model = resource.ToModel(); var model = resource.ToModel();
@ -28,7 +28,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles
[Test] [Test]
public void should_deserialize_releaseprofile_v3_ignored_string() public void should_deserialize_releaseprofile_v3_ignored_string()
{ {
var resource = Json.Deserialize<ReleaseProfileResource>("{ \"ignored\": \"testa,testb\", \"required\": \"testc,testd\" }"); var resource = STJson.Deserialize<ReleaseProfileResource>("{ \"ignored\": \"testa,testb\", \"required\": \"testc,testd\" }");
var model = resource.ToModel(); var model = resource.ToModel();
@ -39,7 +39,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles
[Test] [Test]
public void should_deserialize_releaseprofile_v3_ignored_string_array() public void should_deserialize_releaseprofile_v3_ignored_string_array()
{ {
var resource = Json.Deserialize<ReleaseProfileResource>("{ \"ignored\": [ \"testa\", \"testb\" ], \"required\": [ \"testc\", \"testd\" ] }"); var resource = STJson.Deserialize<ReleaseProfileResource>("{ \"ignored\": [ \"testa\", \"testb\" ], \"required\": [ \"testc\", \"testd\" ] }");
var model = resource.ToModel(); var model = resource.ToModel();
@ -50,7 +50,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles
[Test] [Test]
public void should_throw_with_bad_releaseprofile_v3_ignored_type() public void should_throw_with_bad_releaseprofile_v3_ignored_type()
{ {
var resource = Json.Deserialize<ReleaseProfileResource>("{ \"ignored\": {} }"); var resource = STJson.Deserialize<ReleaseProfileResource>("{ \"ignored\": {} }");
Assert.Throws<BadRequestException>(() => resource.ToModel()); Assert.Throws<BadRequestException>(() => resource.ToModel());
} }

@ -20,8 +20,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
private static Exception[] FilteredExceptions = new Exception[] private static Exception[] FilteredExceptions = new Exception[]
{ {
new UnauthorizedAccessException(), new UnauthorizedAccessException()
new TinyIoC.TinyIoCResolutionException(typeof(string))
}; };
[SetUp] [SetUp]

@ -1,8 +1,13 @@
using System.Linq; using System.Linq;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Datastore; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host; using NzbDrone.Host;
@ -16,12 +21,16 @@ namespace NzbDrone.Common.Test
[Test] [Test]
public void event_handlers_should_be_unique() public void event_handlers_should_be_unique()
{ {
var container = MainAppContainerBuilder.BuildContainer(new StartupContext()); var container = new Container(rules => rules.WithNzbDroneRules())
container.Register<IMainDatabase>(new MainDatabase(null)); .AddNzbDroneLogger()
container.Register<ILogDatabase>(new LogDatabase(null)); .AutoAddServices(Bootstrap.ASSEMBLIES)
container.Resolve<IAppFolderFactory>().Register(); .AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"))
.GetServiceProvider();
Mocker.SetConstant(container); container.GetRequiredService<IAppFolderFactory>().Register();
Mocker.SetConstant<System.IServiceProvider>(container);
var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>() var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>()
.Select(c => c.GetType().FullName); .Select(c => c.GetType().FullName);

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Composition
{
public class AssemblyLoader
{
static AssemblyLoader()
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
}
public static IEnumerable<Assembly> Load(IEnumerable<string> assemblyNames)
{
var toLoad = assemblyNames.ToList();
toLoad.Add("Sonarr.Common");
toLoad.Add(OsInfo.IsWindows ? "Sonarr.Windows" : "Sonarr.Mono");
var toRegisterResolver = new List<string> { "System.Data.SQLite" };
toRegisterResolver.AddRange(assemblyNames.Intersect(new[] { "Sonarr.Core" }));
RegisterNativeResolver(toRegisterResolver);
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
return toLoad.Select(x =>
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")));
}
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)
{
var resolver = new AssemblyDependencyResolver(args.RequestingAssembly.Location);
var assemblyPath = resolver.ResolveAssemblyToPath(new AssemblyName(args.Name));
if (assemblyPath == null)
{
return null;
}
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
}
public static void RegisterNativeResolver(IEnumerable<string> assemblyNames)
{
foreach (var name in assemblyNames)
{
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
// is less likely to exist.
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{name}.dll"));
try
{
NativeLibrary.SetDllImportResolver(assembly, LoadNativeLib);
}
catch (InvalidOperationException)
{
// This can only be set once per assembly
// Catch required for NzbDrone.Host tests
}
}
}
private static IntPtr LoadNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
{
var mappedName = libraryName;
if (OsInfo.IsLinux)
{
if (libraryName == "sqlite3")
{
mappedName = "libsqlite3.so.0";
}
else if (libraryName == "mediainfo")
{
mappedName = "libmediainfo.so.0";
}
}
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
}
}
}

@ -1,113 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TinyIoC;
namespace NzbDrone.Common.Composition
{
public class Container : IContainer
{
private readonly TinyIoCContainer _container;
private readonly List<Type> _loadedTypes;
public Container(TinyIoCContainer container, List<Type> loadedTypes)
{
_container = container;
_loadedTypes = loadedTypes;
_container.Register<IContainer>(this);
}
public void Register<TService, TImplementation>()
where TImplementation : class, TService
where TService : class
{
_container.Register<TService, TImplementation>();
}
public void Register<T>(T instance)
where T : class
{
_container.Register<T>(instance);
}
public T Resolve<T>()
where T : class
{
return _container.Resolve<T>();
}
public object Resolve(Type type)
{
return _container.Resolve(type);
}
public void Register(Type serviceType, Type implementationType)
{
_container.Register(serviceType, implementationType);
}
public void Register<TService>(Func<IContainer, TService> factory)
where TService : class
{
_container.Register((c, n) => factory(this));
}
public void RegisterSingleton(Type service, Type implementation)
{
var factory = CreateSingletonImplementationFactory(implementation);
// For Resolve and ResolveAll
_container.Register(service, factory);
// For ctor(IEnumerable<T>)
var enumerableType = typeof(IEnumerable<>).MakeGenericType(service);
_container.Register(enumerableType, (c, p) =>
{
var instance = factory(c, p);
var result = Array.CreateInstance(service, 1);
result.SetValue(instance, 0);
return result;
});
}
public IEnumerable<T> ResolveAll<T>()
where T : class
{
return _container.ResolveAll<T>();
}
public void RegisterAllAsSingleton(Type service, IEnumerable<Type> implementationList)
{
foreach (var implementation in implementationList)
{
var factory = CreateSingletonImplementationFactory(implementation);
// For ResolveAll and ctor(IEnumerable<T>)
_container.Register(service, factory, implementation.FullName);
}
}
private Func<TinyIoCContainer, NamedParameterOverloads, object> CreateSingletonImplementationFactory(Type implementation)
{
const string singleImplPrefix = "singleImpl_";
_container.Register(implementation, implementation, singleImplPrefix + implementation.FullName).AsSingleton();
return (c, p) => _container.Resolve(implementation, singleImplPrefix + implementation.FullName);
}
public bool IsTypeRegistered(Type type)
{
return _container.CanResolve(type);
}
public IEnumerable<Type> GetImplementations(Type contractType)
{
return _loadedTypes
.Where(implementation =>
contractType.IsAssignableFrom(implementation) &&
!implementation.IsInterface &&
!implementation.IsAbstract);
}
}
}

@ -1,136 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Messaging;
using TinyIoC;
namespace NzbDrone.Common.Composition
{
public abstract class ContainerBuilderBase
{
private readonly List<Type> _loadedTypes;
protected IContainer Container { get; }
protected ContainerBuilderBase(IStartupContext args, List<string> assemblies)
{
_loadedTypes = new List<Type>();
assemblies.Add(OsInfo.IsWindows ? "Sonarr.Windows" : "Sonarr.Mono");
assemblies.Add("Sonarr.Common");
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
foreach (var assemblyName in assemblies)
{
_loadedTypes.AddRange(AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{assemblyName}.dll")).GetExportedTypes());
}
var toRegisterResolver = new List<string> { "System.Data.SQLite" };
toRegisterResolver.AddRange(assemblies.Intersect(new[] { "Sonarr.Core" }));
RegisterNativeResolver(toRegisterResolver);
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
Container = new Container(new TinyIoCContainer(), _loadedTypes);
AutoRegisterInterfaces();
Container.Register(args);
}
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)
{
var resolver = new AssemblyDependencyResolver(args.RequestingAssembly.Location);
var assemblyPath = resolver.ResolveAssemblyToPath(new AssemblyName(args.Name));
if (assemblyPath == null)
{
return null;
}
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
}
public static void RegisterNativeResolver(IEnumerable<string> assemblyNames)
{
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
// is less likely to exist.
foreach (var name in assemblyNames)
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{name}.dll"));
try
{
NativeLibrary.SetDllImportResolver(assembly, LoadNativeLib);
}
catch (InvalidOperationException)
{
// This can only be set once per assembly
// Catch required for NzbDrone.Host tests
}
}
}
private static IntPtr LoadNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
{
var mappedName = libraryName;
if (OsInfo.IsLinux)
{
if (libraryName == "sqlite3")
{
mappedName = "libsqlite3.so.0";
}
else if (libraryName == "mediainfo")
{
mappedName = "libmediainfo.so.0";
}
}
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
}
private void AutoRegisterInterfaces()
{
var loadedInterfaces = _loadedTypes.Where(t => t.IsInterface).ToList();
var implementedInterfaces = _loadedTypes.SelectMany(t => t.GetInterfaces());
var contracts = loadedInterfaces.Union(implementedInterfaces).Where(c => !c.IsGenericTypeDefinition && !string.IsNullOrWhiteSpace(c.FullName))
.Where(c => !c.FullName.StartsWith("System"))
.Except(new List<Type> { typeof(IMessage), typeof(IEvent), typeof(IContainer) }).Distinct().OrderBy(c => c.FullName);
foreach (var contract in contracts)
{
AutoRegisterImplementations(contract);
}
}
protected void AutoRegisterImplementations<TContract>()
{
AutoRegisterImplementations(typeof(TContract));
}
private void AutoRegisterImplementations(Type contractType)
{
var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList();
if (implementations.Count == 0)
{
return;
}
if (implementations.Count == 1)
{
var impl = implementations.Single();
Container.RegisterSingleton(contractType, impl);
}
else
{
Container.RegisterAllAsSingleton(contractType, implementations);
}
}
}
}

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using DryIoc;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Composition.Extensions
{
public static class ServiceCollectionExtensions
{
public static Rules WithNzbDroneRules(this Rules rules)
{
return rules.WithMicrosoftDependencyInjectionRules()
.WithAutoConcreteTypeResolution()
.WithDefaultReuse(Reuse.Singleton);
}
public static IContainer AddStartupContext(this IContainer container, StartupContext context)
{
container.RegisterInstance<IStartupContext>(context, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
return container;
}
public static IContainer AutoAddServices(this IContainer container, List<string> assemblyNames)
{
var assemblies = AssemblyLoader.Load(assemblyNames);
container.RegisterMany(assemblies,
serviceTypeCondition: type => type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"),
reuse: Reuse.Singleton);
container.RegisterMany(assemblies,
serviceTypeCondition: type => !type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"),
reuse: Reuse.Transient);
var knownTypes = new KnownTypes(assemblies.SelectMany(x => x.GetTypes()).ToList());
container.RegisterInstance(knownTypes);
return container;
}
}
}

@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Common.Composition
{
public interface IContainer
{
void Register<T>(T instance)
where T : class;
void Register<TService, TImplementation>()
where TImplementation : class, TService
where TService : class;
T Resolve<T>()
where T : class;
object Resolve(Type type);
void Register(Type serviceType, Type implementationType);
void Register<TService>(Func<IContainer, TService> factory)
where TService : class;
void RegisterSingleton(Type service, Type implementation);
IEnumerable<T> ResolveAll<T>()
where T : class;
void RegisterAllAsSingleton(Type registrationType, IEnumerable<Type> implementationList);
bool IsTypeRegistered(Type type);
IEnumerable<Type> GetImplementations(Type contractType);
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Common.Composition
{
public class KnownTypes
{
private List<Type> _knownTypes;
// So unity can resolve for tests
public KnownTypes()
: this(new List<Type>())
{
}
public KnownTypes(List<Type> loadedTypes)
{
_knownTypes = loadedTypes;
}
public IEnumerable<Type> GetImplementations(Type contractType)
{
return _knownTypes
.Where(implementation =>
contractType.IsAssignableFrom(implementation) &&
!implementation.IsInterface &&
!implementation.IsAbstract);
}
}
}

@ -286,6 +286,11 @@ namespace NzbDrone.Common.Extensions
return appFolderInfo.AppDataFolder; return appFolderInfo.AppDataFolder;
} }
public static string GetDataProtectionPath(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetAppDataPath(appFolderInfo), "asp");
}
public static string GetLogFolder(this IAppFolderInfo appFolderInfo) public static string GetLogFolder(this IAppFolderInfo appFolderInfo)
{ {
return Path.Combine(GetAppDataPath(appFolderInfo), "logs"); return Path.Combine(GetAppDataPath(appFolderInfo), "logs");

@ -0,0 +1,14 @@
using DryIoc;
using NLog;
namespace NzbDrone.Common.Instrumentation.Extensions
{
public static class CompositionExtensions
{
public static IContainer AddNzbDroneLogger(this IContainer container)
{
container.Register(Made.Of<Logger>(() => LogManager.GetLogger(Arg.Index<string>(0)), r => r.Parent.ImplementationType.Name.ToString()), reuse: Reuse.Transient);
return container;
}
}
}

@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
"UnauthorizedAccessException", "UnauthorizedAccessException",
// Filter out people stuck in boot loops // Filter out people stuck in boot loops
"CorruptDatabaseException", "CorruptDatabaseException"
// This also filters some people in boot loops
"TinyIoCResolutionException"
}; };
public static readonly List<string> FilteredExceptionMessages = new List<string> public static readonly List<string> FilteredExceptionMessages = new List<string>

@ -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,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Composition; using Microsoft.Extensions.DependencyInjection;
namespace NzbDrone.Common namespace NzbDrone.Common
{ {
@ -17,9 +17,9 @@ namespace NzbDrone.Common
public class ServiceFactory : IServiceFactory public class ServiceFactory : IServiceFactory
{ {
private readonly IContainer _container; private readonly System.IServiceProvider _container;
public ServiceFactory(IContainer container) public ServiceFactory(System.IServiceProvider container)
{ {
_container = container; _container = container;
} }
@ -27,23 +27,23 @@ namespace NzbDrone.Common
public T Build<T>() public T Build<T>()
where T : class where T : class
{ {
return _container.Resolve<T>(); return _container.GetRequiredService<T>();
} }
public IEnumerable<T> BuildAll<T>() public IEnumerable<T> BuildAll<T>()
where T : class where T : class
{ {
return _container.ResolveAll<T>().GroupBy(c => c.GetType().FullName).Select(g => g.First()); return _container.GetServices<T>().GroupBy(c => c.GetType().FullName).Select(g => g.First());
} }
public object Build(Type contract) public object Build(Type contract)
{ {
return _container.Resolve(contract); return _container.GetRequiredService(contract);
} }
public IEnumerable<Type> GetImplementations(Type contract) public IEnumerable<Type> GetImplementations(Type contract)
{ {
return _container.GetImplementations(contract); return _container.GetServices(contract).Select(x => x.GetType());
} }
} }
} }

@ -4,10 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" /> <PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" /> <PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Sentry" Version="2.1.8" /> <PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.4" /> <PackageReference Include="System.Text.Json" Version="6.0.4" />

File diff suppressed because it is too large Load Diff

@ -1,15 +0,0 @@
using NzbDrone.Host;
namespace NzbDrone.Console
{
public class ConsoleAlerts : IUserAlert
{
public void Alert(string message)
{
System.Console.WriteLine();
System.Console.WriteLine(message);
System.Console.WriteLine("Press enter to continue");
System.Console.ReadLine();
}
}
}

@ -1,5 +1,9 @@
using System; using System;
using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
@ -14,7 +18,7 @@ namespace NzbDrone.Console
{ {
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConsoleApp)); private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConsoleApp));
private enum ExitCodes : int private enum ExitCodes
{ {
Normal = 0, Normal = 0,
UnknownFailure = 1, UnknownFailure = 1,
@ -40,7 +44,7 @@ namespace NzbDrone.Console
throw; throw;
} }
Bootstrap.Start(startupArgs, new ConsoleAlerts()); Bootstrap.Start(args);
} }
catch (SonarrStartupException ex) catch (SonarrStartupException ex)
{ {
@ -56,6 +60,20 @@ namespace NzbDrone.Console
Logger.Fatal(ex.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions"); Logger.Fatal(ex.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions");
Exit(ExitCodes.RecoverableFailure, startupArgs); Exit(ExitCodes.RecoverableFailure, startupArgs);
} }
catch (IOException ex)
{
if (ex.InnerException is AddressInUseException)
{
System.Console.WriteLine("");
System.Console.WriteLine("");
Logger.Fatal(ex.Message + " This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions");
Exit(ExitCodes.RecoverableFailure, startupArgs);
}
else
{
throw;
}
}
catch (RemoteAccessException ex) catch (RemoteAccessException ex)
{ {
System.Console.WriteLine(""); System.Console.WriteLine("");

@ -2,6 +2,7 @@ using System;
using System.Data; using System.Data;
using FluentMigrator; using FluentMigrator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
@ -32,7 +33,7 @@ namespace NzbDrone.Core.Test.Framework
protected override void SetupLogging() protected override void SetupLogging()
{ {
Mocker.SetConstant<ILoggerProvider>(Mocker.Resolve<MigrationLoggerProvider>()); Mocker.SetConstant<ILoggerProvider>(Mocker.Resolve<NLogLoggerProvider>());
} }
private ITestDatabase WithMigrationAction(Action<TMigration> beforeMigration = null) private ITestDatabase WithMigrationAction(Action<TMigration> beforeMigration = null)

@ -1,7 +1,6 @@
using System; using System;
using System.Data.SQLite; using System.Data.SQLite;
using NLog; using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
@ -40,17 +39,6 @@ namespace NzbDrone.Core.Datastore
Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true"); Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true");
} }
public static void RegisterDatabase(IContainer container)
{
var mainDb = new MainDatabase(container.Resolve<IDbFactory>().Create());
container.Register<IMainDatabase>(mainDb);
var logDb = new LogDatabase(container.Resolve<IDbFactory>().Create(MigrationType.Log));
container.Register<ILogDatabase>(logDb);
}
public DbFactory(IMigrationController migrationController, public DbFactory(IMigrationController migrationController,
IConnectionStringFactory connectionStringFactory, IConnectionStringFactory connectionStringFactory,
IDiskProvider diskProvider, IDiskProvider diskProvider,

@ -0,0 +1,24 @@
using DryIoc;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Extensions
{
public static class CompositionExtensions
{
public static IContainer AddDatabase(this IContainer container)
{
container.RegisterDelegate<IDbFactory, IMainDatabase>(f => new MainDatabase(f.Create()), Reuse.Singleton);
container.RegisterDelegate<IDbFactory, ILogDatabase>(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton);
return container;
}
public static IContainer AddDummyDatabase(this IContainer container)
{
container.RegisterInstance<IMainDatabase>(new MainDatabase(null));
container.RegisterInstance<ILogDatabase>(new LogDatabase(null));
return container;
}
}
}

@ -7,6 +7,7 @@ using FluentMigrator.Runner.Processors;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NLog; using NLog;
using NLog.Extensions.Logging;
namespace NzbDrone.Core.Datastore.Migration.Framework namespace NzbDrone.Core.Datastore.Migration.Framework
{ {
@ -34,7 +35,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_logger.Info("*** Migrating {0} ***", connectionString); _logger.Info("*** Migrating {0} ***", connectionString);
var serviceProvider = new ServiceCollection() var serviceProvider = new ServiceCollection()
.AddLogging(lb => lb.AddProvider(_migrationLoggerProvider)) .AddLogging(b => b.AddNLog())
.AddFluentMigratorCore() .AddFluentMigratorCore()
.ConfigureRunner( .ConfigureRunner(
builder => builder builder => builder

@ -1,59 +0,0 @@
using System;
using FluentMigrator.Runner;
using FluentMigrator.Runner.Logging;
using NLog;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
public class MigrationLogger : FluentMigratorLogger
{
private readonly Logger _logger;
public MigrationLogger(Logger logger,
FluentMigratorLoggerOptions options)
: base(options)
{
_logger = logger;
}
protected override void WriteHeading(string message)
{
_logger.Info("*** {0} ***", message);
}
protected override void WriteSay(string message)
{
_logger.Debug(message);
}
protected override void WriteEmphasize(string message)
{
_logger.Warn(message);
}
protected override void WriteSql(string sql)
{
_logger.Debug(sql);
}
protected override void WriteEmptySql()
{
_logger.Debug(@"No SQL statement executed.");
}
protected override void WriteElapsedTime(TimeSpan timeSpan)
{
_logger.Debug("Took: {0}", timeSpan);
}
protected override void WriteError(string message)
{
_logger.Error(message);
}
protected override void WriteError(Exception exception)
{
_logger.Error(exception);
}
}
}

@ -1,26 +0,0 @@
using FluentMigrator.Runner;
using Microsoft.Extensions.Logging;
using NLog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
public class MigrationLoggerProvider : ILoggerProvider
{
private readonly Logger _logger;
public MigrationLoggerProvider(Logger logger)
{
_logger = logger;
}
public ILogger CreateLogger(string categoryName)
{
return new MigrationLogger(_logger, new FluentMigratorLoggerOptions() { ShowElapsedTime = true, ShowSql = true });
}
public void Dispose()
{
}
}
}

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@ -22,7 +21,7 @@ namespace NzbDrone.Core.Download
public DownloadClientFactory(IDownloadClientStatusService downloadClientStatusService, public DownloadClientFactory(IDownloadClientStatusService downloadClientStatusService,
IDownloadClientRepository providerRepository, IDownloadClientRepository providerRepository,
IEnumerable<IDownloadClient> providers, IEnumerable<IDownloadClient> providers,
IContainer container, IServiceProvider container,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Extras
public ExistingExtraFileService(IDiskProvider diskProvider, public ExistingExtraFileService(IDiskProvider diskProvider,
IDiskScanService diskScanService, IDiskScanService diskScanService,
List<IImportExistingExtraFiles> existingExtraFileImporters, IEnumerable<IImportExistingExtraFiles> existingExtraFileImporters,
Logger logger) Logger logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;

@ -17,7 +17,7 @@ namespace NzbDrone.Core.Extras.Metadata
{ {
private readonly IMetadataRepository _providerRepository; private readonly IMetadataRepository _providerRepository;
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, IEventAggregator eventAggregator, Logger logger) public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
@ -21,7 +22,7 @@ namespace NzbDrone.Core.ImportLists
public ImportListFactory(IImportListStatusService importListStatusService, public ImportListFactory(IImportListStatusService importListStatusService,
IImportListRepository providerRepository, IImportListRepository providerRepository,
IEnumerable<IImportList> providers, IEnumerable<IImportList> providers,
IContainer container, IServiceProvider container,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
@ -23,7 +24,7 @@ namespace NzbDrone.Core.Indexers
public IndexerFactory(IIndexerStatusService indexerStatusService, public IndexerFactory(IIndexerStatusService indexerStatusService,
IIndexerRepository providerRepository, IIndexerRepository providerRepository,
IEnumerable<IIndexer> providers, IEnumerable<IIndexer> providers,
IContainer container, IServiceProvider container,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)

@ -5,6 +5,7 @@ using System.Net;
using System.Threading; using System.Threading;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
@ -36,17 +37,18 @@ namespace NzbDrone.Core.Messaging.Commands
public class CommandQueueManager : IManageCommandQueue, IHandle<ApplicationStartedEvent> public class CommandQueueManager : IManageCommandQueue, IHandle<ApplicationStartedEvent>
{ {
private readonly ICommandRepository _repo; private readonly ICommandRepository _repo;
private readonly IServiceFactory _serviceFactory; private readonly KnownTypes _knownTypes;
private readonly Logger _logger; private readonly Logger _logger;
private readonly CommandQueue _commandQueue; private readonly CommandQueue _commandQueue;
public CommandQueueManager(ICommandRepository repo, public CommandQueueManager(ICommandRepository repo,
IServiceFactory serviceFactory, IServiceFactory serviceFactory,
KnownTypes knownTypes,
Logger logger) Logger logger)
{ {
_repo = repo; _repo = repo;
_serviceFactory = serviceFactory; _knownTypes = knownTypes;
_logger = logger; _logger = logger;
_commandQueue = new CommandQueue(); _commandQueue = new CommandQueue();
@ -232,9 +234,8 @@ namespace NzbDrone.Core.Messaging.Commands
private dynamic GetCommand(string commandName) private dynamic GetCommand(string commandName)
{ {
commandName = commandName.Split('.').Last(); commandName = commandName.Split('.').Last();
var commands = _knownTypes.GetImplementations(typeof(Command));
var commandType = _serviceFactory.GetImplementations(typeof(Command)) var commandType = commands.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase));
.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase));
return Json.Deserialize("{}", commandType); return Json.Deserialize("{}", commandType);
} }

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
@ -22,7 +23,7 @@ namespace NzbDrone.Core.Notifications
public class NotificationFactory : ProviderFactory<INotification, NotificationDefinition>, INotificationFactory public class NotificationFactory : ProviderFactory<INotification, NotificationDefinition>, INotificationFactory
{ {
public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IContainer container, IEventAggregator eventAggregator, Logger logger) public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
} }

@ -6,13 +6,15 @@
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="MailKit" Version="2.10.1" /> <PackageReference Include="MailKit" Version="2.10.1" />
<PackageReference Include="System.Memory" Version="4.5.4" /> <PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" /> <PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" /> <PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
<PackageReference Include="FluentValidation" Version="8.6.2" /> <PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="TinyTwitter" Version="1.1.2" />
<PackageReference Include="Kveer.XmlRPC" Version="1.2.0" /> <PackageReference Include="Kveer.XmlRPC" Version="1.2.0" />
<PackageReference Include="MonoTorrent" Version="2.0.5" /> <PackageReference Include="MonoTorrent" Version="2.0.5" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.0-0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.0-0" />

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using Microsoft.Extensions.DependencyInjection;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
@ -15,7 +16,7 @@ namespace NzbDrone.Core.ThingiProvider
where TProvider : IProvider where TProvider : IProvider
{ {
private readonly IProviderRepository<TProviderDefinition> _providerRepository; private readonly IProviderRepository<TProviderDefinition> _providerRepository;
private readonly IContainer _container; private readonly IServiceProvider _container;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
@ -23,7 +24,7 @@ namespace NzbDrone.Core.ThingiProvider
protected ProviderFactory(IProviderRepository<TProviderDefinition> providerRepository, protected ProviderFactory(IProviderRepository<TProviderDefinition> providerRepository,
IEnumerable<TProvider> providers, IEnumerable<TProvider> providers,
IContainer container, IServiceProvider container,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
{ {
@ -129,7 +130,7 @@ namespace NzbDrone.Core.ThingiProvider
public TProvider GetInstance(TProviderDefinition definition) public TProvider GetInstance(TProviderDefinition definition)
{ {
var type = GetImplementation(definition); var type = GetImplementation(definition);
var instance = (TProvider)_container.Resolve(type); var instance = (TProvider)_container.GetRequiredService(type);
instance.Definition = definition; instance.Definition = definition;
SetProviderCharacteristics(instance, definition); SetProviderCharacteristics(instance, definition);
return instance; return instance;

@ -0,0 +1,44 @@
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Validation.Paths
{
public class RecycleBinValidator : PropertyValidator
{
private readonly IConfigService _configService;
public RecycleBinValidator(IConfigService configService)
: base("Path is {relationship} configured recycle bin folder")
{
_configService = configService;
}
protected override bool IsValid(PropertyValidatorContext context)
{
var recycleBin = _configService.RecycleBin;
var folder = context.PropertyValue.ToString();
if (context.PropertyValue == null || recycleBin.IsNullOrWhiteSpace())
{
return true;
}
if (recycleBin.PathEquals(folder))
{
context.MessageFormatter.AppendArgument("relationship", "set to");
return false;
}
if (recycleBin.IsParentPath(folder))
{
context.MessageFormatter.AppendArgument("relationship", "child of");
return false;
}
return true;
}
}
}

@ -0,0 +1,28 @@
using System.Linq;
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Validation.Paths
{
public class RootFolderAncestorValidator : PropertyValidator
{
private readonly IRootFolderService _rootFolderService;
public RootFolderAncestorValidator(IRootFolderService rootFolderService)
: base("Path is an ancestor of an existing root folder")
{
_rootFolderService = rootFolderService;
}
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null)
{
return true;
}
return !_rootFolderService.All().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path));
}
}
}

@ -1,12 +1,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Datastore; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@ -17,44 +21,50 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host; using NzbDrone.Host;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using IServiceProvider = System.IServiceProvider;
namespace NzbDrone.App.Test namespace NzbDrone.App.Test
{ {
[TestFixture] [TestFixture]
public class ContainerFixture : TestBase public class ContainerFixture : TestBase
{ {
private IContainer _container; private IServiceProvider _container;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
var args = new StartupContext("first", "second"); var args = new StartupContext("first", "second");
_container = MainAppContainerBuilder.BuildContainer(args);
_container.Register<IMainDatabase>(new MainDatabase(null));
// set up a dummy broadcaster to allow tests to resolve // set up a dummy broadcaster to allow tests to resolve
var mockBroadcaster = new Mock<IBroadcastSignalRMessage>(); var mockBroadcaster = new Mock<IBroadcastSignalRMessage>();
_container.Register<IBroadcastSignalRMessage>(mockBroadcaster.Object);
var container = new Container(rules => rules.WithNzbDroneRules())
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddNzbDroneLogger()
.AddDummyDatabase()
.AddStartupContext(args);
container.RegisterInstance<IBroadcastSignalRMessage>(mockBroadcaster.Object);
_container = container.GetServiceProvider();
} }
[Test] [Test]
public void should_be_able_to_resolve_indexers() public void should_be_able_to_resolve_indexers()
{ {
_container.Resolve<IEnumerable<IIndexer>>().Should().NotBeEmpty(); _container.GetRequiredService<IEnumerable<IIndexer>>().Should().NotBeEmpty();
} }
[Test] [Test]
public void should_be_able_to_resolve_downloadclients() public void should_be_able_to_resolve_downloadclients()
{ {
_container.Resolve<IEnumerable<IDownloadClient>>().Should().NotBeEmpty(); _container.GetRequiredService<IEnumerable<IDownloadClient>>().Should().NotBeEmpty();
} }
[Test] [Test]
public void container_should_inject_itself() public void container_should_inject_itself()
{ {
var factory = _container.Resolve<IServiceFactory>(); var factory = _container.GetRequiredService<IServiceFactory>();
factory.Build<IIndexerFactory>().Should().NotBeNull(); factory.Build<IIndexerFactory>().Should().NotBeNull();
} }
@ -64,7 +74,7 @@ namespace NzbDrone.App.Test
{ {
var genericExecutor = typeof(IExecute<>).MakeGenericType(typeof(RssSyncCommand)); var genericExecutor = typeof(IExecute<>).MakeGenericType(typeof(RssSyncCommand));
var executor = _container.Resolve(genericExecutor); var executor = _container.GetRequiredService(genericExecutor);
executor.Should().NotBeNull(); executor.Should().NotBeNull();
executor.Should().BeAssignableTo<IExecute<RssSyncCommand>>(); executor.Should().BeAssignableTo<IExecute<RssSyncCommand>>();
@ -73,17 +83,17 @@ namespace NzbDrone.App.Test
[Test] [Test]
public void should_return_same_instance_via_resolve_and_resolveall() public void should_return_same_instance_via_resolve_and_resolveall()
{ {
var first = (DownloadMonitoringService)_container.Resolve<IHandle<TrackedDownloadsRemovedEvent>>(); var first = (DownloadMonitoringService)_container.GetRequiredService<IHandle<TrackedDownloadsRemovedEvent>>();
var second = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single(); var second = _container.GetServices<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
first.Should().BeSameAs(second); first.Should().BeSameAs(second);
} }
[Test] [Test]
public void should_return_same_instance_of_singletons_by_same_interface() public void should_return_same_instance_of_singletons_by_different_same_interface()
{ {
var first = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single(); var first = _container.GetServices<IHandle<EpisodeGrabbedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single(); var second = _container.GetServices<IHandle<EpisodeGrabbedEvent>>().OfType<DownloadMonitoringService>().Single();
first.Should().BeSameAs(second); first.Should().BeSameAs(second);
} }
@ -91,8 +101,8 @@ namespace NzbDrone.App.Test
[Test] [Test]
public void should_return_same_instance_of_singletons_by_different_interfaces() public void should_return_same_instance_of_singletons_by_different_interfaces()
{ {
var first = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single(); var first = _container.GetServices<IHandle<EpisodeGrabbedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = (DownloadMonitoringService)_container.Resolve<IExecute<RefreshMonitoredDownloadsCommand>>(); var second = (DownloadMonitoringService)_container.GetRequiredService<IExecute<RefreshMonitoredDownloadsCommand>>();
first.Should().BeSameAs(second); first.Should().BeSameAs(second);
} }

@ -1,4 +1,3 @@
using System.ServiceProcess;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common; using NzbDrone.Common;
@ -10,7 +9,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.App.Test namespace NzbDrone.App.Test
{ {
[TestFixture] [TestFixture]
public class RouterTest : TestBase<Router> public class RouterTest : TestBase<UtilityModeRouter>
{ {
[SetUp] [SetUp]
public void Setup() public void Setup()
@ -49,33 +48,6 @@ namespace NzbDrone.App.Test
serviceProviderMock.Verify(c => c.Uninstall(ServiceProvider.SERVICE_NAME), Times.Once()); serviceProviderMock.Verify(c => c.Uninstall(ServiceProvider.SERVICE_NAME), Times.Once());
} }
[Test]
public void Route_should_call_console_service_when_application_mode_is_console()
{
Mocker.GetMock<IRuntimeInfo>().SetupGet(c => c.IsUserInteractive).Returns(true);
Subject.Route(ApplicationModes.Interactive);
Mocker.GetMock<INzbDroneConsoleFactory>().Verify(c => c.Start(), Times.Once());
}
[Test]
public void Route_should_call_service_start_when_run_in_service_mode()
{
var envMock = Mocker.GetMock<IRuntimeInfo>();
var serviceProvider = Mocker.GetMock<IServiceProvider>();
envMock.SetupGet(c => c.IsUserInteractive).Returns(false);
serviceProvider.Setup(c => c.Run(It.IsAny<ServiceBase>()));
serviceProvider.Setup(c => c.ServiceExist(It.IsAny<string>())).Returns(true);
serviceProvider.Setup(c => c.GetStatus(It.IsAny<string>())).Returns(ServiceControllerStatus.StartPending);
Subject.Route(ApplicationModes.Service);
serviceProvider.Verify(c => c.Run(It.IsAny<ServiceBase>()), Times.Once());
}
[Test] [Test]
public void show_error_on_install_if_service_already_exist() public void show_error_on_install_if_service_already_exist()
{ {

@ -2,6 +2,11 @@ using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Host.AccessControl namespace NzbDrone.Host.AccessControl
{ {
public interface IRemoteAccessAdapter
{
void MakeAccessible(bool passive);
}
public class RemoteAccessAdapter : IRemoteAccessAdapter public class RemoteAccessAdapter : IRemoteAccessAdapter
{ {
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;

@ -0,0 +1,120 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host;
namespace NzbDrone.Host
{
public class AppLifetime : IHostedService, IHandle<ApplicationShutdownRequested>
{
private readonly IHostApplicationLifetime _appLifetime;
private readonly IConfigFileProvider _configFileProvider;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IStartupContext _startupContext;
private readonly IBrowserService _browserService;
private readonly IProcessProvider _processProvider;
private readonly IEventAggregator _eventAggregator;
private readonly IUtilityModeRouter _utilityModeRouter;
private readonly Logger _logger;
public AppLifetime(IHostApplicationLifetime appLifetime,
IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo,
IStartupContext startupContext,
IBrowserService browserService,
IProcessProvider processProvider,
IEventAggregator eventAggregator,
IUtilityModeRouter utilityModeRouter,
Logger logger)
{
_appLifetime = appLifetime;
_configFileProvider = configFileProvider;
_runtimeInfo = runtimeInfo;
_startupContext = startupContext;
_browserService = browserService;
_processProvider = processProvider;
_eventAggregator = eventAggregator;
_utilityModeRouter = utilityModeRouter;
_logger = logger;
appLifetime.ApplicationStarted.Register(OnAppStarted);
appLifetime.ApplicationStopped.Register(OnAppStopped);
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnAppStarted()
{
_runtimeInfo.IsExiting = false;
if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER)
&& _configFileProvider.LaunchBrowser)
{
_browserService.LaunchWebUI();
}
_eventAggregator.PublishEvent(new ApplicationStartedEvent());
}
private void OnAppStopped()
{
if (_runtimeInfo.RestartPending)
{
var restartArgs = GetRestartArgs();
_logger.Info("Attempting restart with arguments: {0}", restartArgs);
_processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, restartArgs);
}
}
private void Shutdown()
{
_logger.Info("Attempting to stop application.");
_logger.Info("Application has finished stop routine.");
_runtimeInfo.IsExiting = true;
_appLifetime.StopApplication();
}
private string GetRestartArgs()
{
var args = _startupContext.PreservedArguments;
args += " /restart";
if (!args.Contains("/nobrowser"))
{
args += " /nobrowser";
}
return args;
}
public void Handle(ApplicationShutdownRequested message)
{
if (!_runtimeInfo.IsWindowsService)
{
if (message.Restarting)
{
_runtimeInfo.RestartPending = true;
}
LogManager.Configuration = null;
Shutdown();
}
}
}
}

@ -1,136 +0,0 @@
using System;
using System.ServiceProcess;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Host
{
public interface INzbDroneServiceFactory
{
ServiceBase Build();
}
public interface INzbDroneConsoleFactory
{
void Start();
void Shutdown();
}
public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory
{
private readonly INzbDroneConsoleFactory _consoleFactory;
public NzbDroneServiceFactory(INzbDroneConsoleFactory consoleFactory)
{
_consoleFactory = consoleFactory;
}
protected override void OnStart(string[] args)
{
_consoleFactory.Start();
}
protected override void OnStop()
{
_consoleFactory.Shutdown();
}
public ServiceBase Build()
{
return this;
}
}
public class DummyNzbDroneServiceFactory : INzbDroneServiceFactory
{
public ServiceBase Build()
{
return null;
}
}
public class NzbDroneConsoleFactory : INzbDroneConsoleFactory, IHandle<ApplicationShutdownRequested>
{
private readonly IConfigFileProvider _configFileProvider;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IHostController _hostController;
private readonly IStartupContext _startupContext;
private readonly IBrowserService _browserService;
private readonly IContainer _container;
private readonly Logger _logger;
// private CancelHandler _cancelHandler;
public NzbDroneConsoleFactory(IConfigFileProvider configFileProvider,
IHostController hostController,
IRuntimeInfo runtimeInfo,
IStartupContext startupContext,
IBrowserService browserService,
IContainer container,
Logger logger)
{
_configFileProvider = configFileProvider;
_hostController = hostController;
_runtimeInfo = runtimeInfo;
_startupContext = startupContext;
_browserService = browserService;
_container = container;
_logger = logger;
}
public void Start()
{
if (OsInfo.IsNotWindows)
{
//Console.CancelKeyPress += (sender, eventArgs) => eventArgs.Cancel = true;
//_cancelHandler = new CancelHandler();
}
_runtimeInfo.IsExiting = false;
DbFactory.RegisterDatabase(_container);
_container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartingEvent());
if (_runtimeInfo.IsExiting)
{
return;
}
_hostController.StartServer();
if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER)
&& _configFileProvider.LaunchBrowser)
{
_browserService.LaunchWebUI();
}
_container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
}
public void Shutdown()
{
_logger.Info("Attempting to stop application.");
_hostController.StopServer();
_logger.Info("Application has finished stop routine.");
_runtimeInfo.IsExiting = true;
}
public void Handle(ApplicationShutdownRequested message)
{
if (!_runtimeInfo.IsWindowsService)
{
if (message.Restarting)
{
_runtimeInfo.RestartPending = true;
}
LogManager.Configuration = null;
Shutdown();
}
}
}
}

@ -1,115 +1,160 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Host;
namespace NzbDrone.Host namespace NzbDrone.Host
{ {
public static class Bootstrap public static class Bootstrap
{ {
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Bootstrap)); private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Bootstrap));
private static IContainer _container;
public static void Start(StartupContext startupContext, IUserAlert userAlert, Action<IContainer> startCallback = null) public static readonly List<string> ASSEMBLIES = new List<string>
{
"Sonarr.Host",
"Sonarr.Core",
"Sonarr.SignalR",
"Sonarr.Api.V3",
"Sonarr.Http"
};
public static void Start(string[] args, Action<IHostBuilder> trayCallback = null)
{ {
try try
{ {
Logger.Info("Starting Sonarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version); Logger.Info("Starting Sonarr - {0} - Version {1}",
Process.GetCurrentProcess().MainModule.FileName,
Assembly.GetExecutingAssembly().GetName().Version);
var startupContext = new StartupContext(args);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var appMode = GetApplicationMode(startupContext);
if (!PlatformValidation.IsValidate(userAlert)) switch (appMode)
{ {
throw new TerminateApplicationException("Missing system requirements"); case ApplicationModes.Service:
} {
Logger.Debug("Service selected");
LongPathSupport.Enable(); CreateConsoleHostBuilder(args, startupContext).UseWindowsService().Build().Run();
break;
}
_container = MainAppContainerBuilder.BuildContainer(startupContext); case ApplicationModes.Interactive:
_container.Resolve<InitializeLogger>().Initialize(); {
_container.Resolve<IAppFolderFactory>().Register(); Logger.Debug(trayCallback != null ? "Tray selected" : "Console selected");
_container.Resolve<IProvidePidFile>().Write(); var builder = CreateConsoleHostBuilder(args, startupContext);
var appMode = GetApplicationMode(startupContext); if (trayCallback != null)
{
trayCallback(builder);
}
Start(appMode, startupContext); builder.Build().Run();
break;
}
if (startCallback != null) // Utility mode
{ default:
startCallback(_container); {
} new Container(rules => rules.WithNzbDroneRules())
else .AutoAddServices(ASSEMBLIES)
{ .AddNzbDroneLogger()
SpinToExit(appMode); .AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(appMode);
break;
}
} }
} }
catch (InvalidConfigFileException ex) catch (InvalidConfigFileException ex)
{ {
throw new SonarrStartupException(ex); throw new SonarrStartupException(ex);
} }
catch (TerminateApplicationException ex) catch (TerminateApplicationException e)
{ {
Logger.Info(ex.Message); Logger.Info(e.Message);
LogManager.Configuration = null; LogManager.Configuration = null;
} }
} }
private static void Start(ApplicationModes applicationModes, StartupContext startupContext) public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContext context)
{ {
_container.Resolve<ReconfigureLogging>().Reconfigure(); var config = GetConfiguration(context);
if (!IsInUtilityMode(applicationModes)) var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*");
{ var port = config.GetValue(nameof(ConfigFileProvider.Port), 8989);
if (startupContext.Flags.Contains(StartupContext.RESTART)) var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), 9898);
{ var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
Thread.Sleep(2000); var sslCertPath = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
} var sslCertPassword = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
EnsureSingleInstance(applicationModes == ApplicationModes.Service, startupContext); var urls = new List<string> { BuildUrl("http", bindAddress, port) };
}
_container.Resolve<Router>().Route(applicationModes);
}
private static void SpinToExit(ApplicationModes applicationModes) if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
if (IsInUtilityMode(applicationModes))
{ {
return; urls.Add(BuildUrl("https", bindAddress, sslPort));
} }
_container.Resolve<IWaitForExit>().Spin(); return new HostBuilder()
} .UseContentRoot(Directory.GetCurrentDirectory())
.UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules())))
private static void EnsureSingleInstance(bool isService, IStartupContext startupContext) .ConfigureContainer<IContainer>(c =>
{ {
var instancePolicy = _container.Resolve<ISingleInstancePolicy>(); c.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddNzbDroneLogger()
if (startupContext.Flags.Contains(StartupContext.TERMINATE)) .AddDatabase()
{ .AddStartupContext(context);
instancePolicy.KillAllOtherInstance(); })
} .ConfigureWebHost(builder =>
else if (startupContext.Args.ContainsKey(StartupContext.APPDATA)) {
{ builder.UseConfiguration(config);
instancePolicy.WarnIfAlreadyRunning(); builder.UseUrls(urls.ToArray());
} builder.UseKestrel(options =>
else if (isService) {
{ if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
instancePolicy.KillAllOtherInstance(); {
} options.ConfigureHttpsDefaults(configureOptions =>
else {
{ configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslCertPassword);
instancePolicy.PreventStartIfAlreadyRunning(); });
} }
});
builder.ConfigureKestrel(serverOptions =>
{
serverOptions.AllowSynchronousIO = true;
serverOptions.Limits.MaxRequestBodySize = null;
});
builder.UseStartup<Startup>();
});
} }
private static ApplicationModes GetApplicationMode(IStartupContext startupContext) public static ApplicationModes GetApplicationMode(IStartupContext startupContext)
{ {
if (startupContext.Help) if (startupContext.Help)
{ {
@ -131,7 +176,7 @@ namespace NzbDrone.Host
return ApplicationModes.UninstallService; return ApplicationModes.UninstallService;
} }
if (_container.Resolve<IRuntimeInfo>().IsWindowsService) if (OsInfo.IsWindows && WindowsServiceHelpers.IsWindowsService())
{ {
return ApplicationModes.Service; return ApplicationModes.Service;
} }
@ -139,23 +184,40 @@ namespace NzbDrone.Host
return ApplicationModes.Interactive; return ApplicationModes.Interactive;
} }
private static bool IsInUtilityMode(ApplicationModes applicationMode) private static IConfiguration GetConfiguration(StartupContext context)
{
var appFolder = new AppFolderInfo(context);
return new ConfigurationBuilder()
.AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false)
.AddInMemoryCollection(new List<KeyValuePair<string, string>> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) })
.Build();
}
private static string BuildUrl(string scheme, string bindAddress, int port)
{
return $"{scheme}://{bindAddress}:{port}";
}
private static X509Certificate2 ValidateSslCertificate(string cert, string password)
{ {
switch (applicationMode) X509Certificate2 certificate;
try
{ {
case ApplicationModes.InstallService: certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet);
case ApplicationModes.UninstallService: }
case ApplicationModes.RegisterUrl: catch (CryptographicException ex)
case ApplicationModes.Help: {
{ if (ex.HResult == 0x2 || ex.HResult == 0x2006D080)
return true; {
} throw new SonarrStartupException(ex,
$"The SSL certificate file {cert} does not exist");
}
default: throw new SonarrStartupException(ex);
{
return false;
}
} }
return certificate;
} }
} }
} }

@ -1,8 +0,0 @@
namespace NzbDrone.Host
{
public interface IHostController
{
void StartServer();
void StopServer();
}
}

@ -1,7 +0,0 @@
namespace NzbDrone.Host.AccessControl
{
public interface IRemoteAccessAdapter
{
void MakeAccessible(bool passive);
}
}

@ -1,7 +0,0 @@
namespace NzbDrone.Host
{
public interface IUserAlert
{
void Alert(string message);
}
}

@ -1,43 +0,0 @@
using System.Collections.Generic;
using Nancy.Bootstrapper;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.SignalR;
using Sonarr.Http;
namespace NzbDrone.Host
{
public class MainAppContainerBuilder : ContainerBuilderBase
{
public static IContainer BuildContainer(StartupContext args)
{
var assemblies = new List<string>
{
"Sonarr.Host",
"Sonarr.Core",
"Sonarr.SignalR",
"Sonarr.Api.V3",
"Sonarr.Http"
};
return new MainAppContainerBuilder(args, assemblies).Container;
}
private MainAppContainerBuilder(StartupContext args, List<string> assemblies)
: base(args, assemblies)
{
AutoRegisterImplementations<MessageHub>();
Container.Register<INancyBootstrapper, SonarrBootstrapper>();
if (OsInfo.IsWindows)
{
Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>();
}
else
{
Container.Register<INzbDroneServiceFactory, DummyNzbDroneServiceFactory>();
}
}
}
}

@ -1,55 +0,0 @@
using System;
using System.Diagnostics;
using System.Reflection;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Host
{
public static class PlatformValidation
{
private const string DOWNLOAD_LINK = "http://www.microsoft.com/en-us/download/details.aspx?id=42643";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(PlatformValidation));
public static bool IsValidate(IUserAlert userAlert)
{
if (OsInfo.IsNotWindows)
{
return true;
}
if (!IsAssemblyAvailable("System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"))
{
userAlert.Alert("It looks like you don't have the correct version of .NET Framework installed. You will now be directed the download page.");
try
{
Process.Start(DOWNLOAD_LINK);
}
catch (Exception)
{
userAlert.Alert("Oops. Couldn't start your browser. Please visit http://www.microsoft.com/net to download the latest version of .NET Framework");
}
return false;
}
return true;
}
private static bool IsAssemblyAvailable(string assemblyString)
{
try
{
Assembly.Load(assemblyString);
return true;
}
catch (Exception e)
{
Logger.Warn(e, "Couldn't load {0}", assemblyString);
return false;
}
}
}
}

@ -7,6 +7,9 @@
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.5" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="DryIoc.dll" Version="4.8.6" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" />

@ -1,87 +0,0 @@
using System.IO;
using System.Threading;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
namespace NzbDrone.Host
{
public interface IWaitForExit
{
void Spin();
}
public class SpinService : IWaitForExit
{
private readonly IRuntimeInfo _runtimeInfo;
private readonly IProcessProvider _processProvider;
private readonly IDiskProvider _diskProvider;
private readonly IStartupContext _startupContext;
private readonly Logger _logger;
public SpinService(IRuntimeInfo runtimeInfo, IProcessProvider processProvider, IDiskProvider diskProvider, IStartupContext startupContext, Logger logger)
{
_runtimeInfo = runtimeInfo;
_processProvider = processProvider;
_diskProvider = diskProvider;
_startupContext = startupContext;
_logger = logger;
}
public void Spin()
{
while (!_runtimeInfo.IsExiting)
{
Thread.Sleep(1000);
}
_logger.Debug("Wait loop was terminated.");
if (_runtimeInfo.RestartPending)
{
var restartArgs = GetRestartArgs();
var path = _runtimeInfo.ExecutingApplication;
var installationFolder = Path.GetDirectoryName(path);
_logger.Info("Attempting restart with arguments: {0} {1}", path, restartArgs);
if (OsInfo.IsOsx)
{
if (installationFolder.EndsWith(".app/Contents/MacOS/bin"))
{
// New MacOS App stores Sonarr binaries in MacOS/bin and has a shim in MacOS
// Run the app bundle instead of the binary
path = Path.GetDirectoryName(installationFolder);
path = Path.GetDirectoryName(path);
path = Path.GetDirectoryName(path);
}
else if (installationFolder.EndsWith(".app/Contents/MacOS"))
{
// Old MacOS App stores Sonarr binaries in MacOS together with shell script
// Run the app bundle instead
path = Path.GetDirectoryName(installationFolder);
path = Path.GetDirectoryName(path);
}
}
_processProvider.SpawnNewProcess(path, restartArgs);
}
}
private string GetRestartArgs()
{
var args = _startupContext.PreservedArguments;
args += " /restart";
if (!args.Contains("/nobrowser"))
{
args += " /nobrowser";
}
return args;
}
}
}

@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Host;
using NzbDrone.Host.AccessControl;
using NzbDrone.Http.Authentication;
using NzbDrone.SignalR;
using Sonarr.Api.V3.System;
using Sonarr.Http;
using Sonarr.Http.Authentication;
using Sonarr.Http.ErrorManagement;
using Sonarr.Http.Frontend;
using Sonarr.Http.Middleware;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(b =>
{
b.ClearProviders();
b.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
b.AddFilter("Microsoft.AspNetCore", Microsoft.Extensions.Logging.LogLevel.Warning);
b.AddFilter("Sonarr.Http.Authentication", LogLevel.Information);
b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error);
b.AddNLog();
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
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);
})
.AddControllersAsServices();
services
.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
services.AddAuthorization(options =>
{
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();
}
public void Configure(IApplicationBuilder app,
IStartupContext startupContext,
Lazy<IMainDatabase> mainDatabaseFactory,
Lazy<ILogDatabase> logDatabaseFactory,
DatabaseTarget dbTarget,
ISingleInstancePolicy singleInstancePolicy,
InitializeLogger initializeLogger,
ReconfigureLogging reconfigureLogging,
IAppFolderFactory appFolderFactory,
IProvidePidFile pidFileProvider,
IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo,
IFirewallAdapter firewallAdapter,
SonarrErrorPipeline errorHandler)
{
initializeLogger.Initialize();
appFolderFactory.Register();
pidFileProvider.Write();
reconfigureLogging.Reconfigure();
EnsureSingleInstance(false, startupContext, singleInstancePolicy);
// instantiate the databases to initialize/migrate them
_ = mainDatabaseFactory.Value;
_ = logDatabaseFactory.Value;
dbTarget.Register();
if (OsInfo.IsNotWindows)
{
Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null;
}
if (OsInfo.IsWindows && runtimeInfo.IsAdmin)
{
firewallAdapter.MakeAccessible();
}
app.UseForwardedHeaders();
app.UseMiddleware<LoggingMiddleware>();
app.UsePathBase(new PathString(configFileProvider.UrlBase));
app.UseExceptionHandler(new ExceptionHandlerOptions
{
AllowStatusCode404Response = true,
ExceptionHandler = errorHandler.HandleException
});
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.Properties["host.AppName"] = BuildInfo.AppName;
app.UseMiddleware<VersionMiddleware>();
app.UseMiddleware<UrlBaseMiddleware>(configFileProvider.UrlBase);
app.UseMiddleware<CacheHeaderMiddleware>();
app.UseMiddleware<IfModifiedMiddleware>();
app.UseMiddleware<BufferingMiddleware>(new List<string> { "/api/v3/command" });
app.UseWebSockets();
app.UseEndpoints(x =>
{
x.MapHub<MessageHub>("/signalr/messages").RequireAuthorization("SignalR");
x.MapControllers();
});
}
private void EnsureSingleInstance(bool isService, IStartupContext startupContext, ISingleInstancePolicy instancePolicy)
{
if (startupContext.Flags.Contains(StartupContext.NO_SINGLE_INSTANCE_CHECK))
{
return;
}
if (startupContext.Flags.Contains(StartupContext.TERMINATE))
{
instancePolicy.KillAllOtherInstance();
}
else if (startupContext.Args.ContainsKey(StartupContext.APPDATA))
{
instancePolicy.WarnIfAlreadyRunning();
}
else if (isService)
{
instancePolicy.KillAllOtherInstance();
}
else
{
instancePolicy.PreventStartIfAlreadyRunning();
}
}
}
}

@ -7,10 +7,13 @@ using IServiceProvider = NzbDrone.Common.IServiceProvider;
namespace NzbDrone.Host namespace NzbDrone.Host
{ {
public class Router public interface IUtilityModeRouter
{
void Route(ApplicationModes applicationModes);
}
public class UtilityModeRouter : IUtilityModeRouter
{ {
private readonly INzbDroneConsoleFactory _nzbDroneConsoleFactory;
private readonly INzbDroneServiceFactory _nzbDroneServiceFactory;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IConsoleService _consoleService; private readonly IConsoleService _consoleService;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
@ -18,17 +21,13 @@ namespace NzbDrone.Host
private readonly IRemoteAccessAdapter _remoteAccessAdapter; private readonly IRemoteAccessAdapter _remoteAccessAdapter;
private readonly Logger _logger; private readonly Logger _logger;
public Router(INzbDroneConsoleFactory nzbDroneConsoleFactory, public UtilityModeRouter(IServiceProvider serviceProvider,
INzbDroneServiceFactory nzbDroneServiceFactory,
IServiceProvider serviceProvider,
IConsoleService consoleService, IConsoleService consoleService,
IRuntimeInfo runtimeInfo, IRuntimeInfo runtimeInfo,
IProcessProvider processProvider, IProcessProvider processProvider,
IRemoteAccessAdapter remoteAccessAdapter, IRemoteAccessAdapter remoteAccessAdapter,
Logger logger) Logger logger)
{ {
_nzbDroneConsoleFactory = nzbDroneConsoleFactory;
_nzbDroneServiceFactory = nzbDroneServiceFactory;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_consoleService = consoleService; _consoleService = consoleService;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
@ -43,22 +42,6 @@ namespace NzbDrone.Host
switch (applicationModes) switch (applicationModes)
{ {
case ApplicationModes.Service:
{
_logger.Debug("Service selected");
_serviceProvider.Run(_nzbDroneServiceFactory.Build());
break;
}
case ApplicationModes.Interactive:
{
_logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected");
_nzbDroneConsoleFactory.Start();
break;
}
case ApplicationModes.InstallService: case ApplicationModes.InstallService:
{ {
_logger.Debug("Install Service selected"); _logger.Debug("Install Service selected");

@ -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,73 +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"))
{
if (!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);
}
}
}

@ -1,157 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Host.AccessControl;
using NzbDrone.Host.Middleware;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
{
public class WebHostController : IHostController
{
private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigFileProvider _configFileProvider;
private readonly IFirewallAdapter _firewallAdapter;
private readonly IEnumerable<IAspNetCoreMiddleware> _middlewares;
private readonly Logger _logger;
private IWebHost _host;
public WebHostController(IRuntimeInfo runtimeInfo,
IConfigFileProvider configFileProvider,
IFirewallAdapter firewallAdapter,
IEnumerable<IAspNetCoreMiddleware> middlewares,
Logger logger)
{
_runtimeInfo = runtimeInfo;
_configFileProvider = configFileProvider;
_firewallAdapter = firewallAdapter;
_middlewares = middlewares;
_logger = logger;
}
public void StartServer()
{
if (OsInfo.IsWindows)
{
if (_runtimeInfo.IsAdmin)
{
_firewallAdapter.MakeAccessible();
}
}
var bindAddress = _configFileProvider.BindAddress;
var enableSsl = _configFileProvider.EnableSsl;
var sslCertPath = _configFileProvider.SslCertPath;
var urls = new List<string>();
urls.Add(BuildUrl("http", bindAddress, _configFileProvider.Port));
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
urls.Add(BuildUrl("https", bindAddress, _configFileProvider.SslPort));
}
_host = new WebHostBuilder()
.UseUrls(urls.ToArray())
.UseKestrel(options =>
{
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
options.ConfigureHttpsDefaults(configureOptions =>
{
X509Certificate2 certificate;
try
{
certificate = new X509Certificate2(sslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet);
}
catch (CryptographicException ex)
{
if (ex.HResult == 0x2 || ex.HResult == 0x2006D080)
{
throw new SonarrStartupException(ex, $"The SSL certificate file {sslCertPath} does not exist");
}
throw new SonarrStartupException(ex);
}
configureOptions.ServerCertificate = certificate;
});
}
})
.ConfigureKestrel(serverOptions =>
{
serverOptions.AllowSynchronousIO = true;
serverOptions.Limits.MaxRequestBodySize = null;
})
.ConfigureLogging(logging =>
{
logging.AddProvider(new NLogLoggerProvider());
logging.SetMinimumLevel(LogLevel.Warning);
})
.ConfigureServices(services =>
{
services
.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
})
.Configure(app =>
{
app.UseRouting();
app.Properties["host.AppName"] = BuildInfo.AppName;
app.UsePathBase(_configFileProvider.UrlBase);
foreach (var middleWare in _middlewares.OrderBy(c => c.Order))
{
_logger.Debug("Attaching {0} to host", middleWare.GetType().Name);
middleWare.Attach(app);
}
})
.UseContentRoot(Directory.GetCurrentDirectory())
.Build();
_logger.Info("Listening on the following URLs:");
foreach (var url in urls)
{
_logger.Info(" {0}", url);
}
_host.Start();
}
public async void StopServer()
{
_logger.Info("Attempting to stop OWIN host");
await _host.StopAsync(TimeSpan.FromSeconds(5));
_host.Dispose();
_host = null;
_logger.Info("Host has stopped");
}
private string BuildUrl(string scheme, string bindAddress, int port)
{
return $"{scheme}://{bindAddress}:{port}";
}
}
}

@ -5,6 +5,7 @@ using NzbDrone.Integration.Test.Client;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests
{ {
[TestFixture] [TestFixture]
[Ignore("Not ready to be used on this branch")]
public class CommandFixture : IntegrationTest public class CommandFixture : IntegrationTest
{ {
[Test] [Test]

@ -51,7 +51,7 @@ namespace NzbDrone.Integration.Test.Client
throw response.ErrorException; throw response.ErrorException;
} }
AssertDisableCache(response.Headers); AssertDisableCache(response);
response.ErrorMessage.Should().BeNullOrWhiteSpace(); response.ErrorMessage.Should().BeNullOrWhiteSpace();
@ -68,13 +68,14 @@ namespace NzbDrone.Integration.Test.Client
return Json.Deserialize<T>(content); return Json.Deserialize<T>(content);
} }
private static void AssertDisableCache(IList<Parameter> headers) private static void AssertDisableCache(IRestResponse response)
{ {
// cache control header gets reordered on net core // cache control header gets reordered on net core
var headers = response.Headers;
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim()) ((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 = "series") private RestRequest BuildGet(string route = "series")
{ {
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 = "series") private RestRequest BuildOptions(string route = "series")
{ {
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,5 @@
using System.Net; using System.Linq;
using System.Net;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
@ -13,5 +14,19 @@ namespace NzbDrone.Integration.Test
var text = new WebClient().DownloadString(RootUrl); var text = new WebClient().DownloadString(RootUrl);
text.Should().NotBeNullOrWhiteSpace(); text.Should().NotBeNullOrWhiteSpace();
} }
[Test]
public void index_should_not_be_cached()
{
var client = new WebClient();
_ = client.DownloadString(RootUrl);
var headers = client.ResponseHeaders;
headers.Get("Cache-Control").Split(',').Select(x => x.Trim())
.Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim()));
headers.Get("Pragma").Should().Be("no-cache");
headers.Get("Expires").Should().Be("-1");
}
} }
} }

@ -176,7 +176,7 @@ namespace NzbDrone.Integration.Test
protected async Task ConnectSignalR() protected async Task ConnectSignalR()
{ {
_signalRReceived = new List<SignalRMessage>(); _signalRReceived = new List<SignalRMessage>();
_signalrConnection = new HubConnectionBuilder().WithUrl("http://localhost:7878/signalr/messages").Build(); _signalrConnection = new HubConnectionBuilder().WithUrl("http://localhost:8989/signalr/messages").Build();
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();

@ -148,11 +148,12 @@ namespace NzbDrone.Test.Common.AutoMoq
_container = container; _container = container;
container.RegisterInstance(this); container.RegisterInstance(this);
RegisterPlatformLibrary(container);
_registeredMocks = new Dictionary<Type, object>(); _registeredMocks = new Dictionary<Type, object>();
RegisterPlatformLibrary(container);
AddTheAutoMockingContainerExtensionToTheContainer(container); AddTheAutoMockingContainerExtensionToTheContainer(container);
ContainerBuilderBase.RegisterNativeResolver(new[] { "System.Data.SQLite", "Sonarr.Core" });
AssemblyLoader.RegisterNativeResolver(new[] { "System.Data.SQLite", "Sonarr.Core" });
} }
private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityContainer container) private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityContainer container)

@ -4,6 +4,8 @@
<TargetFrameworks>net6.0</TargetFrameworks> <TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.6" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
<PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="NLog" Version="4.7.14" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using DryIoc;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Update.UpdateEngine; using NzbDrone.Update.UpdateEngine;
@ -18,8 +21,6 @@ namespace NzbDrone.Update
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(UpdateApp)); private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(UpdateApp));
private static IContainer _container;
public UpdateApp(IInstallUpdateService installUpdateService, IProcessProvider processProvider) public UpdateApp(IInstallUpdateService installUpdateService, IProcessProvider processProvider)
{ {
_installUpdateService = installUpdateService; _installUpdateService = installUpdateService;
@ -30,14 +31,18 @@ namespace NzbDrone.Update
{ {
try try
{ {
var startupContext = new StartupContext(args); var startupArgument = new StartupContext(args);
NzbDroneLogger.Register(startupContext, true, true); NzbDroneLogger.Register(startupArgument, true, true);
Logger.Info("Starting Sonarr Update Client"); Logger.Info("Starting Sonarr Update Client");
_container = UpdateContainerBuilder.Build(startupContext); var container = new Container(rules => rules.WithNzbDroneRules())
_container.Resolve<InitializeLogger>().Initialize(); .AutoAddServices(new List<string> { "Sonarr.Update" })
_container.Resolve<UpdateApp>().Start(args); .AddNzbDroneLogger()
.AddStartupContext(startupArgument);
container.Resolve<InitializeLogger>().Initialize();
container.Resolve<UpdateApp>().Start(args);
Logger.Info("Update completed successfully"); Logger.Info("Update completed successfully");
} }
@ -59,17 +64,17 @@ namespace NzbDrone.Update
{ {
if (args == null || !args.Any()) if (args == null || !args.Any())
{ {
throw new ArgumentOutOfRangeException(nameof(args), "args must be specified"); throw new ArgumentOutOfRangeException("args", "args must be specified");
} }
var startupContext = new UpdateStartupContext var startupContext = new UpdateStartupContext
{ {
ProcessId = ParseProcessId(args[0]) ProcessId = ParseProcessId(args[0])
}; };
if (OsInfo.IsNotWindows) if (OsInfo.IsNotWindows)
{ {
switch (args.Count()) switch (args.Length)
{ {
case 1: case 1:
return startupContext; return startupContext;
@ -98,7 +103,7 @@ namespace NzbDrone.Update
int id; int id;
if (!int.TryParse(arg, out id) || id <= 0) if (!int.TryParse(arg, out id) || id <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(arg), "Invalid process ID"); throw new ArgumentOutOfRangeException("arg", "Invalid process ID");
} }
Logger.Debug("NzbDrone process ID: {0}", id); Logger.Debug("NzbDrone process ID: {0}", id);

@ -1,25 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http.Dispatchers;
namespace NzbDrone.Update
{
public class UpdateContainerBuilder : ContainerBuilderBase
{
private UpdateContainerBuilder(IStartupContext startupContext, List<string> assemblies)
: base(startupContext, assemblies)
{
}
public static IContainer Build(IStartupContext startupContext)
{
var assemblies = new List<string>
{
"Sonarr.Update"
};
return new UpdateContainerBuilder(startupContext, assemblies).Container;
}
}
}

@ -1,13 +0,0 @@
using System.Windows.Forms;
using NzbDrone.Host;
namespace NzbDrone
{
public class MessageBoxUserAlert : IUserAlert
{
public void Alert(string message)
{
MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Warning, caption: "NzbDrone");
}
}
}

@ -1,6 +1,9 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Microsoft.Extensions.Hosting;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
@ -8,12 +11,7 @@ using NzbDrone.Host;
namespace NzbDrone.SysTray namespace NzbDrone.SysTray
{ {
public interface ISystemTrayApp public class SystemTrayApp : Form, IHostedService
{
void Start();
}
public class SystemTrayApp : Form, ISystemTrayApp
{ {
private readonly IBrowserService _browserService; private readonly IBrowserService _browserService;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
@ -34,8 +32,12 @@ namespace NzbDrone.SysTray
Application.ThreadException += OnThreadException; Application.ThreadException += OnThreadException;
Application.ApplicationExit += OnApplicationExit; Application.ApplicationExit += OnApplicationExit;
Application.SetHighDpiMode(HighDpiMode.PerMonitor);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
_trayMenu.Items.Add(new ToolStripMenuItem("Launch Browser", null, LaunchBrowser)); _trayMenu.Items.Add(new ToolStripMenuItem("Launch Browser", null, LaunchBrowser));
_trayMenu.Items.Add(new ToolStripMenuItem("-")); _trayMenu.Items.Add(new ToolStripSeparator());
_trayMenu.Items.Add(new ToolStripMenuItem("Exit", null, OnExit)); _trayMenu.Items.Add(new ToolStripMenuItem("Exit", null, OnExit));
_trayIcon.Text = string.Format("Sonarr - {0}", BuildInfo.Version); _trayIcon.Text = string.Format("Sonarr - {0}", BuildInfo.Version);
@ -48,6 +50,20 @@ namespace NzbDrone.SysTray
Application.Run(this); Application.Run(this);
} }
public Task StartAsync(CancellationToken cancellationToken)
{
var thread = new Thread(Start);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected override void OnClosing(CancelEventArgs e) protected override void OnClosing(CancelEventArgs e)
{ {
DisposeTrayIcon(); DisposeTrayIcon();

@ -1,5 +1,7 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
@ -20,11 +22,9 @@ namespace NzbDrone
NzbDroneLogger.Register(startupArgs, false, true); NzbDroneLogger.Register(startupArgs, false, true);
Bootstrap.Start(startupArgs, new MessageBoxUserAlert(), container => Bootstrap.Start(args, e =>
{ {
container.Register<ISystemTrayApp, SystemTrayApp>(); e.ConfigureServices((_, s) => s.AddSingleton<IHostedService, SystemTrayApp>());
var trayApp = container.Resolve<ISystemTrayApp>();
trayApp.Start();
}); });
} }
catch (Exception e) catch (Exception e)

@ -1,44 +0,0 @@
// Blacklist has been deprecated for blocklist.
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Datastore;
using Sonarr.Api.V3.Blocklist;
using Sonarr.Http;
using Sonarr.Http.Extensions;
namespace Sonarr.Api.V3.Blacklist
{
public class BlacklistModule : SonarrRestModule<BlocklistResource>
{
private readonly BlocklistService _blocklistService;
public BlacklistModule(BlocklistService blocklistService)
{
_blocklistService = blocklistService;
GetResourcePaged = Blocklist;
DeleteResource = DeleteBlockList;
Delete("/bulk", x => Remove());
}
private PagingResource<BlocklistResource> Blocklist(PagingResource<BlocklistResource> pagingResource)
{
var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending);
return ApplyToPage(_blocklistService.Paged, pagingSpec, BlocklistResourceMapper.MapToResource);
}
private void DeleteBlockList(int id)
{
_blocklistService.Delete(id);
}
private object Remove()
{
var resource = Request.Body.FromJson<BlocklistBulkResource>();
_blocklistService.Delete(resource.Ids);
return new object();
}
}
}

@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Datastore;
using Sonarr.Http;
using Sonarr.Http.Extensions;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V3.Blocklist
{
[V3ApiController]
public class BlocklistController : Controller
{
private readonly IBlocklistService _blocklistService;
public BlocklistController(IBlocklistService blocklistService)
{
_blocklistService = blocklistService;
}
[HttpGet]
public PagingResource<BlocklistResource> GetBlocklist()
{
var pagingResource = Request.ReadPagingResourceFromRequest<BlocklistResource>();
var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending);
return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model));
}
[RestDeleteById]
public void DeleteBlocklist(int id)
{
_blocklistService.Delete(id);
}
[HttpDelete("bulk")]
public object Remove([FromBody] BlocklistBulkResource resource)
{
_blocklistService.Delete(resource.Ids);
return new { };
}
}
}

@ -1,42 +0,0 @@
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Datastore;
using Sonarr.Http;
using Sonarr.Http.Extensions;
namespace Sonarr.Api.V3.Blocklist
{
public class BlocklistModule : SonarrRestModule<BlocklistResource>
{
private readonly BlocklistService _blocklistService;
public BlocklistModule(BlocklistService blocklistService)
{
_blocklistService = blocklistService;
GetResourcePaged = Blocklist;
DeleteResource = DeleteBlockList;
Delete("/bulk", x => Remove());
}
private PagingResource<BlocklistResource> Blocklist(PagingResource<BlocklistResource> pagingResource)
{
var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending);
return ApplyToPage(_blocklistService.Paged, pagingSpec, BlocklistResourceMapper.MapToResource);
}
private void DeleteBlockList(int id)
{
_blocklistService.Delete(id);
}
private object Remove()
{
var resource = Request.Body.FromJson<BlocklistBulkResource>();
_blocklistService.Delete(resource.Ids);
return new object();
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Tv;
using NzbDrone.SignalR;
using Sonarr.Api.V3.Episodes;
using Sonarr.Http;
namespace Sonarr.Api.V3.Calendar
{
[V3ApiController]
public class CalendarController : EpisodeControllerWithSignalR
{
public CalendarController(IBroadcastSignalRMessage signalR,
IEpisodeService episodeService,
ISeriesService seriesService,
IUpgradableSpecification qualityUpgradableSpecification)
: base(episodeService, seriesService, qualityUpgradableSpecification, signalR)
{
}
[HttpGet]
public List<EpisodeResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeSeries = false, bool includeEpisodeFile = false, bool includeEpisodeImages = false)
{
var startUse = start ?? DateTime.Today;
var endUse = end ?? DateTime.Today.AddDays(2);
var resources = MapToResource(_episodeService.EpisodesBetweenDates(startUse, endUse, unmonitored), includeSeries, includeEpisodeFile, includeEpisodeImages);
return resources.OrderBy(e => e.AirDateUtc).ToList();
}
}
}

@ -5,65 +5,42 @@ 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.Tags; using NzbDrone.Core.Tags;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using Sonarr.Http.Extensions; using Sonarr.Http;
namespace Sonarr.Api.V3.Calendar namespace Sonarr.Api.V3.Calendar
{ {
public class CalendarFeedModule : SonarrV3FeedModule [V3FeedController("calendar")]
public class CalendarFeedController : Controller
{ {
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly ISeriesService _seriesService;
private readonly ITagService _tagService; private readonly ITagService _tagService;
public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService) public CalendarFeedController(IEpisodeService episodeService, ISeriesService seriesService, ITagService tagService)
: base("calendar")
{ {
_episodeService = episodeService; _episodeService = episodeService;
_seriesService = seriesService;
_tagService = tagService; _tagService = tagService;
Get("/Sonarr.ics", options => GetCalendarFeed());
} }
private object GetCalendarFeed() [HttpGet("Sonarr.ics")]
public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tagList = "", bool unmonitored = false, bool premieresOnly = false, bool asAllDay = 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");
// There was a typo, recognize both the correct 'premieresOnly' and mistyped 'premiersOnly' boolean for background compat.
var premieresOnly = Request.GetBooleanQueryParameter("premieresOnly") || Request.GetBooleanQueryParameter("premiersOnly");
var asAllDay = Request.GetBooleanQueryParameter("asAllDay");
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); tags.AddRange(tagList.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
end = DateTime.Today.AddDays(futureDays);
}
if (queryTags.HasValue)
{
var tagInput = (string)queryTags.Value.ToString();
tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
} }
var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored); var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored);
var allSeries = _seriesService.GetAllSeries();
var calendar = new Ical.Net.Calendar var calendar = new Ical.Net.Calendar
{ {
ProductId = "-//sonarr.tv//Sonarr//EN" ProductId = "-//sonarr.tv//Sonarr//EN"
@ -75,12 +52,14 @@ namespace Sonarr.Api.V3.Calendar
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value)) foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))
{ {
var series = allSeries.SingleOrDefault(s => s.Id == episode.SeriesId);
if (premieresOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1)) if (premieresOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1))
{ {
continue; continue;
} }
if (tags.Any() && tags.None(episode.Series.Tags.Contains)) if (tags.Any() && tags.None(series.Tags.Contains))
{ {
continue; continue;
} }
@ -89,7 +68,7 @@ namespace Sonarr.Api.V3.Calendar
occurrence.Uid = "NzbDrone_episode_" + episode.Id; occurrence.Uid = "NzbDrone_episode_" + episode.Id;
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
occurrence.Description = episode.Overview; occurrence.Description = episode.Overview;
occurrence.Categories = new List<string>() { episode.Series.Network }; occurrence.Categories = new List<string>() { series.Network };
if (asAllDay) if (asAllDay)
{ {
@ -98,16 +77,16 @@ namespace Sonarr.Api.V3.Calendar
else else
{ {
occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true }; occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true };
occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true }; occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(series.Runtime)) { HasTime = true };
} }
switch (episode.Series.SeriesType) switch (series.SeriesType)
{ {
case SeriesTypes.Daily: case SeriesTypes.Daily:
occurrence.Summary = $"{episode.Series.Title} - {episode.Title}"; occurrence.Summary = $"{series.Title} - {episode.Title}";
break; break;
default: default:
occurrence.Summary = $"{episode.Series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}"; occurrence.Summary = $"{series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}";
break; break;
} }
} }
@ -115,7 +94,7 @@ namespace Sonarr.Api.V3.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,51 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Tv;
using NzbDrone.SignalR;
using Sonarr.Api.V3.Episodes;
using Sonarr.Http.Extensions;
namespace Sonarr.Api.V3.Calendar
{
public class CalendarModule : EpisodeModuleWithSignalR
{
public CalendarModule(IEpisodeService episodeService,
ISeriesService seriesService,
IUpgradableSpecification ugradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(episodeService, seriesService, ugradableSpecification, signalRBroadcaster, "calendar")
{
GetResourceAll = GetCalendar;
}
private List<EpisodeResource> GetCalendar()
{
var start = DateTime.Today;
var end = DateTime.Today.AddDays(2);
var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored");
var includeSeries = Request.GetBooleanQueryParameter("includeSeries");
var includeEpisodeFile = Request.GetBooleanQueryParameter("includeEpisodeFile");
var includeEpisodeImages = Request.GetBooleanQueryParameter("includeEpisodeImages");
var queryStart = Request.Query.Start;
var queryEnd = Request.Query.End;
if (queryStart.HasValue)
{
start = DateTime.Parse(queryStart.Value);
}
if (queryEnd.HasValue)
{
end = DateTime.Parse(queryEnd.Value);
}
var resources = MapToResource(_episodeService.EpisodesBetweenDates(start, end, includeUnmonitored), includeSeries, includeEpisodeFile, includeEpisodeImages);
return resources.OrderBy(e => e.AirDateUtc).ToList();
}
}
}

@ -1,7 +1,11 @@
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.Composition;
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;
@ -9,63 +13,68 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging; using NzbDrone.Core.ProgressMessaging;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.Extensions; using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
using Sonarr.Http.Validation; using Sonarr.Http.Validation;
namespace Sonarr.Api.V3.Commands namespace Sonarr.Api.V3.Commands
{ {
public class CommandModule : SonarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent> [V3ApiController]
public class CommandController : RestControllerWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
{ {
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly IServiceFactory _serviceFactory; private readonly KnownTypes _knownTypes;
private readonly Debouncer _debouncer; private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates; private readonly Dictionary<int, CommandResource> _pendingUpdates;
private readonly CommandPriorityComparer _commandPriorityComparer = new CommandPriorityComparer(); private readonly CommandPriorityComparer _commandPriorityComparer = new CommandPriorityComparer();
public CommandModule(IManageCommandQueue commandQueueManager, public CommandController(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster, IBroadcastSignalRMessage signalRBroadcaster,
IServiceFactory serviceFactory) KnownTypes knownTypes)
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
_serviceFactory = serviceFactory; _knownTypes = knownTypes;
_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>();
GetResourceById = GetCommand;
CreateResource = StartCommand;
GetResourceAll = GetStartedCommands;
DeleteResource = CancelCommand;
PostValidator.RuleFor(c => c.Name).NotBlank(); PostValidator.RuleFor(c => c.Name).NotBlank();
} }
private CommandResource GetCommand(int id) protected 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)) _knownTypes.GetImplementations(typeof(Command))
.Single(c => c.Name.Replace("Command", "") .Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
dynamic command = Request.Body.FromJson(commandType); Request.Body.Seek(0, SeekOrigin.Begin);
command.Trigger = CommandTrigger.Manual; using (var reader = new StreamReader(Request.Body))
command.SuppressMessages = !command.SendUpdatesToClient; {
command.SendUpdatesToClient = true; var body = reader.ReadToEnd();
dynamic command = STJson.Deserialize(body, commandType);
command.ClientUserAgent = Request.Headers.UserAgent; command.Trigger = CommandTrigger.Manual;
command.SuppressMessages = !command.SendUpdatesToClient;
command.SendUpdatesToClient = true;
command.ClientUserAgent = Request.Headers["UserAgent"];
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
return trackedCommand.Id; return Created(trackedCommand.Id);
}
} }
private List<CommandResource> GetStartedCommands() [HttpGet]
public List<CommandResource> GetStartedCommands()
{ {
return _commandQueueManager.All() return _commandQueueManager.All()
.OrderBy(c => c.Status, _commandPriorityComparer) .OrderBy(c => c.Status, _commandPriorityComparer)
@ -73,11 +82,13 @@ namespace Sonarr.Api.V3.Commands
.ToResource(); .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 Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V3.Config
{
public abstract class ConfigController<TResource> : RestController<TResource>
where TResource : RestResource, new()
{
private readonly IConfigService _configService;
protected ConfigController(IConfigService configService)
{
_configService = configService;
}
protected 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 Sonarr.Http;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
{ {
public class DownloadClientConfigModule : SonarrConfigModule<DownloadClientConfigResource> [V3ApiController("config/downloadclient")]
public class DownloadClientConfigController : ConfigController<DownloadClientConfigResource>
{ {
public DownloadClientConfigModule(IConfigService configService) public DownloadClientConfigController(IConfigService configService)
: base(configService) : base(configService)
{ {
} }

@ -3,6 +3,7 @@ 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;
@ -10,29 +11,27 @@ using NzbDrone.Core.Update;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
{ {
public class HostConfigModule : SonarrRestModule<HostConfigResource> [V3ApiController("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()
@ -80,7 +79,13 @@ namespace Sonarr.Api.V3.Config
return cert != null; return cert != null;
} }
private HostConfigResource GetHostConfig() protected 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;
@ -95,12 +100,8 @@ namespace Sonarr.Api.V3.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)
@ -113,6 +114,8 @@ namespace Sonarr.Api.V3.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 Sonarr.Http;
using Sonarr.Http.Validation; using Sonarr.Http.Validation;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
{ {
public class IndexerConfigModule : SonarrConfigModule<IndexerConfigResource> [V3ApiController("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)

@ -0,0 +1,45 @@
using FluentValidation;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using Sonarr.Http;
namespace Sonarr.Api.V3.Config
{
[V3ApiController("config/mediamanagement")]
public class MediaManagementConfigController : ConfigController<MediaManagementConfigResource>
{
public MediaManagementConfigController(IConfigService configService,
PathExistsValidator pathExistsValidator,
FolderChmodValidator folderChmodValidator,
FolderWritableValidator folderWritableValidator,
SeriesPathValidator seriesPathValidator,
StartupFolderValidator startupFolderValidator,
SystemFolderValidator systemFolderValidator,
RootFolderAncestorValidator rootFolderAncestorValidator,
RootFolderValidator rootFolderValidator)
: base(configService)
{
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx));
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath()
.SetValidator(folderWritableValidator)
.SetValidator(rootFolderValidator)
.SetValidator(pathExistsValidator)
.SetValidator(rootFolderAncestorValidator)
.SetValidator(startupFolderValidator)
.SetValidator(systemFolderValidator)
.SetValidator(seriesPathValidator)
.When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
}
protected override MediaManagementConfigResource ToResource(IConfigService model)
{
return MediaManagementConfigResourceMapper.ToResource(model);
}
}
}

@ -1,25 +0,0 @@
using FluentValidation;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
namespace Sonarr.Api.V3.Config
{
public class MediaManagementConfigModule : SonarrConfigModule<MediaManagementConfigResource>
{
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
: base(configService)
{
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && OsInfo.IsNotWindows);
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
}
protected override MediaManagementConfigResource ToResource(IConfigService model)
{
return MediaManagementConfigResourceMapper.ToResource(model);
}
}
}

@ -1,36 +1,33 @@
using System.Collections.Generic; 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 Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
{ {
public class NamingConfigModule : SonarrRestModule<NamingConfigResource> [V3ApiController("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.MultiEpisodeStyle).InclusiveBetween(0, 5); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5);
SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat();
@ -41,15 +38,13 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat(); SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat();
} }
private void UpdateNamingConfig(NamingConfigResource resource) protected 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();
@ -63,12 +58,19 @@ namespace Sonarr.Api.V3.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 Sonarr.Http;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Config
{
public abstract class SonarrConfigModule<TResource> : SonarrRestModule<TResource>
where TResource : RestResource, new()
{
private readonly IConfigService _configService;
protected SonarrConfigModule(IConfigService configService)
: this(new TResource().ResourceName.Replace("config", ""), configService)
{
}
protected SonarrConfigModule(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 Sonarr.Http;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
{ {
public class UiConfigModule : SonarrConfigModule<UiConfigResource> [V3ApiController("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 Sonarr.Http;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V3.CustomFilters
{
[V3ApiController]
public class CustomFilterController : RestController<CustomFilterResource>
{
private readonly ICustomFilterService _customFilterService;
public CustomFilterController(ICustomFilterService customFilterService)
{
_customFilterService = customFilterService;
}
protected 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);
}
}
}

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

Loading…
Cancel
Save