diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index 5a767365d..a49020351 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -1,8 +1,8 @@ using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.ClientSchema; using NzbDrone.Core.Annotations; using NzbDrone.Test.Common; +using Sonarr.Http.ClientSchema; namespace NzbDrone.Api.Test.ClientSchemaTests { diff --git a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj index 9d787542e..bca5c6d2d 100644 --- a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj +++ b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj @@ -90,6 +90,10 @@ {CADDFCE0-7509-4430-8364-2074E1EEFCA2} NzbDrone.Test.Common + + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Sonarr.Http + diff --git a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs index 1687b31e3..29002875b 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs @@ -1,9 +1,10 @@ using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Datastore; +using Sonarr.Http; namespace NzbDrone.Api.Blacklist { - public class BlacklistModule : NzbDroneRestModule + public class BlacklistModule : SonarrRestModule { private readonly IBlacklistService _blacklistService; diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs index c3f1c6b1b..e7bba03a3 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; using NzbDrone.Core.Indexers; diff --git a/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs b/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs deleted file mode 100644 index 4e796bd8c..000000000 --- a/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace NzbDrone.Api.ClientSchema -{ - -} \ No newline at end of file diff --git a/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs b/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs deleted file mode 100644 index 6af07257f..000000000 --- a/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Api.ClientSchema -{ - public static class SchemaDeserializer - { - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Commands/CommandModule.cs b/src/NzbDrone.Api/Commands/CommandModule.cs index 0d085eb3e..bfd6ddf68 100644 --- a/src/NzbDrone.Api/Commands/CommandModule.cs +++ b/src/NzbDrone.Api/Commands/CommandModule.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Validation; +using Sonarr.Http.Extensions; using NzbDrone.Common; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; @@ -10,11 +9,14 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ProgressMessaging; using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Mapping; +using Sonarr.Http.Validation; namespace NzbDrone.Api.Commands { - public class CommandModule : NzbDroneRestModuleWithSignalR, IHandle + public class CommandModule : SonarrRestModuleWithSignalR, IHandle { private readonly IManageCommandQueue _commandQueueManager; private readonly IServiceFactory _serviceFactory; diff --git a/src/NzbDrone.Api/Commands/CommandResource.cs b/src/NzbDrone.Api/Commands/CommandResource.cs index cf09f12ac..4180851f4 100644 --- a/src/NzbDrone.Api/Commands/CommandResource.cs +++ b/src/NzbDrone.Api/Commands/CommandResource.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Api.Commands diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs index 8309c9f4d..0c4b00173 100644 --- a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs +++ b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index 367bf770d..d661a5ecf 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -8,10 +8,11 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; namespace NzbDrone.Api.Config { - public class HostConfigModule : NzbDroneRestModule + public class HostConfigModule : SonarrRestModule { private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; diff --git a/src/NzbDrone.Api/Config/HostConfigResource.cs b/src/NzbDrone.Api/Config/HostConfigResource.cs index 930e0301c..90d51ab19 100644 --- a/src/NzbDrone.Api/Config/HostConfigResource.cs +++ b/src/NzbDrone.Api/Config/HostConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; diff --git a/src/NzbDrone.Api/Config/IndexerConfigModule.cs b/src/NzbDrone.Api/Config/IndexerConfigModule.cs index ebb8f7cd8..f8c034754 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigModule.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigModule.cs @@ -1,6 +1,6 @@ using FluentValidation; -using NzbDrone.Api.Validation; using NzbDrone.Core.Configuration; +using Sonarr.Http.Validation; namespace NzbDrone.Api.Config { diff --git a/src/NzbDrone.Api/Config/IndexerConfigResource.cs b/src/NzbDrone.Api/Config/IndexerConfigResource.cs index 59fcb48e3..f70c03e68 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigResource.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs index b10d8209f..0593f33b3 100644 --- a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs +++ b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index 0b72e0b0c..f7dc2bf19 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -6,11 +6,13 @@ using Nancy.Responses; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; using Nancy.ModelBinding; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Config { - public class NamingConfigModule : NzbDroneRestModule + public class NamingConfigModule : SonarrRestModule { private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index 39147b993..cfc6d507a 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Organizer; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs index e5d324950..50e1d82e3 100644 --- a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs +++ b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs @@ -1,11 +1,12 @@ using System.Linq; using System.Reflection; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Configuration; +using Sonarr.Http; namespace NzbDrone.Api.Config { - public abstract class NzbDroneConfigModule : NzbDroneRestModule where TResource : RestResource, new() + public abstract class NzbDroneConfigModule : SonarrRestModule where TResource : RestResource, new() { private readonly IConfigService _configService; diff --git a/src/NzbDrone.Api/Config/UiConfigResource.cs b/src/NzbDrone.Api/Config/UiConfigResource.cs index 7c7d27b67..accda475e 100644 --- a/src/NzbDrone.Api/Config/UiConfigResource.cs +++ b/src/NzbDrone.Api/Config/UiConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs b/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs index f6d8354b4..d19c33f03 100644 --- a/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs +++ b/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using NzbDrone.Core.DiskSpace; +using Sonarr.Http; namespace NzbDrone.Api.DiskSpace { - public class DiskSpaceModule :NzbDroneRestModule + public class DiskSpaceModule :SonarrRestModule { private readonly IDiskSpaceService _diskSpaceService; diff --git a/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs b/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs index fc36f9d5c..068bf7ad5 100644 --- a/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs +++ b/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Sonarr.Http.REST; namespace NzbDrone.Api.DiskSpace { diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs index b061ef343..9919e8907 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Sonarr.Http.REST; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; @@ -11,7 +12,7 @@ using HttpStatusCode = System.Net.HttpStatusCode; namespace NzbDrone.Api.EpisodeFiles { - public class EpisodeFileModule : NzbDroneRestModuleWithSignalR, + public class EpisodeFileModule : SonarrRestModuleWithSignalR, IHandle { private readonly IMediaFileService _mediaFileService; diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs index ed85d7119..9ce4d9938 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs @@ -1,6 +1,8 @@ using System; using System.IO; -using NzbDrone.Api.REST; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles; +using Sonarr.Http.REST; using NzbDrone.Core.Qualities; namespace NzbDrone.Api.EpisodeFiles @@ -23,7 +25,7 @@ namespace NzbDrone.Api.EpisodeFiles public static class EpisodeFileResourceMapper { - private static EpisodeFileResource ToResource(this Core.MediaFiles.EpisodeFile model) + private static EpisodeFileResource ToResource(this EpisodeFile model) { if (model == null) return null; @@ -44,7 +46,7 @@ namespace NzbDrone.Api.EpisodeFiles }; } - public static EpisodeFileResource ToResource(this Core.MediaFiles.EpisodeFile model, Core.Tv.Series series, Core.DecisionEngine.IQualityUpgradableSpecification qualityUpgradableSpecification) + public static EpisodeFileResource ToResource(this EpisodeFile model, Core.Tv.Series series, IQualityUpgradableSpecification qualityUpgradableSpecification) { if (model == null) return null; @@ -60,7 +62,7 @@ namespace NzbDrone.Api.EpisodeFiles DateAdded = model.DateAdded, SceneName = model.SceneName, Quality = model.Quality, - QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality), + QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.Profile.Value, model.Quality), MediaInfo = model.MediaInfo.ToResource(model.SceneName), OriginalFilePath = model.OriginalFilePath }; diff --git a/src/NzbDrone.Api/EpisodeFiles/MediaInfoResource.cs b/src/NzbDrone.Api/EpisodeFiles/MediaInfoResource.cs index 672c48fcd..f34cafd2f 100644 --- a/src/NzbDrone.Api/EpisodeFiles/MediaInfoResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/MediaInfoResource.cs @@ -1,5 +1,5 @@ -using NzbDrone.Api.REST; using NzbDrone.Core.MediaFiles.MediaInfo; +using Sonarr.Http.REST; namespace NzbDrone.Api.EpisodeFiles { diff --git a/src/NzbDrone.Api/Episodes/EpisodeModule.cs b/src/NzbDrone.Api/Episodes/EpisodeModule.cs index 7f6f5692c..bd57332b7 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModule.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModule.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; using NzbDrone.SignalR; diff --git a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs index 349a629a4..ffdc051fa 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Api.EpisodeFiles; using NzbDrone.Api.Series; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; @@ -9,10 +9,11 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.SignalR; +using Sonarr.Http; namespace NzbDrone.Api.Episodes { - public abstract class EpisodeModuleWithSignalR : NzbDroneRestModuleWithSignalR, + public abstract class EpisodeModuleWithSignalR : SonarrRestModuleWithSignalR, IHandle, IHandle { diff --git a/src/NzbDrone.Api/Episodes/EpisodeResource.cs b/src/NzbDrone.Api/Episodes/EpisodeResource.cs index 3eb09bbf4..6c785c8b5 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeResource.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeResource.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using NzbDrone.Api.EpisodeFiles; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Api/Episodes/RenameEpisodeModule.cs b/src/NzbDrone.Api/Episodes/RenameEpisodeModule.cs index 87f39b964..b0bb2ecb7 100644 --- a/src/NzbDrone.Api/Episodes/RenameEpisodeModule.cs +++ b/src/NzbDrone.Api/Episodes/RenameEpisodeModule.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.MediaFiles; +using Sonarr.Http; namespace NzbDrone.Api.Episodes { - public class RenameEpisodeModule : NzbDroneRestModule + public class RenameEpisodeModule : SonarrRestModule { private readonly IRenameEpisodeFileService _renameEpisodeFileService; diff --git a/src/NzbDrone.Api/Episodes/RenameEpisodeResource.cs b/src/NzbDrone.Api/Episodes/RenameEpisodeResource.cs index c48f2cdf4..b1f99a28b 100644 --- a/src/NzbDrone.Api/Episodes/RenameEpisodeResource.cs +++ b/src/NzbDrone.Api/Episodes/RenameEpisodeResource.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; namespace NzbDrone.Api.Episodes { diff --git a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs index 67c2be7bd..0dab92ee8 100644 --- a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs +++ b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.IO; using System.Linq; using Nancy; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; @@ -31,15 +31,10 @@ namespace NzbDrone.Api.FileSystem private Response GetContents() { var pathQuery = Request.Query.path; - var includeFilesQuery = Request.Query.includeFiles; - bool includeFiles = false; + var includeFiles = Request.GetBooleanQueryParameter("includeFiles"); + var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes"); - if (includeFilesQuery.HasValue) - { - includeFiles = Convert.ToBoolean(includeFilesQuery.Value); - } - - return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles).AsResponse(); + return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes).AsResponse(); } private Response GetEntityType() @@ -73,4 +68,4 @@ namespace NzbDrone.Api.FileSystem }).AsResponse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs deleted file mode 100644 index 218d185f5..000000000 --- a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; -using Nancy; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Analytics; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Frontend.Mappers -{ - public class IndexHtmlMapper : StaticResourceMapperBase - { - private readonly IDiskProvider _diskProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly IAnalyticsService _analyticsService; - private readonly Func _cacheBreakProviderFactory; - private readonly string _indexPath; - private static readonly Regex ReplaceRegex = new Regex(@"(?:(?href|src)=\"")(?.*?(?css|js|png|ico|ics|svg))(?:\"")(?:\s(?data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static string API_KEY; - private static string URL_BASE; - private string _generatedContent - ; - - public IndexHtmlMapper(IAppFolderInfo appFolderInfo, - IDiskProvider diskProvider, - IConfigFileProvider configFileProvider, - IAnalyticsService analyticsService, - Func cacheBreakProviderFactory, - Logger logger) - : base(diskProvider, logger) - { - _diskProvider = diskProvider; - _configFileProvider = configFileProvider; - _analyticsService = analyticsService; - _cacheBreakProviderFactory = cacheBreakProviderFactory; - _indexPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "index.html"); - - API_KEY = configFileProvider.ApiKey; - URL_BASE = configFileProvider.UrlBase; - } - - public override string Map(string resourceUrl) - { - return _indexPath; - } - - public override bool CanHandle(string resourceUrl) - { - resourceUrl = resourceUrl.ToLowerInvariant(); - - return !resourceUrl.StartsWith("/content") && - !resourceUrl.StartsWith("/mediacover") && - !resourceUrl.Contains(".") && - !resourceUrl.StartsWith("/login"); - } - - public override Response GetResponse(string resourceUrl) - { - var response = base.GetResponse(resourceUrl); - response.Headers["X-UA-Compatible"] = "IE=edge"; - - return response; - } - - protected override Stream GetContentStream(string filePath) - { - var text = GetIndexText(); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(text); - writer.Flush(); - stream.Position = 0; - return stream; - } - - private string GetIndexText() - { - if (RuntimeInfo.IsProduction && _generatedContent != null) - { - return _generatedContent; - } - - var text = _diskProvider.ReadAllText(_indexPath); - - var cacheBreakProvider = _cacheBreakProviderFactory(); - - text = ReplaceRegex.Replace(text, match => - { - string url; - - if (match.Groups["nohash"].Success) - { - url = match.Groups["path"].Value; - } - - else - { - url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value); - } - - return string.Format("{0}=\"{1}{2}\"", match.Groups["attribute"].Value, URL_BASE, url); - }); - - text = text.Replace("API_ROOT", URL_BASE + "/api"); - text = text.Replace("API_KEY", API_KEY); - text = text.Replace("APP_VERSION", BuildInfo.Version.ToString()); - text = text.Replace("APP_BRANCH", _configFileProvider.Branch.ToLower()); - text = text.Replace("APP_ANALYTICS", _analyticsService.IsEnabled.ToString().ToLowerInvariant()); - text = text.Replace("URL_BASE", URL_BASE); - text = text.Replace("PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant()); - - _generatedContent = text; - - return _generatedContent; - } - } -} diff --git a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs deleted file mode 100644 index 974e117f9..000000000 --- a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; -using Nancy; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Frontend.Mappers -{ - public class LoginHtmlMapper : StaticResourceMapperBase - { - private readonly IDiskProvider _diskProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly Func _cacheBreakProviderFactory; - private readonly string _indexPath; - private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static string URL_BASE; - private string _generatedContent; - - public LoginHtmlMapper(IAppFolderInfo appFolderInfo, - IDiskProvider diskProvider, - IConfigFileProvider configFileProvider, - Func cacheBreakProviderFactory, - Logger logger) - : base(diskProvider, logger) - { - _diskProvider = diskProvider; - _configFileProvider = configFileProvider; - _cacheBreakProviderFactory = cacheBreakProviderFactory; - _indexPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "login.html"); - - URL_BASE = configFileProvider.UrlBase; - } - - public override string Map(string resourceUrl) - { - return _indexPath; - } - - public override bool CanHandle(string resourceUrl) - { - return resourceUrl.StartsWith("/login"); - } - - public override Response GetResponse(string resourceUrl) - { - var response = base.GetResponse(resourceUrl); - response.Headers["X-UA-Compatible"] = "IE=edge"; - - return response; - } - - protected override Stream GetContentStream(string filePath) - { - var text = GetLoginText(); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(text); - writer.Flush(); - stream.Position = 0; - return stream; - } - - private string GetLoginText() - { - if (RuntimeInfo.IsProduction && _generatedContent != null) - { - return _generatedContent; - } - - var text = _diskProvider.ReadAllText(_indexPath); - - var cacheBreakProvider = _cacheBreakProviderFactory(); - - text = ReplaceRegex.Replace(text, match => - { - var url = cacheBreakProvider.AddCacheBreakerToPath(match.Value); - return URL_BASE + url; - }); - - _generatedContent = text; - - return _generatedContent; - } - } -} diff --git a/src/NzbDrone.Api/Health/HealthModule.cs b/src/NzbDrone.Api/Health/HealthModule.cs index 2699fa7d6..c3b64c607 100644 --- a/src/NzbDrone.Api/Health/HealthModule.cs +++ b/src/NzbDrone.Api/Health/HealthModule.cs @@ -3,10 +3,11 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; +using Sonarr.Http; namespace NzbDrone.Api.Health { - public class HealthModule : NzbDroneRestModuleWithSignalR, + public class HealthModule : SonarrRestModuleWithSignalR, IHandle { private readonly IHealthCheckService _healthCheckService; diff --git a/src/NzbDrone.Api/Health/HealthResource.cs b/src/NzbDrone.Api/Health/HealthResource.cs index e860cb778..934079d38 100644 --- a/src/NzbDrone.Api/Health/HealthResource.cs +++ b/src/NzbDrone.Api/Health/HealthResource.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Common.Http; using NzbDrone.Core.HealthCheck; +using Sonarr.Http.REST; namespace NzbDrone.Api.Health { diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs index 4902c195a..3b441dd6b 100644 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ b/src/NzbDrone.Api/History/HistoryModule.cs @@ -3,17 +3,18 @@ using System.Collections.Generic; using System.Linq; using Nancy; using NzbDrone.Api.Episodes; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.REST; +using Sonarr.Http.Extensions; using NzbDrone.Api.Series; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.History; +using Sonarr.Http; +using Sonarr.Http.REST; namespace NzbDrone.Api.History { - public class HistoryModule : NzbDroneRestModule + public class HistoryModule : SonarrRestModule { private readonly IHistoryService _historyService; private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; @@ -50,19 +51,19 @@ namespace NzbDrone.Api.History private PagingResource GetHistory(PagingResource pagingResource) { var episodeId = Request.Query.EpisodeId; - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + var filter = pagingResource.Filters.FirstOrDefault(); - if (pagingResource.FilterKey == "eventType") + if (filter != null && filter.Key == "eventType") { - var filterValue = (HistoryEventType)Convert.ToInt32(pagingResource.FilterValue); - pagingSpec.FilterExpression = v => v.EventType == filterValue; + var filterValue = (HistoryEventType)Convert.ToInt32(filter.Value); + pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue); } if (episodeId.HasValue) { int i = (int)episodeId; - pagingSpec.FilterExpression = h => h.EpisodeId == i; + pagingSpec.FilterExpressions.Add(h => h.EpisodeId == i); } return ApplyToPage(_historyService.Paged, pagingSpec, MapToResource); diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index dba4149dd..ad5786f34 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; using NzbDrone.Api.Episodes; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; - namespace NzbDrone.Api.History { public class HistoryResource : RestResource diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 0f28dc3fa..9793800b5 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -10,7 +10,7 @@ using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using Nancy.ModelBinding; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Common.Cache; using HttpStatusCode = System.Net.HttpStatusCode; diff --git a/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs b/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs index f6a223475..e5d0cca47 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; +using Sonarr.Http; namespace NzbDrone.Api.Indexers { - public abstract class ReleaseModuleBase : NzbDroneRestModule + public abstract class ReleaseModuleBase : SonarrRestModule { protected virtual List MapDecisions(IEnumerable decisions) { diff --git a/src/NzbDrone.Api/Indexers/ReleasePushModule.cs b/src/NzbDrone.Api/Indexers/ReleasePushModule.cs index 7cb93739c..e9e6da37f 100644 --- a/src/NzbDrone.Api/Indexers/ReleasePushModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleasePushModule.cs @@ -11,6 +11,8 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; +using Sonarr.Http.Extensions; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Indexers { diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index 6bf07910d..c9c0f6d78 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; using NzbDrone.Core.Indexers; diff --git a/src/NzbDrone.Api/Logs/LogFileModuleBase.cs b/src/NzbDrone.Api/Logs/LogFileModuleBase.cs index d8a12d1bf..53c8e57a3 100644 --- a/src/NzbDrone.Api/Logs/LogFileModuleBase.cs +++ b/src/NzbDrone.Api/Logs/LogFileModuleBase.cs @@ -5,10 +5,11 @@ using NzbDrone.Common.Disk; using Nancy; using Nancy.Responses; using NzbDrone.Core.Configuration; +using Sonarr.Http; namespace NzbDrone.Api.Logs { - public abstract class LogFileModuleBase : NzbDroneRestModule + public abstract class LogFileModuleBase : SonarrRestModule { protected const string LOGFILE_ROUTE = @"/(?[-.a-zA-Z0-9]+?\.txt)"; diff --git a/src/NzbDrone.Api/Logs/LogFileResource.cs b/src/NzbDrone.Api/Logs/LogFileResource.cs index 9f67c8af7..743d797f3 100644 --- a/src/NzbDrone.Api/Logs/LogFileResource.cs +++ b/src/NzbDrone.Api/Logs/LogFileResource.cs @@ -1,5 +1,5 @@ using System; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; namespace NzbDrone.Api.Logs { diff --git a/src/NzbDrone.Api/Logs/LogModule.cs b/src/NzbDrone.Api/Logs/LogModule.cs index 88ead3ec0..3e395a6fa 100644 --- a/src/NzbDrone.Api/Logs/LogModule.cs +++ b/src/NzbDrone.Api/Logs/LogModule.cs @@ -1,8 +1,10 @@ -using NzbDrone.Core.Instrumentation; +using System.Linq; +using NzbDrone.Core.Instrumentation; +using Sonarr.Http; namespace NzbDrone.Api.Logs { - public class LogModule : NzbDroneRestModule + public class LogModule : SonarrRestModule { private readonly ILogService _logService; @@ -21,27 +23,29 @@ namespace NzbDrone.Api.Logs pageSpec.SortKey = "id"; } - if (pagingResource.FilterKey == "level") + var filter = pagingResource.Filters.FirstOrDefault(); + + if (filter != null && filter.Key == "level") { - switch (pagingResource.FilterValue) + switch (filter.Value) { case "Fatal": - pageSpec.FilterExpression = h => h.Level == "Fatal"; + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal"); break; case "Error": - pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error"; + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error"); break; case "Warn": - pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn"; + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn"); break; case "Info": - pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info"; + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info"); break; case "Debug": - pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug"; + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug"); break; case "Trace": - pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace"; + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace"); break; } } @@ -49,4 +53,4 @@ namespace NzbDrone.Api.Logs return ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Logs/LogResource.cs b/src/NzbDrone.Api/Logs/LogResource.cs index 504a45839..27a8f7047 100644 --- a/src/NzbDrone.Api/Logs/LogResource.cs +++ b/src/NzbDrone.Api/Logs/LogResource.cs @@ -1,5 +1,5 @@ using System; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; namespace NzbDrone.Api.Logs { diff --git a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs b/src/NzbDrone.Api/ManualImport/ManualImportModule.cs index 024b8e452..1937d01f8 100644 --- a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs +++ b/src/NzbDrone.Api/ManualImport/ManualImportModule.cs @@ -1,11 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Qualities; +using Sonarr.Http; +using Sonarr.Http.Extensions; namespace NzbDrone.Api.ManualImport { - public class ManualImportModule : NzbDroneRestModule + public class ManualImportModule : SonarrRestModule { private readonly IManualImportService _manualImportService; @@ -24,8 +26,9 @@ namespace NzbDrone.Api.ManualImport var downloadIdQuery = Request.Query.downloadId; var downloadId = (string)downloadIdQuery.Value; + var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true); - return _manualImportService.GetMediaFiles(folder, downloadId).ToResource().Select(AddQualityWeight).ToList(); + return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); } private ManualImportResource AddQualityWeight(ManualImportResource item) @@ -40,4 +43,4 @@ namespace NzbDrone.Api.ManualImport return item; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs index 1a779a410..f283b543e 100644 --- a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs +++ b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Api.Episodes; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Common.Crypto; using NzbDrone.Core.DecisionEngine; diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index dfa4bfb4e..039f70edd 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -78,7 +78,6 @@ - False @@ -88,43 +87,31 @@ + + ..\packages\ValueInjecter.2.3.3\lib\net35\Omu.ValueInjecter.dll + Properties\SharedAssemblyInfo.cs - - - - - - - - - - - - - - - - - + + @@ -154,31 +141,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -196,14 +158,9 @@ - - - - - @@ -212,21 +169,13 @@ - - - - - - - - @@ -245,12 +194,8 @@ - - - - @@ -278,6 +223,10 @@ {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} NzbDrone.SignalR + + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Sonarr.Http + diff --git a/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs b/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs deleted file mode 100644 index a2061a770..000000000 --- a/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.SignalR; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneRestModuleWithSignalR : NzbDroneRestModule, IHandle> - where TResource : RestResource, new() - where TModel : ModelBase, new() - { - private readonly IBroadcastSignalRMessage _signalRBroadcaster; - - protected NzbDroneRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) - { - _signalRBroadcaster = signalRBroadcaster; - } - - protected NzbDroneRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) - : base(resource) - { - _signalRBroadcaster = signalRBroadcaster; - } - - public void Handle(ModelEvent message) - { - if (message.Action == ModelAction.Deleted || message.Action == ModelAction.Sync) - { - BroadcastResourceChange(message.Action); - } - - BroadcastResourceChange(message.Action, message.Model.Id); - } - - protected void BroadcastResourceChange(ModelAction action, int id) - { - var resource = GetResourceById(id); - BroadcastResourceChange(action, resource); - } - - - protected void BroadcastResourceChange(ModelAction action, TResource resource) - { - var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(resource, action) - }; - - _signalRBroadcaster.BroadcastMessage(signalRMessage); - } - - - protected void BroadcastResourceChange(ModelAction action) - { - var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(action) - }; - - _signalRBroadcaster.BroadcastMessage(signalRMessage); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Parse/ParseModule.cs b/src/NzbDrone.Api/Parse/ParseModule.cs index 266f66eb4..0dec532ae 100644 --- a/src/NzbDrone.Api/Parse/ParseModule.cs +++ b/src/NzbDrone.Api/Parse/ParseModule.cs @@ -2,10 +2,11 @@ using NzbDrone.Api.Series; using NzbDrone.Common.Extensions; using NzbDrone.Core.Parser; +using Sonarr.Http; namespace NzbDrone.Api.Parse { - public class ParseModule : NzbDroneRestModule + public class ParseModule : SonarrRestModule { private readonly IParsingService _parsingService; diff --git a/src/NzbDrone.Api/Parse/ParseResource.cs b/src/NzbDrone.Api/Parse/ParseResource.cs index c795f09c3..8ba51bd0e 100644 --- a/src/NzbDrone.Api/Parse/ParseResource.cs +++ b/src/NzbDrone.Api/Parse/ParseResource.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using NzbDrone.Api.Episodes; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs index e7975b661..636979d86 100644 --- a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs +++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using FluentValidation; using FluentValidation.Results; -using NzbDrone.Api.REST; -using NzbDrone.Api.Validation; +using Sonarr.Http.REST; using NzbDrone.Core.Profiles.Delay; +using Sonarr.Http; +using Sonarr.Http.Validation; namespace NzbDrone.Api.Profiles.Delay { - public class DelayProfileModule : NzbDroneRestModule + public class DelayProfileModule : SonarrRestModule { private readonly IDelayProfileService _delayProfileService; diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs index e35df9043..53b7c4f19 100644 --- a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Delay; diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs index 147bc69aa..5576c8e9f 100644 --- a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs +++ b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Parser; +using Sonarr.Http; namespace NzbDrone.Api.Profiles.Languages { - public class LanguageModule : NzbDroneRestModule + public class LanguageModule : SonarrRestModule { public LanguageModule() { diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs index 09e5ba28c..adae3854c 100644 --- a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs +++ b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; namespace NzbDrone.Api.Profiles.Languages { diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs index e5803db20..4290c5a56 100644 --- a/src/NzbDrone.Api/Profiles/ProfileModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileModule.cs @@ -2,10 +2,12 @@ using FluentValidation; using NzbDrone.Core.Profiles; using NzbDrone.Core.Validation; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Profiles { - public class ProfileModule : NzbDroneRestModule + public class ProfileModule : SonarrRestModule { private readonly IProfileService _profileService; diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs index ee02bcb32..d407942dc 100644 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; diff --git a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs index ec5f3ae01..423db02bd 100644 --- a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs @@ -3,10 +3,12 @@ using System.Linq; using NzbDrone.Core.Parser; using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Profiles { - public class ProfileSchemaModule : NzbDroneRestModule + public class ProfileSchemaModule : SonarrRestModule { private readonly IQualityDefinitionService _qualityDefinitionService; diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index d7ad2ec67..6705437bf 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -3,16 +3,18 @@ using System.Linq; using FluentValidation; using FluentValidation.Results; using Nancy; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using Newtonsoft.Json; +using Sonarr.Http; +using Sonarr.Http.ClientSchema; +using Sonarr.Http.Mapping; namespace NzbDrone.Api { - public abstract class ProviderModuleBase : NzbDroneRestModule + public abstract class ProviderModuleBase : SonarrRestModule where TProviderDefinition : ProviderDefinition, new() where TProvider : IProvider where TProviderResource : ProviderResource, new() diff --git a/src/NzbDrone.Api/ProviderResource.cs b/src/NzbDrone.Api/ProviderResource.cs index 9927a09cc..832e6becf 100644 --- a/src/NzbDrone.Api/ProviderResource.cs +++ b/src/NzbDrone.Api/ProviderResource.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.ThingiProvider; +using Sonarr.Http.ClientSchema; namespace NzbDrone.Api { diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs index 1b5351300..bf1740db3 100644 --- a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using NzbDrone.Core.Qualities; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Qualities { - public class QualityDefinitionModule : NzbDroneRestModule + public class QualityDefinitionModule : SonarrRestModule { private readonly IQualityDefinitionService _qualityDefinitionService; diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs index ea0edc0ab..fefe8fcd0 100644 --- a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using Sonarr.Http.REST; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; namespace NzbDrone.Api.Qualities diff --git a/src/NzbDrone.Api/Queue/QueueActionModule.cs b/src/NzbDrone.Api/Queue/QueueActionModule.cs index 9882e60e6..7f5307929 100644 --- a/src/NzbDrone.Api/Queue/QueueActionModule.cs +++ b/src/NzbDrone.Api/Queue/QueueActionModule.cs @@ -1,16 +1,17 @@ using System; using Nancy; using Nancy.Responses; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.REST; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Queue; +using Sonarr.Http; namespace NzbDrone.Api.Queue { - public class QueueActionModule : NzbDroneRestModule + public class QueueActionModule : SonarrRestModule { private readonly IQueueService _queueService; private readonly ITrackedDownloadService _trackedDownloadService; diff --git a/src/NzbDrone.Api/Queue/QueueModule.cs b/src/NzbDrone.Api/Queue/QueueModule.cs index 00e614132..39053d0fc 100644 --- a/src/NzbDrone.Api/Queue/QueueModule.cs +++ b/src/NzbDrone.Api/Queue/QueueModule.cs @@ -5,10 +5,11 @@ using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Queue; using NzbDrone.SignalR; +using Sonarr.Http; namespace NzbDrone.Api.Queue { - public class QueueModule : NzbDroneRestModuleWithSignalR, + public class QueueModule : SonarrRestModuleWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index cf1356c49..88fd05f43 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; using NzbDrone.Api.Episodes; diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs index a61b5f7b3..c5e2f0bd3 100644 --- a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs +++ b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs @@ -2,10 +2,11 @@ using FluentValidation; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; namespace NzbDrone.Api.RemotePathMappings { - public class RemotePathMappingModule : NzbDroneRestModule + public class RemotePathMappingModule : SonarrRestModule { private readonly IRemotePathMappingService _remotePathMappingService; diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs index 60c01b682..5b5ff727b 100644 --- a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs +++ b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using Sonarr.Http.REST; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Api.RemotePathMappings diff --git a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs index 918b3a50b..569b9efd9 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs +++ b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs @@ -2,10 +2,12 @@ using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Restrictions; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Restrictions { - public class RestrictionModule : NzbDroneRestModule + public class RestrictionModule : SonarrRestModule { private readonly IRestrictionService _restrictionService; diff --git a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs b/src/NzbDrone.Api/Restrictions/RestrictionResource.cs index 14085e820..0e1eddfb1 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs +++ b/src/NzbDrone.Api/Restrictions/RestrictionResource.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Restrictions; namespace NzbDrone.Api.Restrictions diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index 30303ab73..2581f4c35 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -3,10 +3,12 @@ using FluentValidation; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.RootFolders { - public class RootFolderModule : NzbDroneRestModuleWithSignalR + public class RootFolderModule : SonarrRestModuleWithSignalR { private readonly IRootFolderService _rootFolderService; diff --git a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs b/src/NzbDrone.Api/RootFolders/RootFolderResource.cs index df55fcf1a..13fc1b198 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderResource.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Sonarr.Http.REST; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.RootFolders; namespace NzbDrone.Api.RootFolders diff --git a/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs b/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs index 93cd25ce5..78c7d713a 100644 --- a/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs +++ b/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs @@ -1,5 +1,5 @@ using Nancy; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Core.Tv; namespace NzbDrone.Api.SeasonPass diff --git a/src/NzbDrone.Api/Series/SeriesEditorModule.cs b/src/NzbDrone.Api/Series/SeriesEditorModule.cs index d68fa7aa4..7fa65a16e 100644 --- a/src/NzbDrone.Api/Series/SeriesEditorModule.cs +++ b/src/NzbDrone.Api/Series/SeriesEditorModule.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Linq; using Nancy; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Core.Tv; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Series { @@ -23,7 +24,7 @@ namespace NzbDrone.Api.Series var series = resources.Select(seriesResource => seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id))).ToList(); - return _seriesService.UpdateSeries(series) + return _seriesService.UpdateSeries(series, true) .ToResource(false) .AsResponse(HttpStatusCode.Accepted); } diff --git a/src/NzbDrone.Api/Series/SeriesLookupModule.cs b/src/NzbDrone.Api/Series/SeriesLookupModule.cs index 6506c1f82..843edc7b7 100644 --- a/src/NzbDrone.Api/Series/SeriesLookupModule.cs +++ b/src/NzbDrone.Api/Series/SeriesLookupModule.cs @@ -1,13 +1,15 @@ using System.Collections.Generic; using Nancy; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using System.Linq; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Series { - public class SeriesLookupModule : NzbDroneRestModule + public class SeriesLookupModule : SonarrRestModule { private readonly ISearchForNewSeries _searchProxy; diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index 2ad1012e0..2ba912aec 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using FluentValidation; -using NzbDrone.Api.Extensions; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; @@ -16,10 +15,12 @@ using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Validation; using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; namespace NzbDrone.Api.Series { - public class SeriesModule : NzbDroneRestModuleWithSignalR, + public class SeriesModule : SonarrRestModuleWithSignalR, IHandle, IHandle, IHandle, @@ -64,7 +65,7 @@ namespace NzbDrone.Api.Series UpdateResource = UpdateSeries; DeleteResource = DeleteSeries; - Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); + SharedValidator.RuleFor(s => s.ProfileId).ValidId(); SharedValidator.RuleFor(s => s.Path) .Cascade(CascadeMode.StopOnFirstFailure) @@ -207,7 +208,12 @@ namespace NzbDrone.Api.Series if (mappings == null) return; - resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource + { + Title = v.Title, + SeasonNumber = v.SeasonNumber, + SceneSeasonNumber = v.SceneSeasonNumber + }).ToList(); } public void Handle(EpisodeImportedEvent message) diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs index b973ad30d..e73b9db3e 100644 --- a/src/NzbDrone.Api/Series/SeriesResource.cs +++ b/src/NzbDrone.Api/Series/SeriesResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; @@ -76,8 +76,6 @@ namespace NzbDrone.Api.Series public AddSeriesOptions AddOptions { get; set; } public Ratings Ratings { get; set; } - //TODO: Add series statistics as a property of the series (instead of individual properties) - //Used to support legacy consumers public int QualityProfileId { diff --git a/src/NzbDrone.Api/System/Backup/BackupModule.cs b/src/NzbDrone.Api/System/Backup/BackupModule.cs index 8874ad420..d9bbb4f5a 100644 --- a/src/NzbDrone.Api/System/Backup/BackupModule.cs +++ b/src/NzbDrone.Api/System/Backup/BackupModule.cs @@ -2,10 +2,11 @@ using System.IO; using System.Linq; using NzbDrone.Core.Backup; +using Sonarr.Http; namespace NzbDrone.Api.System.Backup { - public class BackupModule : NzbDroneRestModule + public class BackupModule : SonarrRestModule { private readonly IBackupService _backupService; diff --git a/src/NzbDrone.Api/System/Backup/BackupResource.cs b/src/NzbDrone.Api/System/Backup/BackupResource.cs index 7eac82838..40a0877f1 100644 --- a/src/NzbDrone.Api/System/Backup/BackupResource.cs +++ b/src/NzbDrone.Api/System/Backup/BackupResource.cs @@ -1,5 +1,5 @@ using System; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Backup; namespace NzbDrone.Api.System.Backup diff --git a/src/NzbDrone.Api/System/SystemModule.cs b/src/NzbDrone.Api/System/SystemModule.cs index c62ed3b9e..27b10f385 100644 --- a/src/NzbDrone.Api/System/SystemModule.cs +++ b/src/NzbDrone.Api/System/SystemModule.cs @@ -1,6 +1,6 @@ using Nancy; using Nancy.Routing; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; diff --git a/src/NzbDrone.Api/System/Tasks/TaskModule.cs b/src/NzbDrone.Api/System/Tasks/TaskModule.cs index db8c4f376..7eb75a79d 100644 --- a/src/NzbDrone.Api/System/Tasks/TaskModule.cs +++ b/src/NzbDrone.Api/System/Tasks/TaskModule.cs @@ -5,10 +5,11 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Jobs; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; +using Sonarr.Http; namespace NzbDrone.Api.System.Tasks { - public class TaskModule : NzbDroneRestModuleWithSignalR, IHandle + public class TaskModule : SonarrRestModuleWithSignalR, IHandle { private readonly ITaskManager _taskManager; diff --git a/src/NzbDrone.Api/System/Tasks/TaskResource.cs b/src/NzbDrone.Api/System/Tasks/TaskResource.cs index fda392cae..91d54f95c 100644 --- a/src/NzbDrone.Api/System/Tasks/TaskResource.cs +++ b/src/NzbDrone.Api/System/Tasks/TaskResource.cs @@ -1,5 +1,5 @@ using System; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; namespace NzbDrone.Api.System.Tasks { diff --git a/src/NzbDrone.Api/Tags/TagModule.cs b/src/NzbDrone.Api/Tags/TagModule.cs index d2a01667c..da4a04cbb 100644 --- a/src/NzbDrone.Api/Tags/TagModule.cs +++ b/src/NzbDrone.Api/Tags/TagModule.cs @@ -3,10 +3,12 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tags; using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Tags { - public class TagModule : NzbDroneRestModuleWithSignalR, IHandle + public class TagModule : SonarrRestModuleWithSignalR, IHandle { private readonly ITagService _tagService; diff --git a/src/NzbDrone.Api/Tags/TagResource.cs b/src/NzbDrone.Api/Tags/TagResource.cs index 678107bf5..13ca810ba 100644 --- a/src/NzbDrone.Api/Tags/TagResource.cs +++ b/src/NzbDrone.Api/Tags/TagResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using Sonarr.Http.REST; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.Tags; namespace NzbDrone.Api.Tags diff --git a/src/NzbDrone.Api/Update/UpdateModule.cs b/src/NzbDrone.Api/Update/UpdateModule.cs index 2104f23ea..79b269b32 100644 --- a/src/NzbDrone.Api/Update/UpdateModule.cs +++ b/src/NzbDrone.Api/Update/UpdateModule.cs @@ -2,10 +2,12 @@ using System.Linq; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Update; +using Sonarr.Http; +using Sonarr.Http.Mapping; namespace NzbDrone.Api.Update { - public class UpdateModule : NzbDroneRestModule + public class UpdateModule : SonarrRestModule { private readonly IRecentUpdateProvider _recentUpdateProvider; diff --git a/src/NzbDrone.Api/Update/UpdateResource.cs b/src/NzbDrone.Api/Update/UpdateResource.cs index dca6f6725..d1d7a33d2 100644 --- a/src/NzbDrone.Api/Update/UpdateResource.cs +++ b/src/NzbDrone.Api/Update/UpdateResource.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Core.Update; namespace NzbDrone.Api.Update diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs index d2d08edab..58a4f748f 100644 --- a/src/NzbDrone.Api/Wanted/CutoffModule.cs +++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs @@ -1,8 +1,10 @@ -using NzbDrone.Api.Episodes; +using System.Linq; +using NzbDrone.Api.Episodes; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Tv; using NzbDrone.SignalR; +using Sonarr.Http; namespace NzbDrone.Api.Wanted { @@ -24,14 +26,15 @@ namespace NzbDrone.Api.Wanted private PagingResource GetCutoffUnmetEpisodes(PagingResource pagingResource) { var pagingSpec = pagingResource.MapToPagingSpec("airDateUtc", SortDirection.Descending); + var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); - if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + if (filter != null && filter.Value == "false") { - pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false; + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } else { - pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true; + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); } var resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec, v => MapToResource(v, true, true)); diff --git a/src/NzbDrone.Api/Wanted/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs index 9f6215a2e..e30dc9cf4 100644 --- a/src/NzbDrone.Api/Wanted/MissingModule.cs +++ b/src/NzbDrone.Api/Wanted/MissingModule.cs @@ -1,8 +1,10 @@ -using NzbDrone.Api.Episodes; +using System.Linq; +using NzbDrone.Api.Episodes; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Tv; using NzbDrone.SignalR; +using Sonarr.Http; namespace NzbDrone.Api.Wanted { @@ -20,14 +22,15 @@ namespace NzbDrone.Api.Wanted private PagingResource GetMissingEpisodes(PagingResource pagingResource) { var pagingSpec = pagingResource.MapToPagingSpec("airDateUtc", SortDirection.Descending); + var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); - if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + if (monitoredFilter != null && monitoredFilter.Value == "false") { - pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false; + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } else { - pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true; + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); } var resource = ApplyToPage(_episodeService.EpisodesWithoutFiles, pagingSpec, v => MapToResource(v, true, false)); diff --git a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs index 804666ea1..70b68ca0a 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FluentAssertions; @@ -49,7 +49,7 @@ namespace NzbDrone.Common.Test.DiskTests .Setup(s => s.GetDirectoryInfos(It.IsAny())) .Returns(_folders); - Subject.LookupContents(root, false).Directories.Should().NotContain(Path.Combine(root, RECYCLING_BIN)); + Subject.LookupContents(root, false, false).Directories.Should().NotContain(Path.Combine(root, RECYCLING_BIN)); } [Test] @@ -62,7 +62,7 @@ namespace NzbDrone.Common.Test.DiskTests .Setup(s => s.GetDirectoryInfos(It.IsAny())) .Returns(_folders); - Subject.LookupContents(root, false).Directories.Should().NotContain(Path.Combine(root, SYSTEM_VOLUME_INFORMATION)); + Subject.LookupContents(root, false, false).Directories.Should().NotContain(Path.Combine(root, SYSTEM_VOLUME_INFORMATION)); } [Test] @@ -75,7 +75,7 @@ namespace NzbDrone.Common.Test.DiskTests .Setup(s => s.GetDirectoryInfos(It.IsAny())) .Returns(_folders); - var result = Subject.LookupContents(root, false); + var result = Subject.LookupContents(root, false, false); result.Directories.Should().HaveCount(_folders.Count - 3); diff --git a/src/NzbDrone.Common.Test/EnvironmentInfo/BuildInfoFixture.cs b/src/NzbDrone.Common.Test/EnvironmentInfo/BuildInfoFixture.cs index dca0b292e..40d2012a7 100644 --- a/src/NzbDrone.Common.Test/EnvironmentInfo/BuildInfoFixture.cs +++ b/src/NzbDrone.Common.Test/EnvironmentInfo/BuildInfoFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.EnvironmentInfo; @@ -10,7 +10,7 @@ namespace NzbDrone.Common.Test.EnvironmentInfo [Test] public void should_return_version() { - BuildInfo.Version.Major.Should().BeOneOf(2, 10); + BuildInfo.Version.Major.Should().BeOneOf(3, 10); } [Test] @@ -20,4 +20,4 @@ namespace NzbDrone.Common.Test.EnvironmentInfo BuildInfo.Branch.Should().NotBeNullOrWhiteSpace(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs index b262c9918..a3aa8810d 100644 --- a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs +++ b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -10,13 +9,13 @@ namespace NzbDrone.Common.Disk { public interface IFileSystemLookupService { - FileSystemResult LookupContents(string query, bool includeFiles); + FileSystemResult LookupContents(string query, bool includeFiles, bool allowFoldersWithoutTrailingSlashes); } public class FileSystemLookupService : IFileSystemLookupService { private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; + private readonly IRuntimeInfo _runtimeInfo; private readonly HashSet _setToRemove = new HashSet { @@ -48,20 +47,19 @@ namespace NzbDrone.Common.Disk "@eadir" }; - public FileSystemLookupService(IDiskProvider diskProvider, Logger logger) + public FileSystemLookupService(IDiskProvider diskProvider, IRuntimeInfo runtimeInfo) { _diskProvider = diskProvider; - _logger = logger; + _runtimeInfo = runtimeInfo; } - public FileSystemResult LookupContents(string query, bool includeFiles) + public FileSystemResult LookupContents(string query, bool includeFiles, bool allowFoldersWithoutTrailingSlashes) { - var result = new FileSystemResult(); - if (query.IsNullOrWhiteSpace()) { if (OsInfo.IsWindows) { + var result = new FileSystemResult(); result.Directories = GetDrives(); return result; @@ -70,67 +68,94 @@ namespace NzbDrone.Common.Disk query = "/"; } + if ( + allowFoldersWithoutTrailingSlashes && + query.IsPathValid() && + _diskProvider.FolderExists(query)) + { + return GetResult(query, includeFiles); + } + var lastSeparatorIndex = query.LastIndexOf(Path.DirectorySeparatorChar); var path = query.Substring(0, lastSeparatorIndex + 1); if (lastSeparatorIndex != -1) { - try - { - result.Parent = GetParent(path); - result.Directories = GetDirectories(path); - - if (includeFiles) - { - result.Files = GetFiles(path); - } - } - - catch (DirectoryNotFoundException) - { - return new FileSystemResult { Parent = GetParent(path) }; - } - catch (ArgumentException) - { - return new FileSystemResult(); - } - catch (IOException) - { - return new FileSystemResult { Parent = GetParent(path) }; - } - catch (UnauthorizedAccessException) - { - return new FileSystemResult { Parent = GetParent(path) }; - } + return GetResult(path, includeFiles); } - return result; + return new FileSystemResult(); } private List GetDrives() { return _diskProvider.GetMounts() + .Where(d => + { + // Fow Windows Services, exclude mapped network drives. + if (_runtimeInfo.IsWindowsService) + { + return d.DriveType != DriveType.Network; + } + + return true; + }) .Select(d => new FileSystemModel - { - Type = FileSystemEntityType.Drive, - Name = d.VolumeName, - Path = d.RootDirectory, - LastModified = null - }) + { + Type = FileSystemEntityType.Drive, + Name = GetVolumeName(d), + Path = d.RootDirectory, + LastModified = null + }) .ToList(); } + private FileSystemResult GetResult(string path, bool includeFiles) + { + var result = new FileSystemResult(); + + try + { + result.Parent = GetParent(path); + result.Directories = GetDirectories(path); + + if (includeFiles) + { + result.Files = GetFiles(path); + } + } + + catch (DirectoryNotFoundException) + { + return new FileSystemResult { Parent = GetParent(path) }; + } + catch (ArgumentException) + { + return new FileSystemResult(); + } + catch (IOException) + { + return new FileSystemResult { Parent = GetParent(path) }; + } + catch (UnauthorizedAccessException) + { + return new FileSystemResult { Parent = GetParent(path) }; + } + + return result; + } + private List GetDirectories(string path) { var directories = _diskProvider.GetDirectoryInfos(path) .OrderBy(d => d.Name) .Select(d => new FileSystemModel - { - Name = d.Name, - Path = GetDirectoryPath(d.FullName.GetActualCasing()), - LastModified = d.LastWriteTimeUtc, - Type = FileSystemEntityType.Folder - }) + { + Name = d.Name, + Path = GetDirectoryPath(d.FullName.GetActualCasing()), + LastModified = d.LastWriteTimeUtc, + Type = FileSystemEntityType.Folder + }) .ToList(); directories.RemoveAll(d => _setToRemove.Contains(d.Name.ToLowerInvariant())); @@ -143,18 +168,28 @@ namespace NzbDrone.Common.Disk return _diskProvider.GetFileInfos(path) .OrderBy(d => d.Name) .Select(d => new FileSystemModel - { - Name = d.Name, - Path = d.FullName.GetActualCasing(), - LastModified = d.LastWriteTimeUtc, - Extension = d.Extension, - Size = d.Length, - Type = FileSystemEntityType.File - }) + { + Name = d.Name, + Path = d.FullName.GetActualCasing(), + LastModified = d.LastWriteTimeUtc, + Extension = d.Extension, + Size = d.Length, + Type = FileSystemEntityType.File + }) .ToList(); } - private string GetDirectoryPath(string path) + private static string GetVolumeName(IMount mountInfo) + { + if (mountInfo.VolumeLabel.IsNullOrWhiteSpace()) + { + return mountInfo.Name; + } + + return $"{mountInfo.Name} ({mountInfo.VolumeLabel})"; + } + + private static string GetDirectoryPath(string path) { if (path.Last() != Path.DirectorySeparatorChar) { @@ -164,7 +199,7 @@ namespace NzbDrone.Common.Disk return path; } - private string GetParent(string path) + private static string GetParent(string path) { var di = new DirectoryInfo(path); diff --git a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs index d387001ef..a8e4bd9ad 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs @@ -1,12 +1,17 @@ +using System; + namespace NzbDrone.Common.EnvironmentInfo { public interface IRuntimeInfo { + DateTime StartTime { get; } bool IsUserInteractive { get; } bool IsAdmin { get; } bool IsWindowsService { get; } bool IsWindowsTray { get; } bool IsExiting { get; set; } + bool IsTray { get; } + RuntimeMode Mode { get; } bool RestartPending { get; set; } string ExecutingApplication { get; } } diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index 3337b99b8..753bd91e6 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Common.EnvironmentInfo public class RuntimeInfo : IRuntimeInfo { private readonly Logger _logger; + private readonly DateTime _startTime = DateTime.UtcNow; public RuntimeInfo(IServiceProvider serviceProvider, Logger logger) { @@ -37,6 +38,14 @@ namespace NzbDrone.Common.EnvironmentInfo IsProduction = InternalIsProduction(); } + public DateTime StartTime + { + get + { + return _startTime; + } + } + public static bool IsUserInteractive => Environment.UserInteractive; bool IRuntimeInfo.IsUserInteractive => IsUserInteractive; @@ -61,6 +70,37 @@ namespace NzbDrone.Common.EnvironmentInfo public bool IsWindowsService { get; private set; } public bool IsExiting { get; set; } + public bool IsTray + { + get + { + if (OsInfo.IsWindows) + { + return IsUserInteractive && Process.GetCurrentProcess().ProcessName.Equals(ProcessProvider.NZB_DRONE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase); + } + + return false; + } + } + + public RuntimeMode Mode + { + get + { + if (IsWindowsService) + { + return RuntimeMode.Service; + } + + if (IsTray) + { + return RuntimeMode.Tray; + } + + return RuntimeMode.Console; + } + } + public bool RestartPending { get; set; } public string ExecutingApplication { get; } diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs new file mode 100644 index 000000000..ad4c0a786 --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.EnvironmentInfo +{ + public enum RuntimeMode + { + Console, + Service, + Tray + } +} diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index 2eeb2fe4e..afa00d3a5 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; namespace NzbDrone.Common.Extensions { @@ -108,5 +109,13 @@ namespace NzbDrone.Common.Extensions { return source.Select(predicate).ToList(); } + +// public static IOrderedEnumerable OrderBy(this IEnumerable source, string propertyName, bool descending) +// { +// var property = typeof(TEntity).GetProperty(propertyName); +// Func orderByFunc = x => property.GetValue(x, null); +// +// return descending ? source.OrderByDescending(orderByFunc) : source.OrderBy(orderByFunc); +// } } } diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index cf854bdb6..4e16b072c 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -97,6 +97,7 @@ + diff --git a/src/NzbDrone.Common/Serializer/Json.cs b/src/NzbDrone.Common/Serializer/Json.cs index 5f8cbfd8d..4a0565a4d 100644 --- a/src/NzbDrone.Common/Serializer/Json.cs +++ b/src/NzbDrone.Common/Serializer/Json.cs @@ -10,11 +10,17 @@ namespace NzbDrone.Common.Serializer public static class Json { private static readonly JsonSerializer Serializer; - private static readonly JsonSerializerSettings SerializerSetting; + private static readonly JsonSerializerSettings SerializerSettings; static Json() { - SerializerSetting = new JsonSerializerSettings + SerializerSettings = GetSerializerSettings(); + Serializer = JsonSerializer.Create(SerializerSettings); + } + + public static JsonSerializerSettings GetSerializerSettings() + { + var serializerSettings = new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Utc, NullValueHandling = NullValueHandling.Ignore, @@ -23,14 +29,11 @@ namespace NzbDrone.Common.Serializer ContractResolver = new CamelCasePropertyNamesContractResolver() }; + serializerSettings.Converters.Add(new StringEnumConverter { CamelCaseText = true }); + serializerSettings.Converters.Add(new VersionConverter()); + serializerSettings.Converters.Add(new HttpUriConverter()); - SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); - //SerializerSetting.Converters.Add(new IntConverter()); - SerializerSetting.Converters.Add(new VersionConverter()); - SerializerSetting.Converters.Add(new HttpUriConverter()); - - Serializer = JsonSerializer.Create(SerializerSetting); - + return serializerSettings; } public static T Deserialize(string json) where T : new() @@ -113,7 +116,7 @@ namespace NzbDrone.Common.Serializer public static string ToJson(this object obj) { - return JsonConvert.SerializeObject(obj, SerializerSetting); + return JsonConvert.SerializeObject(obj, SerializerSettings); } public static void Serialize(TModel model, TextWriter outputStream) diff --git a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs index ce59cf37c..699b6d5ab 100644 --- a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs +++ b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs @@ -1,3 +1,4 @@ +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 660a58d6f..d57c01893 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Linq; using FizzWare.NBuilder; using Moq; diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index ee77f0df0..fa14ed95b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -39,6 +40,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Mocker.GetMock() .Setup(s => s.GetParentFolder(It.IsAny())) .Returns((string path) => Directory.GetParent(path).FullName); + + Mocker.GetMock() + .Setup(s => s.GetBestRootFolderPath(It.IsAny())) + .Returns(_rootFolder); } private void GivenRootFolder(params string[] subfolders) diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 2acbe077b..a81bc9215 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FizzWare.NBuilder; @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Never()); VerifyNoImport(); @@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -271,7 +271,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -380,7 +380,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 719682080..ac9d8d97b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, false); + Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, false, true); _fail1.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); _fail2.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 3c510140c..2ecb0b1f9 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -317,5 +317,16 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock().Verify(v => v.Add(It.Is(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic()))); } + public void should_delete_existing_metadata_files_with_the_same_path() + { + Mocker.GetMock() + .Setup(s => s.GetFilesWithRelativePath(It.IsAny(), It.IsAny())) + .Returns(Builder.CreateListOfSize(1).BuildList()); + + Subject.Import(new List { _approvedDecisions.First() }, false); + + Mocker.GetMock() + .Verify(v => v.Delete(It.IsAny(), DeleteMediaFileReason.ManualOverride), Times.Once()); + } } } diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs index 4dd7560ac..1134911d2 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs @@ -1,7 +1,9 @@ using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.MetadataSource.SkyHook; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; using NzbDrone.Test.Common; using NzbDrone.Test.Common.Categories; @@ -52,5 +54,24 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook ExceptionVerification.IgnoreWarns(); } + + [TestCase("tvdbid:78804")] + [TestCase("Doctor Who")] + public void should_return_existing_series_if_found(string term) + { + const int tvdbId = 78804; + var existingSeries = new Series + { + TvdbId = tvdbId + }; + + Mocker.GetMock().Setup(c => c.FindByTvdbId(tvdbId)).Returns(existingSeries); + + var result = Subject.SearchForNewSeries("tvdbid: " + tvdbId); + + result.Should().Contain(existingSeries); + result.Should().ContainSingle(c => c.TvdbId == tvdbId); + + } } } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 401a909da..4f23ec461 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -340,6 +340,7 @@ + @@ -407,6 +408,7 @@ + @@ -420,6 +422,7 @@ + diff --git a/src/NzbDrone.Core.Test/Profiles/Delay/DelayProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/Delay/DelayProfileServiceFixture.cs new file mode 100644 index 000000000..ff2df10f4 --- /dev/null +++ b/src/NzbDrone.Core.Test/Profiles/Delay/DelayProfileServiceFixture.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Profiles.Delay +{ + [TestFixture] + public class DelayProfileServiceFixture : CoreTest + { + private List _delayProfiles; + private DelayProfile _first; + private DelayProfile _last; + + [SetUp] + public void Setup() + { + _delayProfiles = Builder.CreateListOfSize(4) + .TheFirst(1) + .With(d => d.Order = int.MaxValue) + .TheNext(1) + .With(d => d.Order = 1) + .TheNext(1) + .With(d => d.Order = 2) + .TheNext(1) + .With(d => d.Order = 3) + .Build() + .ToList(); + + _first = _delayProfiles[1]; + _last = _delayProfiles.Last(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(_delayProfiles); + } + + [Test] + public void should_move_to_first_if_afterId_is_null() + { + var moving = _last; + var result = Subject.Reorder(moving.Id, null).OrderBy(d => d.Order).ToList(); + var moved = result.First(); + + moved.Id.Should().Be(moving.Id); + moved.Order.Should().Be(1); + } + + [Test] + public void should_move_after_if_afterId_is_not_null() + { + var after = _first; + var moving = _last; + var result = Subject.Reorder(moving.Id, _first.Id).OrderBy(d => d.Order).ToList(); + var moved = result[1]; + + moved.Id.Should().Be(moving.Id); + moved.Order.Should().Be(after.Order + 1); + } + + [Test] + public void should_reorder_delay_profiles_that_are_after_moved() + { + var moving = _last; + var result = Subject.Reorder(moving.Id, null).OrderBy(d => d.Order).ToList(); + + for (int i = 1; i < result.Count; i++) + { + var delayProfile = result[i]; + + if (delayProfile.Id == 1) + { + delayProfile.Order.Should().Be(int.MaxValue); + } + + else + { + delayProfile.Order.Should().Be(i + 1); + } + } + } + + [Test] + public void should_not_change_afters_order_if_moving_was_after() + { + var after = _first; + var afterOrder = after.Order; + var moving = _last; + var result = Subject.Reorder(moving.Id, _first.Id).OrderBy(d => d.Order).ToList(); + var afterMove = result.First(); + + afterMove.Id.Should().Be(after.Id); + afterMove.Order.Should().Be(afterOrder); + } + + [Test] + public void should_change_afters_order_if_moving_was_before() + { + var after = _last; + var afterOrder = after.Order; + var moving = _first; + + var result = Subject.Reorder(moving.Id, after.Id).OrderBy(d => d.Order).ToList(); + var afterMove = result.Single(d => d.Id == after.Id); + + afterMove.Order.Should().BeLessThan(afterOrder); + } + } +} diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs index e621801e8..737ddfd05 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs @@ -10,7 +10,6 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.Test.Profiles { [TestFixture] - public class ProfileServiceFixture : CoreTest { [Test] diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/LegacySetEpisodeMontitoredFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/LegacySetEpisodeMontitoredFixture.cs new file mode 100644 index 000000000..963c45d5b --- /dev/null +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/LegacySetEpisodeMontitoredFixture.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests +{ + [TestFixture] + public class LegacySetEpisodeMontitoredFixture : CoreTest + { + private Series _series; + private List _episodes; + + [SetUp] + public void Setup() + { + var seasons = 4; + + _series = Builder.CreateNew() + .With(s => s.Seasons = Builder.CreateListOfSize(seasons) + .All() + .With(n => n.Monitored = true) + .Build() + .ToList()) + .Build(); + + _episodes = Builder.CreateListOfSize(seasons) + .All() + .With(e => e.Monitored = true) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-7)) + //Missing + .TheFirst(1) + .With(e => e.EpisodeFileId = 0) + //Has File + .TheNext(1) + .With(e => e.EpisodeFileId = 1) + //Future + .TheNext(1) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(7)) + //Future/TBA + .TheNext(1) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = null) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(s => s.GetEpisodeBySeries(It.IsAny())) + .Returns(_episodes); + } + + private void GivenSpecials() + { + foreach (var episode in _episodes) + { + episode.SeasonNumber = 0; + } + + _series.Seasons = new List{new Season { Monitored = false, SeasonNumber = 0 }}; + } + + [Test] + public void should_be_able_to_monitor_series_without_changing_episodes() + { + Subject.SetEpisodeMonitoredStatus(_series, null); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.UpdateEpisodes(It.IsAny>()), Times.Never()); + } + + [Test] + public void should_be_able_to_monitor_all_episodes() + { + Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions()); + + Mocker.GetMock() + .Verify(v => v.UpdateEpisodes(It.Is>(l => l.All(e => e.Monitored)))); + } + + [Test] + public void should_be_able_to_monitor_missing_episodes_only() + { + var monitoringOptions = new MonitoringOptions + { + IgnoreEpisodesWithFiles = true, + IgnoreEpisodesWithoutFiles = false + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifyMonitored(e => !e.HasFile); + VerifyNotMonitored(e => e.HasFile); + } + + [Test] + public void should_be_able_to_monitor_new_episodes_only() + { + var monitoringOptions = new MonitoringOptions + { + IgnoreEpisodesWithFiles = true, + IgnoreEpisodesWithoutFiles = true + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifyMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.After(DateTime.UtcNow)); + VerifyMonitored(e => !e.AirDateUtc.HasValue); + VerifyNotMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)); + } + + [Test] + public void should_not_monitor_missing_specials() + { + GivenSpecials(); + + var monitoringOptions = new MonitoringOptions + { + IgnoreEpisodesWithFiles = true, + IgnoreEpisodesWithoutFiles = false + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifyNotMonitored(e => e.SeasonNumber == 0); + } + + [Test] + public void should_not_monitor_new_specials() + { + GivenSpecials(); + + var monitoringOptions = new MonitoringOptions + { + IgnoreEpisodesWithFiles = true, + IgnoreEpisodesWithoutFiles = true + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifyNotMonitored(e => e.SeasonNumber == 0); + } + + [Test] + public void should_not_monitor_season_when_all_episodes_are_monitored_except_latest_season() + { + _series.Seasons = Builder.CreateListOfSize(2) + .All() + .With(n => n.Monitored = true) + .Build() + .ToList(); + + _episodes = Builder.CreateListOfSize(5) + .All() + .With(e => e.SeasonNumber = 1) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-5)) + .TheLast(1) + .With(e => e.SeasonNumber = 2) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(s => s.GetEpisodeBySeries(It.IsAny())) + .Returns(_episodes); + + var monitoringOptions = new MonitoringOptions + { + IgnoreEpisodesWithoutFiles = true + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifySeasonMonitored(n => n.SeasonNumber == 2); + VerifySeasonNotMonitored(n => n.SeasonNumber == 1); + } + + [Test] + public void should_ignore_episodes_when_season_is_not_monitored() + { + _series.Seasons.ForEach(s => s.Monitored = false); + + Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions()); + + Mocker.GetMock() + .Verify(v => v.UpdateEpisodes(It.Is>(l => l.All(e => !e.Monitored)))); + } + + [Test] + public void should_should_not_monitor_episodes_if_season_is_not_monitored() + { + _series = Builder.CreateNew() + .With(s => s.Seasons = Builder.CreateListOfSize(2) + .TheFirst(1) + .With(n => n.Monitored = true) + .TheLast(1) + .With(n => n.Monitored = false) + .Build() + .ToList()) + .Build(); + + var episodes = Builder.CreateListOfSize(10) + .All() + .With(e => e.Monitored = true) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-7)) + .TheFirst(5) + .With(e => e.SeasonNumber = 1) + .TheLast(5) + .With(e => e.SeasonNumber = 2) + .BuildList(); + + Mocker.GetMock() + .Setup(s => s.GetEpisodeBySeries(It.IsAny())) + .Returns(episodes); + + Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions + { + IgnoreEpisodesWithFiles = true, + IgnoreEpisodesWithoutFiles = false + }); + + VerifyMonitored(e => e.SeasonNumber == 1); + VerifyNotMonitored(e => e.SeasonNumber == 2); + VerifySeasonMonitored(s => s.SeasonNumber == 1); + VerifySeasonNotMonitored(s => s.SeasonNumber == 2); + } + + private void VerifyMonitored(Func predicate) + { + Mocker.GetMock() + .Verify(v => v.UpdateEpisodes(It.Is>(l => l.Where(predicate).All(e => e.Monitored)))); + } + + private void VerifyNotMonitored(Func predicate) + { + Mocker.GetMock() + .Verify(v => v.UpdateEpisodes(It.Is>(l => l.Where(predicate).All(e => !e.Monitored)))); + } + + private void VerifySeasonMonitored(Func predicate) + { + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Where(predicate).All(n => n.Monitored)), It.IsAny())); + } + + private void VerifySeasonNotMonitored(Func predicate) + { + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Where(predicate).All(n => !n.Monitored)), It.IsAny())); + } + } +} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs index 70108d1be..120661418 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -91,8 +91,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests { var monitoringOptions = new MonitoringOptions { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = false + Monitor = MonitorTypes.Missing }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -106,8 +105,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests { var monitoringOptions = new MonitoringOptions { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = true + Monitor = MonitorTypes.Future }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -124,8 +122,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests var monitoringOptions = new MonitoringOptions { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = false + Monitor = MonitorTypes.Missing }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -140,8 +137,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests var monitoringOptions = new MonitoringOptions { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = true + Monitor = MonitorTypes.Future }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -174,7 +170,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests var monitoringOptions = new MonitoringOptions { - IgnoreEpisodesWithoutFiles = true + Monitor = MonitorTypes.LatestSeason }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -184,54 +180,31 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests } [Test] - public void should_ignore_episodes_when_season_is_not_monitored() + public void should_be_able_to_monitor_no_episodes() { - _series.Seasons.ForEach(s => s.Monitored = false); + var monitoringOptions = new MonitoringOptions + { + Monitor = MonitorTypes.None + }; - Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions()); + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); Mocker.GetMock() .Verify(v => v.UpdateEpisodes(It.Is>(l => l.All(e => !e.Monitored)))); } [Test] - public void should_should_not_monitor_episodes_if_season_is_not_monitored() + public void should_monitor_missing_episodes() { - _series = Builder.CreateNew() - .With(s => s.Seasons = Builder.CreateListOfSize(2) - .TheFirst(1) - .With(n => n.Monitored = true) - .TheLast(1) - .With(n => n.Monitored = false) - .Build() - .ToList()) - .Build(); + var monitoringOptions = new MonitoringOptions + { + Monitor = MonitorTypes.Missing + }; - var episodes = Builder.CreateListOfSize(10) - .All() - .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-7)) - .TheFirst(5) - .With(e => e.SeasonNumber = 1) - .TheLast(5) - .With(e => e.SeasonNumber = 2) - .BuildList(); + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); - Mocker.GetMock() - .Setup(s => s.GetEpisodeBySeries(It.IsAny())) - .Returns(episodes); - - Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions - { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = false - }); - - VerifyMonitored(e => e.SeasonNumber == 1); - VerifyNotMonitored(e => e.SeasonNumber == 2); - VerifySeasonMonitored(s => s.SeasonNumber == 1); - VerifySeasonNotMonitored(s => s.SeasonNumber == 2); + VerifyMonitored(e => !e.HasFile); + VerifyNotMonitored(e => e.HasFile); } private void VerifyMonitored(Func predicate) diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs index 3b8ebeedf..363214a5d 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -126,12 +126,12 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests private void GivenMonitoredFilterExpression() { - _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true; + _pagingSpec.FilterExpressions.Add(e => e.Monitored == true && e.Series.Monitored == true); } private void GivenUnmonitoredFilterExpression() { - _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false; + _pagingSpec.FilterExpressions.Add(e => e.Monitored == false || e.Series.Monitored == false); } [Test] diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs index 4f8f9eb23..c9dece998 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; @@ -90,12 +90,12 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests private void GivenMonitoredFilterExpression() { - _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true; + _pagingSpec.FilterExpressions.Add(e => e.Monitored == true && e.Series.Monitored == true); } private void GivenUnmonitoredFilterExpression() { - _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false; + _pagingSpec.FilterExpressions.Add(e => e.Monitored == false || e.Series.Monitored == false); } [Test] diff --git a/src/NzbDrone.Core.Test/TvTests/MoveSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/MoveSeriesServiceFixture.cs index fcf92117d..523452754 100644 --- a/src/NzbDrone.Core.Test/TvTests/MoveSeriesServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/MoveSeriesServiceFixture.cs @@ -1,4 +1,6 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; @@ -16,6 +18,7 @@ namespace NzbDrone.Core.Test.TvTests { private Series _series; private MoveSeriesCommand _command; + private BulkMoveSeriesCommand _bulkCommand; [SetUp] public void Setup() @@ -31,9 +34,26 @@ namespace NzbDrone.Core.Test.TvTests DestinationPath = @"C:\Test\TV2\Series".AsOsAgnostic() }; + _bulkCommand = new BulkMoveSeriesCommand + { + Series = new List + { + new BulkMoveSeries + { + SeriesId = 1, + SourcePath = @"C:\Test\TV\Series".AsOsAgnostic() + } + }, + DestinationRootFolder = @"C:\Test\TV2".AsOsAgnostic() + }; + Mocker.GetMock() .Setup(s => s.GetSeries(It.IsAny())) .Returns(_series); + + Mocker.GetMock() + .Setup(s => s.FolderExists(It.IsAny())) + .Returns(true); } private void GivenFailedMove() @@ -48,49 +68,64 @@ namespace NzbDrone.Core.Test.TvTests { GivenFailedMove(); - Assert.Throws(() => Subject.Execute(_command)); + Subject.Execute(_command); ExceptionVerification.ExpectedErrors(1); } [Test] - public void should_no_update_series_path_on_error() + public void should_revert_series_path_on_error() { GivenFailedMove(); - Assert.Throws(() => Subject.Execute(_command)); + Subject.Execute(_command); ExceptionVerification.ExpectedErrors(1); Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Once()); + } + + [Test] + public void should_use_destination_path() + { + Subject.Execute(_command); + + Mocker.GetMock() + .Verify(v => v.TransferFolder(_command.SourcePath, _command.DestinationPath, TransferMode.Move, It.IsAny()), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.GetSeriesFolder(It.IsAny(), null), Times.Never()); } [Test] public void should_build_new_path_when_root_folder_is_provided() { - _command.DestinationPath = null; - _command.DestinationRootFolder = @"C:\Test\TV3".AsOsAgnostic(); - - var expectedPath = @"C:\Test\TV3\Series".AsOsAgnostic(); - + var seriesFolder = "Series"; + var expectedPath = Path.Combine(_bulkCommand.DestinationRootFolder, seriesFolder); + Mocker.GetMock() - .Setup(s => s.GetSeriesFolder(It.IsAny(), null)) - .Returns("Series"); + .Setup(s => s.GetSeriesFolder(It.IsAny(), null)) + .Returns(seriesFolder); + + Subject.Execute(_bulkCommand); - Subject.Execute(_command); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Path == expectedPath), It.IsAny()), Times.Once()); + Mocker.GetMock() + .Verify(v => v.TransferFolder(_bulkCommand.Series.First().SourcePath, expectedPath, TransferMode.Move, It.IsAny()), Times.Once()); } [Test] - public void should_use_destination_path_if_destination_root_folder_is_blank() + public void should_skip_series_folder_if_it_does_not_exist() { - Subject.Execute(_command); + Mocker.GetMock() + .Setup(s => s.FolderExists(It.IsAny())) + .Returns(false); - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Path == _command.DestinationPath), It.IsAny()), Times.Once()); + + Subject.Execute(_command); + + Mocker.GetMock() + .Verify(v => v.TransferFolder(_command.SourcePath, _command.DestinationPath, TransferMode.Move, It.IsAny()), Times.Never()); Mocker.GetMock() .Verify(v => v.GetSeriesFolder(It.IsAny(), null), Times.Never()); diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesFolderPathBuilderFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesFolderPathBuilderFixture.cs new file mode 100644 index 000000000..93343034f --- /dev/null +++ b/src/NzbDrone.Core.Test/TvTests/SeriesFolderPathBuilderFixture.cs @@ -0,0 +1,94 @@ +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.TvTests +{ + [TestFixture] + public class SeriesFolderPathBuilderFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Title = "Series Title") + .With(s => s.Path = @"C:\Test\TV\Series.Title".AsOsAgnostic()) + .With(s => s.RootFolderPath = null) + .Build(); + } + + public void GivenSeriesFolderName(string name) + { + Mocker.GetMock() + .Setup(s => s.GetSeriesFolder(_series, null)) + .Returns(name); + } + public void GivenExistingRootFolder(string rootFolder) + { + Mocker.GetMock() + .Setup(s => s.GetBestRootFolderPath(It.IsAny())) + .Returns(rootFolder); + } + + [Test] + public void should_create_new_series_path() + { + var rootFolder = @"C:\Test\TV2".AsOsAgnostic(); + + GivenSeriesFolderName(_series.Title); + _series.RootFolderPath = rootFolder; + + + Subject.BuildPath(_series, false).Should().Be(Path.Combine(rootFolder, _series.Title)); + } + + [Test] + public void should_reuse_existing_relative_folder_name() + { + var folderName = Path.GetFileName(_series.Path); + var rootFolder = @"C:\Test\TV2".AsOsAgnostic(); + + GivenExistingRootFolder(Path.GetDirectoryName(_series.Path)); + GivenSeriesFolderName(_series.Title); + _series.RootFolderPath = rootFolder; + + Subject.BuildPath(_series, true).Should().Be(Path.Combine(rootFolder, folderName)); + } + + [Test] + public void should_reuse_existing_relative_folder_structure() + { + var existingRootFolder = @"C:\Test\TV".AsOsAgnostic(); + var existingRelativePath = @"S\Series.Title"; + var rootFolder = @"C:\Test\TV2".AsOsAgnostic(); + + GivenExistingRootFolder(existingRootFolder); + GivenSeriesFolderName(_series.Title); + _series.RootFolderPath = rootFolder; + _series.Path = Path.Combine(existingRootFolder, existingRelativePath); + + Subject.BuildPath(_series, true).Should().Be(Path.Combine(rootFolder, existingRelativePath)); + } + + [Test] + public void should_use_built_path_for_new_series() + { + var rootFolder = @"C:\Test\TV2".AsOsAgnostic(); + + GivenSeriesFolderName(_series.Title); + _series.RootFolderPath = rootFolder; + _series.Path = null; + + Subject.BuildPath(_series, true).Should().Be(Path.Combine(rootFolder, _series.Title)); + } + } +} diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs index 0fa33a68f..d8ad08ec6 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IO; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -31,7 +34,7 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests [Test] public void should_call_repo_updateMany() { - Subject.UpdateSeries(_series); + Subject.UpdateSeries(_series, false); Mocker.GetMock().Verify(v => v.UpdateMany(_series), Times.Once()); } @@ -42,13 +45,17 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests var newRoot = @"C:\Test\TV2".AsOsAgnostic(); _series.ForEach(s => s.RootFolderPath = newRoot); - Subject.UpdateSeries(_series).ForEach(s => s.Path.Should().StartWith(newRoot)); + Mocker.GetMock() + .Setup(s => s.BuildPath(It.IsAny(), false)) + .Returns((s, u) => Path.Combine(s.RootFolderPath, s.Title)); + + Subject.UpdateSeries(_series, false).ForEach(s => s.Path.Should().StartWith(newRoot)); } [Test] public void should_not_update_path_when_rootFolderPath_is_empty() { - Subject.UpdateSeries(_series).ForEach(s => + Subject.UpdateSeries(_series, false).ForEach(s => { var expectedPath = _series.Single(ser => ser.Id == s.Id).Path; s.Path.Should().Be(expectedPath); @@ -67,7 +74,11 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests var newRoot = @"C:\Test\TV2".AsOsAgnostic(); series.ForEach(s => s.RootFolderPath = newRoot); - Subject.UpdateSeries(series); + Mocker.GetMock() + .Setup(s => s.GetSeriesFolder(It.IsAny(), (NamingConfig)null)) + .Returns((s, n) => s.Title); + + Subject.UpdateSeries(series, false); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 97927d442..35dd1e5df 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace NzbDrone.Core.Annotations { @@ -18,11 +18,13 @@ namespace NzbDrone.Core.Annotations public FieldType Type { get; set; } public bool Advanced { get; set; } public Type SelectOptions { get; set; } + public string Section { get; set; } } public enum FieldType { Textbox, + Number, Password, Checkbox, Select, @@ -32,6 +34,7 @@ namespace NzbDrone.Core.Annotations Tag, Action, Url, - Captcha + Captcha, + OAuth } } diff --git a/src/NzbDrone.Core/CustomFilters/CustomFilter.cs b/src/NzbDrone.Core/CustomFilters/CustomFilter.cs new file mode 100644 index 000000000..1c6e3e9b9 --- /dev/null +++ b/src/NzbDrone.Core/CustomFilters/CustomFilter.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.CustomFilters +{ + public class CustomFilter : ModelBase + { + public string Type { get; set; } + public string Label { get; set; } + public string Filters { get; set; } + } +} diff --git a/src/NzbDrone.Core/CustomFilters/CustomFilterRepository.cs b/src/NzbDrone.Core/CustomFilters/CustomFilterRepository.cs new file mode 100644 index 000000000..9bdb8fd07 --- /dev/null +++ b/src/NzbDrone.Core/CustomFilters/CustomFilterRepository.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.CustomFilters +{ + public interface ICustomFilterRepository : IBasicRepository + { + } + + public class CustomFilterRepository : BasicRepository, ICustomFilterRepository + { + public CustomFilterRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} diff --git a/src/NzbDrone.Core/CustomFilters/CustomFilterService.cs b/src/NzbDrone.Core/CustomFilters/CustomFilterService.cs new file mode 100644 index 000000000..9ef98f8be --- /dev/null +++ b/src/NzbDrone.Core/CustomFilters/CustomFilterService.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.CustomFilters +{ + public interface ICustomFilterService + { + CustomFilter Add(CustomFilter customFilter); + List All(); + void Delete(int id); + CustomFilter Get(int id); + CustomFilter Update(CustomFilter customFilter); + } + + public class CustomFilterService : ICustomFilterService + { + private readonly ICustomFilterRepository _repo; + + public CustomFilterService(ICustomFilterRepository repo) + { + _repo = repo; + } + + public CustomFilter Add(CustomFilter customFilter) + { + return _repo.Insert(customFilter); + } + + public CustomFilter Update(CustomFilter customFilter) + { + return _repo.Update(customFilter); + } + + public void Delete(int id) + { + _repo.Delete(id); + } + + public CustomFilter Get(int id) + { + return _repo.Get(id); + } + + public List All() + { + return _repo.All().ToList(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index b2afafdc4..3cf47611b 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -254,10 +254,21 @@ namespace NzbDrone.Core.Datastore protected virtual SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { - return query.Where(pagingSpec.FilterExpression) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); + var filterExpressions = pagingSpec.FilterExpressions; + var sortQuery = query.Where(filterExpressions.FirstOrDefault()); + + if (filterExpressions.Count > 1) + { + // Start at the second item for the AndWhere clauses + for (var i = 1; i < filterExpressions.Count; i++) + { + sortQuery.AndWhere(filterExpressions[i]); + } + } + + return sortQuery.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize); } protected void ModelCreated(TModel model) diff --git a/src/NzbDrone.Core/Datastore/Migration/123_add_history_seriesId_index.cs b/src/NzbDrone.Core/Datastore/Migration/123_add_history_seriesId_index.cs new file mode 100644 index 000000000..0dd463d8c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/123_add_history_seriesId_index.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(123)] + public class add_history_seriesId_index : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.Index().OnTable("History").OnColumn("SeriesId"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/126_add_custom_filters.cs b/src/NzbDrone.Core/Datastore/Migration/126_add_custom_filters.cs new file mode 100644 index 000000000..21792c3be --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/126_add_custom_filters.cs @@ -0,0 +1,17 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(126)] + public class add_custom_filters : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("CustomFilters") + .WithColumn("Type").AsString().NotNullable() + .WithColumn("Label").AsString().NotNullable() + .WithColumn("Filters").AsString().NotNullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs index 7ebac899c..a76086d28 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs @@ -1,4 +1,5 @@ using System; +using System.Windows.Forms; using FluentMigrator; using NLog; using NzbDrone.Common.Instrumentation; diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs index 9d10255e0..5b1fb0a58 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs @@ -6,6 +6,7 @@ using FluentMigrator; using FluentMigrator.Expressions; using FluentMigrator.Model; using FluentMigrator.Runner; +using FluentMigrator.Runner.Announcers; using FluentMigrator.Runner.Generators.SQLite; using FluentMigrator.Runner.Processors.SQLite; using System.Text.RegularExpressions; diff --git a/src/NzbDrone.Core/Datastore/PagingSpec.cs b/src/NzbDrone.Core/Datastore/PagingSpec.cs index 63f8d719c..0c9179844 100644 --- a/src/NzbDrone.Core/Datastore/PagingSpec.cs +++ b/src/NzbDrone.Core/Datastore/PagingSpec.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq.Expressions; @@ -12,7 +12,12 @@ namespace NzbDrone.Core.Datastore public string SortKey { get; set; } public SortDirection SortDirection { get; set; } public List Records { get; set; } - public Expression> FilterExpression { get; set; } + public List>> FilterExpressions { get; set; } + + public PagingSpec() + { + FilterExpressions = new List>>(); + } } public enum SortDirection diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index cea8b2898..945ed40b2 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -29,6 +29,7 @@ using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; using NzbDrone.Common.Disk; using NzbDrone.Core.Authentication; +using NzbDrone.Core.CustomFilters; using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; @@ -57,7 +58,8 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.Enable) .Ignore(i => i.Protocol) .Ignore(i => i.SupportsRss) - .Ignore(i => i.SupportsSearch); + .Ignore(i => i.SupportsSearch) + .Ignore(d => d.Tags); Mapper.Entity().RegisterDefinition("Notifications") .Ignore(i => i.SupportsOnGrab) @@ -65,10 +67,12 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.SupportsOnUpgrade) .Ignore(i => i.SupportsOnRename); - Mapper.Entity().RegisterDefinition("Metadata"); + Mapper.Entity().RegisterDefinition("Metadata") + .Ignore(d => d.Tags); Mapper.Entity().RegisterDefinition("DownloadClients") - .Ignore(d => d.Protocol); + .Ignore(d => d.Protocol) + .Ignore(d => d.Tags); Mapper.Entity().RegisterModel("SceneMappings"); @@ -121,6 +125,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("IndexerStatus"); Mapper.Entity().RegisterModel("DownloadClientStatus"); + + Mapper.Entity().RegisterModel("CustomFilters"); } private static void RegisterMappers() diff --git a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs index 78c838b1a..f9371e01f 100644 --- a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs +++ b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs @@ -5,8 +5,6 @@ using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Tv; namespace NzbDrone.Core.DiskSpace @@ -19,23 +17,21 @@ namespace NzbDrone.Core.DiskSpace public class DiskSpaceService : IDiskSpaceService { private readonly ISeriesService _seriesService; - private readonly IConfigService _configService; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/(boot|etc)(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled); - public DiskSpaceService(ISeriesService seriesService, IConfigService configService, IDiskProvider diskProvider, Logger logger) + public DiskSpaceService(ISeriesService seriesService, IDiskProvider diskProvider, Logger logger) { _seriesService = seriesService; - _configService = configService; _diskProvider = diskProvider; _logger = logger; } public List GetFreeSpace() { - var importantRootFolders = GetSeriesRootPaths().Concat(GetDroneFactoryRootPaths()).Distinct().ToList(); + var importantRootFolders = GetSeriesRootPaths().Distinct().ToList(); var optionalRootFolders = GetFixedDisksRootPaths().Except(importantRootFolders).Distinct().ToList(); diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 3b5922c6a..3348be4a9 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using NzbDrone.Common.Disk; diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 262e4102b..9b3be9c00 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -203,7 +203,8 @@ namespace NzbDrone.Core.Download.Pending Timeleft = timeleft, EstimatedCompletionTime = ect, Status = pendingRelease.Reason.ToString(), - Protocol = pendingRelease.RemoteEpisode.Release.DownloadProtocol + Protocol = pendingRelease.RemoteEpisode.Release.DownloadProtocol, + Indexer = pendingRelease.RemoteEpisode.Release.Indexer }; queued.Add(queue); diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index bf375a797..9823bb2cc 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -13,7 +13,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads { public class DownloadMonitoringService : IExecute, IHandle, - IHandle + IHandle, + IHandle { private readonly IDownloadClientStatusService _downloadClientStatusService; private readonly IDownloadClientFactory _downloadClientFactory; @@ -67,10 +68,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads { var clientTrackedDownloads = ProcessClientDownloads(downloadClient); - // Only track completed downloads if trackedDownloads.AddRange(clientTrackedDownloads.Where(DownloadIsTrackable)); } + _trackedDownloadService.UpdateTrackable(trackedDownloads); _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); } finally @@ -92,6 +93,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads } catch (Exception ex) { + // TODO: Stop tracking items for the offline client + _downloadClientStatusService.RecordFailure(downloadClient.Definition.Id); _logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name); } @@ -108,7 +111,6 @@ namespace NzbDrone.Core.Download.TrackedDownloads } return trackedDownloads; - } private void RemoveCompletedDownloads(List trackedDownloads) @@ -133,7 +135,6 @@ namespace NzbDrone.Core.Download.TrackedDownloads { _completedDownloadService.Process(trackedDownload); } - } trackedDownloads.AddIfNotNull(trackedDownload); @@ -178,5 +179,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads { _refreshDebounce.Execute(); } + + public void Handle(TrackedDownloadsRemovedEvent message) + { + var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList(); + + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); + } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index be012d57b..b5497e509 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -12,6 +12,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads public RemoteEpisode RemoteEpisode { get; set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public bool IsTrackable { get; set; } public TrackedDownload() { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 4bc95903a..cc6dff316 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -1,9 +1,11 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Download.TrackedDownloads @@ -11,23 +13,30 @@ namespace NzbDrone.Core.Download.TrackedDownloads public interface ITrackedDownloadService { TrackedDownload Find(string downloadId); + void StopTracking(string downloadId); + void StopTracking(List downloadIds); TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem); + List GetTrackedDownloads(); + void UpdateTrackable(List trackedDownloads); } public class TrackedDownloadService : ITrackedDownloadService { private readonly IParsingService _parsingService; private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; private readonly ICached _cache; public TrackedDownloadService(IParsingService parsingService, - ICacheManager cacheManager, - IHistoryService historyService, - Logger logger) + ICacheManager cacheManager, + IHistoryService historyService, + IEventAggregator eventAggregator, + Logger logger) { _parsingService = parsingService; _historyService = historyService; + _eventAggregator = eventAggregator; _cache = cacheManager.GetCache(GetType()); _logger = logger; } @@ -37,6 +46,28 @@ namespace NzbDrone.Core.Download.TrackedDownloads return _cache.Find(downloadId); } + public void StopTracking(string downloadId) + { + var trackedDownload = _cache.Find(downloadId); + + _cache.Remove(downloadId); + _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(new List { trackedDownload })); + } + + public void StopTracking(List downloadIds) + { + var trackedDownloads = new List(); + + foreach (var downloadId in downloadIds) + { + var trackedDownload = _cache.Find(downloadId); + _cache.Remove(downloadId); + trackedDownloads.Add(trackedDownload); + } + + _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(trackedDownloads)); + } + public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem) { var existingItem = Find(downloadItem.DownloadId); @@ -46,6 +77,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); existingItem.DownloadItem = downloadItem; + existingItem.IsTrackable = true; + return existingItem; } @@ -53,7 +86,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads { DownloadClient = downloadClient.Id, DownloadItem = downloadItem, - Protocol = downloadClient.Protocol + Protocol = downloadClient.Protocol, + IsTrackable = true }; try @@ -71,6 +105,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType); + var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed); + trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; + if (parsedEpisodeInfo == null || trackedDownload.RemoteEpisode == null || trackedDownload.RemoteEpisode.Series == null || @@ -106,8 +143,23 @@ namespace NzbDrone.Core.Download.TrackedDownloads return trackedDownload; } - private void LogItemChange(TrackedDownload trackedDownload, DownloadClientItem existingItem, DownloadClientItem downloadItem) + public List GetTrackedDownloads() { + return _cache.Values.ToList(); + } + + public void UpdateTrackable(List trackedDownloads) + { + var untrackable = GetTrackedDownloads().ExceptBy(t => t.DownloadItem.DownloadId, trackedDownloads, t => t.DownloadItem.DownloadId, StringComparer.CurrentCulture).ToList(); + + foreach (var trackedDownload in untrackable) + { + trackedDownload.IsTrackable = false; + } + } + + private void LogItemChange(TrackedDownload trackedDownload, DownloadClientItem existingItem, DownloadClientItem downloadItem) + { if (existingItem == null || existingItem.Status != downloadItem.Status || existingItem.CanBeRemoved != downloadItem.CanBeRemoved || diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs new file mode 100644 index 000000000..76c926d5a --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Download.TrackedDownloads +{ + public class TrackedDownloadsRemovedEvent : IEvent + { + public List TrackedDownloads { get; private set; } + + public TrackedDownloadsRemovedEvent(List trackedDownloads) + { + TrackedDownloads = trackedDownloads; + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs index 11899124f..0f6265bc3 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser SeriesMetadata = true; } - [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox, HelpText = "series.xml")] public bool SeriesMetadata { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs index f0da481bf..8c2bcbf64 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -24,16 +24,16 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox EpisodeImages = true; } - [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.xml")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] + [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Series Title.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] + [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season ##.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename.jpg")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs index e010ff7e5..542bea22f 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -24,16 +24,16 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv EpisodeImages = true; } - [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.xml")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] + [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "folder.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] + [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\folder.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename.metathumb")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs index cd4b833ae..b2e024c3a 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -25,19 +25,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc EpisodeImages = true; } - [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "tvshow.nfo")] public bool SeriesMetadata { get; set; } - [FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Season##\\filename.nfo")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox)] + [FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "fanart.jpg, poster.jpg, banner.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "season##-poster.jpg, season##-banner.jpg, season-specials-poster.jpg, season-specials-banner.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox)] + [FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##\\filename-thumb.jpg")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataSectionType.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataSectionType.cs new file mode 100644 index 000000000..7f2251d85 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataSectionType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Extras.Metadata +{ + public static class MetadataSectionType + { + public const string Metadata = "metadata"; + public const string Image = "image"; + } +} diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 067af8923..ce461911e 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -16,6 +16,8 @@ namespace NzbDrone.Core.History List FindByEpisodeId(int episodeId); History MostRecentForDownloadId(string downloadId); List FindByDownloadId(string downloadId); + List GetBySeries(int seriesId, HistoryEventType? eventType); + List GetBySeason(int seriesId, int seasonNumber, HistoryEventType? eventType); List FindDownloadHistory(int idSeriesId, QualityModel quality); void DeleteForSeries(int seriesId); List Since(DateTime date, HistoryEventType? eventType); @@ -63,6 +65,36 @@ namespace NzbDrone.Core.History return Query.Where(h => h.DownloadId == downloadId); } + public List GetBySeries(int seriesId, HistoryEventType? eventType) + { + var query = Query.Where(h => h.SeriesId == seriesId); + + if (eventType.HasValue) + { + query.AndWhere(h => h.EventType == eventType); + } + + query.OrderByDescending(h => h.Date); + + return query; + } + + public List GetBySeason(int seriesId, int seasonNumber, HistoryEventType? eventType) + { + var query = Query.Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id) + .Where(h => h.SeriesId == seriesId) + .AndWhere(h => h.Episode.SeasonNumber == seasonNumber); + + if (eventType.HasValue) + { + query.AndWhere(h => h.EventType == eventType); + } + + query.OrderByDescending(h => h.Date); + + return query; + } + public List FindDownloadHistory(int idSeriesId, QualityModel quality) { return Query.Where(h => diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index d01fa2ba8..bd49021b2 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Marr.Data.QGen; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -11,7 +12,7 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.History @@ -24,6 +25,8 @@ namespace NzbDrone.Core.History List FindByEpisodeId(int episodeId); History MostRecentForDownloadId(string downloadId); History Get(int historyId); + List GetBySeries(int seriesId, HistoryEventType? eventType); + List GetBySeason(int seriesId, int seasonNumber, HistoryEventType? eventType); List Find(string downloadId, HistoryEventType eventType); List FindByDownloadId(string downloadId); List Since(DateTime date, HistoryEventType? eventType); @@ -71,6 +74,16 @@ namespace NzbDrone.Core.History return _historyRepository.Get(historyId); } + public List GetBySeries(int seriesId, HistoryEventType? eventType) + { + return _historyRepository.GetBySeries(seriesId, eventType); + } + + public List GetBySeason(int seriesId, int seasonNumber, HistoryEventType? eventType) + { + return _historyRepository.GetBySeason(seriesId, seasonNumber, eventType); + } + public List Find(string downloadId, HistoryEventType eventType) { return _historyRepository.FindByDownloadId(downloadId).Where(c => c.EventType == eventType).ToList(); @@ -246,6 +259,11 @@ namespace NzbDrone.Core.History _logger.Debug("Removing episode file from DB as part of cleanup routine, not creating history event."); return; } + else if (message.Reason == DeleteMediaFileReason.ManualOverride) + { + _logger.Debug("Removing episode file from DB as part of manual override of existing file, not creating history event."); + return; + } foreach (var episode in message.EpisodeFile.Episodes.Value) { diff --git a/src/NzbDrone.Core/IndexerSearch/CutoffUnmetEpisodeSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetEpisodeSearchCommand.cs new file mode 100644 index 000000000..2719bfaec --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetEpisodeSearchCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.IndexerSearch +{ + public class CutoffUnmetEpisodeSearchCommand : Command + { + public int? SeriesId { get; set; } + + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public CutoffUnmetEpisodeSearchCommand() + { + } + + public CutoffUnmetEpisodeSearchCommand(int seriesId) + { + SeriesId = seriesId; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index 6762fbaa2..6fc423a15 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; @@ -13,23 +14,28 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.IndexerSearch { - public class EpisodeSearchService : IExecute, IExecute + public class EpisodeSearchService : IExecute, + IExecute, + IExecute { private readonly ISearchForNzb _nzbSearchService; private readonly IProcessDownloadDecisions _processDownloadDecisions; private readonly IEpisodeService _episodeService; + private readonly IEpisodeCutoffService _episodeCutoffService; private readonly IQueueService _queueService; private readonly Logger _logger; public EpisodeSearchService(ISearchForNzb nzbSearchService, IProcessDownloadDecisions processDownloadDecisions, IEpisodeService episodeService, + IEpisodeCutoffService episodeCutoffService, IQueueService queueService, Logger logger) { _nzbSearchService = nzbSearchService; _processDownloadDecisions = processDownloadDecisions; _episodeService = episodeService; + _episodeCutoffService = episodeCutoffService; _queueService = queueService; _logger = logger; } @@ -107,17 +113,17 @@ namespace NzbDrone.Core.IndexerSearch else { - episodes = _episodeService.EpisodesWithoutFiles(new PagingSpec - { - Page = 1, - PageSize = 100000, - SortDirection = SortDirection.Ascending, - SortKey = "Id", - FilterExpression = - v => - v.Monitored == true && - v.Series.Monitored == true - }).Records.ToList(); + var pagingSpec = new PagingSpec + { + Page = 1, + PageSize = 100000, + SortDirection = SortDirection.Ascending, + SortKey = "Id" + }; + + pagingSpec.FilterExpressions.Add(v => v.Monitored == true &&v.Series.Monitored == true); + + episodes = _episodeService.EpisodesWithoutFiles(pagingSpec).Records.ToList(); } var queue = _queueService.GetQueue().Select(q => q.Episode.Id); @@ -125,5 +131,42 @@ namespace NzbDrone.Core.IndexerSearch SearchForMissingEpisodes(missing, message.Trigger == CommandTrigger.Manual); } + + public void Execute(CutoffUnmetEpisodeSearchCommand message) + { + Expression> filterExpression; + + if (message.SeriesId.HasValue) + { + filterExpression = v => + v.SeriesId == message.SeriesId.Value && + v.Monitored == true && + v.Series.Monitored == true; + } + + else + { + filterExpression = v => + v.Monitored == true && + v.Series.Monitored == true; + } + + var pagingSpec = new PagingSpec + { + Page = 1, + PageSize = 100000, + SortDirection = SortDirection.Ascending, + SortKey = "Id" + }; + + pagingSpec.FilterExpressions.Add(filterExpression); + + var episodes = _episodeCutoffService.EpisodesWhereCutoffUnmet(pagingSpec).Records.ToList(); + + var queue = _queueService.GetQueue().Select(q => q.Episode.Id); + var missing = episodes.Where(e => !queue.Contains(e.Id)).ToList(); + + SearchForMissingEpisodes(missing, message.Trigger == CommandTrigger.Manual); + } } } diff --git a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs index babdd3beb..27126dda9 100644 --- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs +++ b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Indexers.BitMeTv [FieldDefinition(3, Label = "Cookie", HelpText = "BitMeTv uses a login cookie needed to access the rss, you'll have to retrieve it via a browser.")] public string Cookie { get; set; } - [FieldDefinition(4, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(4, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(5)] diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index 7d3853216..8b756931a 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet [FieldDefinition(1, Label = "API Key")] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(3)] diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index 48fd48783..d68631f04 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.HDBits [FieldDefinition(2, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] public string BaseUrl { get; set; } - [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(4)] diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index 52255d8fb..d0831d952 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents [FieldDefinition(0, Label = "Feed URL", HelpText = "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)")] public string BaseUrl { get; set; } - [FieldDefinition(1, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(1, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(2)] diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 9213084e5..b8d486d92 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; using System.Text.RegularExpressions; @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(1, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")] public string AdditionalParameters { get; set; } - [FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(3)] diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs index 9a363741a..53be8b974 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.Rarbg [FieldDefinition(2, Type = FieldType.Captcha, Label = "CAPTCHA Token", HelpText = "CAPTCHA Clearance token used to handle CloudFlare Anti-DDOS measures on shared-ip VPNs.")] public string CaptchaToken { get; set; } - [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(4)] diff --git a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs index 815cee782..2c19b6f32 100644 --- a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs +++ b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -57,10 +57,10 @@ namespace NzbDrone.Core.Indexers [FieldDefinition(0, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is download client's default", Advanced = true)] public double? SeedRatio { get; set; } - [FieldDefinition(1, Type = FieldType.Textbox, Label = "Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)] + [FieldDefinition(1, Type = FieldType.Number, Label = "Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)] public int? SeedTime { get; set; } - [FieldDefinition(2, Type = FieldType.Textbox, Label = "Season-Pack Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "Season-Pack Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)] public int? SeasonPackSeedTime { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 93e9bbebd..ac37fba1c 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Allow Zero Size", HelpText="Enabling this will allow you to use feeds that don't specify release size, but be careful, size related checks will not be performed.")] public bool AllowZeroSize { get; set; } - [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(4)] diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index 0a0984647..5c844dcfe 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech [FieldDefinition(1, Label = "API Key")] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(3)] diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 0e0437108..dc8e8eab2 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Torznab MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(6, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(6, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(7)] diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index e53bea79a..c9ad6fbe8 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NLog.Config; diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs index ab2f80480..dfa5f0d3d 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs @@ -5,10 +5,6 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class DownloadedEpisodesScanCommand : Command { - public override bool SendUpdatesToClient => SendUpdates; - - public bool SendUpdates { get; set; } - // Properties used by third-party apps, do not modify. public string Path { get; set; } public string DownloadClientId { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs index a2bcda88c..52c030fb6 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.Commands public RenameSeriesCommand() { + SeriesIds = new List(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs b/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs index 918eedc31..7c9f6eee9 100644 --- a/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs +++ b/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs @@ -1,10 +1,11 @@ -namespace NzbDrone.Core.MediaFiles +namespace NzbDrone.Core.MediaFiles { public enum DeleteMediaFileReason { MissingFromDisk, Manual, Upgrade, - NoLinkedEpisodes + NoLinkedEpisodes, + ManualOverride } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index d4f3b97a9..eb950d1c2 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; @@ -38,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IConfigService _configService; private readonly ISeriesService _seriesService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; + private readonly IRootFolderService _rootFolderService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -47,6 +49,7 @@ namespace NzbDrone.Core.MediaFiles IConfigService configService, ISeriesService seriesService, IMediaFileTableCleanupService mediaFileTableCleanupService, + IRootFolderService rootFolderService, IEventAggregator eventAggregator, Logger logger) { @@ -56,6 +59,7 @@ namespace NzbDrone.Core.MediaFiles _configService = configService; _seriesService = seriesService; _mediaFileTableCleanupService = mediaFileTableCleanupService; + _rootFolderService = rootFolderService; _eventAggregator = eventAggregator; _logger = logger; } @@ -65,7 +69,7 @@ namespace NzbDrone.Core.MediaFiles public void Scan(Series series) { - var rootFolder = _diskProvider.GetParentFolder(series.Path); + var rootFolder = _rootFolderService.GetBestRootFolderPath(series.Path); if (!_diskProvider.FolderExists(rootFolder)) { @@ -81,7 +85,7 @@ namespace NzbDrone.Core.MediaFiles return; } - _logger.ProgressInfo("Scanning disk for {0}", series.Title); + _logger.ProgressInfo("Scanning {0}", series.Title); if (!_diskProvider.FolderExists(series.Path)) { @@ -143,6 +147,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); _logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path); + return mediaFileList.ToArray(); } @@ -158,6 +163,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); _logger.Debug("{0} non-video files were found in {1}", mediaFileList.Count, path); + return mediaFileList.ToArray(); } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 7f77c1286..67d20e61c 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index fb88ad8bf..bd4da4d18 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -111,6 +111,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport else { episodeFile.RelativePath = localEpisode.Series.Path.GetRelativePath(episodeFile.Path); + + // Delete existing files from the DB mapped to this path + var previousFiles = _mediaFileService.GetFilesWithRelativePath(localEpisode.Series.Id, episodeFile.RelativePath); + + foreach (var previousFile in previousFiles) + { + _mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride); + } } _mediaFileService.Add(episodeFile); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 8507bd3c4..1dddf8fbc 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { List GetImportDecisions(List videoFiles, Series series); List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource); + List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles); } public class ImportDecisionMaker : IMakeImportDecision @@ -50,7 +51,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource) { - var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); + return GetImportDecisions(videoFiles, series, downloadClientItem, folderInfo, sceneSource, true); + } + + public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles) + { + var newFiles = filterExistingFiles ? _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series) : videoFiles.ToList(); _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs index 318b7ff7d..849627d19 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual @@ -12,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public int SeriesId { get; set; } public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } public bool Equals(ManualImportFile other) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index 6f055d7d2..d9990483d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -16,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public int? SeasonNumber { get; set; } public List Episodes { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index b3110a540..2048b82e8 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { public interface IManualImportService { - List GetMediaFiles(string path, string downloadId); + List GetMediaFiles(string path, string downloadId, bool filterExistingFiles); } public class ManualImportService : IExecute, IManualImportService @@ -69,7 +69,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual _logger = logger; } - public List GetMediaFiles(string path, string downloadId) + public List GetMediaFiles(string path, string downloadId, bool filterExistingFiles) { if (downloadId.IsNotNullOrWhiteSpace()) { @@ -94,10 +94,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual return new List { ProcessFile(rootFolder, rootFolder, path, downloadId) }; } - return ProcessFolder(path, path, downloadId); + return ProcessFolder(path, path, downloadId, filterExistingFiles); } - private List ProcessFolder(string rootFolder, string baseFolder, string downloadId) + private List ProcessFolder(string rootFolder, string baseFolder, string downloadId, bool filterExistingFiles) { DownloadClientItem downloadClientItem = null; var directoryInfo = new DirectoryInfo(baseFolder); @@ -120,14 +120,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var subfolders = _diskScanService.FilterFiles(baseFolder, _diskProvider.GetDirectories(baseFolder)); var processedFiles = files.Select(file => ProcessFile(rootFolder, baseFolder, file, downloadId)); - var processedFolders = subfolders.SelectMany(subfolder => ProcessFolder(rootFolder, subfolder, downloadId)); + var processedFolders = subfolders.SelectMany(subfolder => ProcessFolder(rootFolder, subfolder, downloadId, filterExistingFiles)); return processedFiles.Concat(processedFolders).Where(i => i != null).ToList(); } var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); var seriesFiles = _diskScanService.GetVideoFiles(baseFolder).ToList(); - var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder)); + var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder), filterExistingFiles); return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList(); } @@ -169,6 +169,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var localEpisode = new LocalEpisode(); localEpisode.Path = file; localEpisode.Quality = QualityParser.ParseQuality(file); + localEpisode.Language = LanguageParser.ParseLanguage(file); localEpisode.Size = _diskProvider.GetFileSize(file); return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), rootFolder, downloadId, null); @@ -232,6 +233,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual } item.Quality = decision.LocalEpisode.Quality; + item.Language = decision.LocalEpisode.Language; item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); item.Rejections = decision.Rejections; @@ -263,6 +265,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual FileEpisodeInfo = fileEpisodeInfo, Path = file.Path, Quality = file.Quality, + Language = file.Language, Series = series, Size = 0 }; diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 206942356..7b3f5cf7b 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.MediaFiles List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); List GetFilesWithoutMediaInfo(); + List GetFilesWithRelativePath(int seriesId, string relativePath); } @@ -36,5 +37,12 @@ namespace NzbDrone.Core.MediaFiles { return Query.Where(c => c.MediaInfo == null).ToList(); } + + public List GetFilesWithRelativePath(int seriesId, string relativePath) + { + return Query.Where(c => c.SeriesId == seriesId) + .AndWhere(c => c.RelativePath == relativePath) + .ToList(); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index ca3f68ce2..93fa6aa19 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -14,14 +14,16 @@ namespace NzbDrone.Core.MediaFiles { EpisodeFile Add(EpisodeFile episodeFile); void Update(EpisodeFile episodeFile); + void Update(List episodeFiles); void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFiles(IEnumerable ids); List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, Series series); EpisodeFile Get(int id); List Get(IEnumerable ids); - + List GetFilesWithRelativePath(int seriesId, string relativePath); } public class MediaFileService : IMediaFileService, IHandleAsync @@ -49,6 +51,11 @@ namespace NzbDrone.Core.MediaFiles _mediaFileRepository.Update(episodeFile); } + public void Update(List episodeFiles) + { + _mediaFileRepository.UpdateMany(episodeFiles); + } + public void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason) { //Little hack so we have the episodes and series attached for the event consumers @@ -69,6 +76,11 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); } + public List GetFiles(IEnumerable ids) + { + return _mediaFileRepository.Get(ids).ToList(); + } + public List GetFilesWithoutMediaInfo() { return _mediaFileRepository.GetFilesWithoutMediaInfo(); @@ -93,10 +105,15 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.Get(ids).ToList(); } + public List GetFilesWithRelativePath(int seriesId, string relativePath) + { + return _mediaFileRepository.GetFilesWithRelativePath(seriesId, relativePath); + } + public void HandleAsync(SeriesDeletedEvent message) { var files = GetFilesBySeries(message.Series.Id); _mediaFileRepository.DeleteMany(files); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index c6bf6a977..4abc772c8 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -367,8 +367,6 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo try { - Logger.Debug("Formatting audio channels using 'AudioChannelPositions', with a value of: '{0}'", audioChannelPositions); - if (audioChannelPositions.Contains("+")) { return audioChannelPositions.Split('+') @@ -385,7 +383,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo } catch (Exception e) { - Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositions'"); + Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositions', with a value of: '{0}'", audioChannelPositions); } return null; @@ -403,13 +401,11 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo try { - Logger.Debug("Formatiting audio channels using 'AudioChannelPositionsText', with a value of: '{0}'", audioChannelPositionsText); - return mediaInfo.AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? audioChannels - 1 + 0.1m : audioChannels; } catch (Exception e) { - Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText'"); + Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText', with a value of: '{0}'", audioChannelPositionsText); } return null; @@ -421,11 +417,11 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo if (mediaInfo.SchemaRevision >= 3) { - Logger.Debug("Formatiting audio channels using 'AudioChannels', with a value of: '{0}'", audioChannels); - return audioChannels; } + Logger.Warn("Unable to format audio channels using 'AudioChannels', with a value of: '{0}'", audioChannels); + return null; } diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 20becd1f0..0e82d8345 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -4,15 +4,28 @@ namespace NzbDrone.Core.Messaging.Commands { public abstract class Command { - public virtual bool SendUpdatesToClient => false; + private bool _sendUpdatesToClient; + + public virtual bool SendUpdatesToClient + { + get + { + return _sendUpdatesToClient; + } + + set + { + _sendUpdatesToClient = value; + } + } public virtual bool UpdateScheduledTask => true; - public virtual string CompletionMessage => "Completed"; public string Name { get; private set; } public DateTime? LastExecutionTime { get; set; } public CommandTrigger Trigger { get; set; } + public bool SuppressMessages { get; set; } public Command() { diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs index 7b87af393..c64adfe80 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Messaging.Commands { public interface IManageCommandQueue { + List PushMany(List commands) where TCommand : Command; CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command; CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified); IEnumerable Queue(CancellationToken cancellationToken); @@ -50,6 +51,47 @@ namespace NzbDrone.Core.Messaging.Commands _commandQueue = new BlockingCollection(new CommandQueue()); } + public List PushMany(List commands) where TCommand : Command + { + _logger.Trace("Publishing {0} commands", commands.Count); + + var commandModels = new List(); + var existingCommands = _commandCache.Values.Where(q => q.Status == CommandStatus.Queued || + q.Status == CommandStatus.Started).ToList(); + + foreach (var command in commands) + { + var existing = existingCommands.SingleOrDefault(c => c.Name == command.Name && CommandEqualityComparer.Instance.Equals(c.Body, command)); + + if (existing != null) + { + continue; + } + + var commandModel = new CommandModel + { + Name = command.Name, + Body = command, + QueuedAt = DateTime.UtcNow, + Trigger = CommandTrigger.Unspecified, + Priority = CommandPriority.Normal, + Status = CommandStatus.Queued + }; + + commandModels.Add(commandModel); + } + + _repo.InsertMany(commandModels); + + foreach (var commandModel in commandModels) + { + _commandCache.Set(commandModel.Id.ToString(), commandModel); + _commandQueue.Add(commandModel); + } + + return commandModels; + } + public CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command { Ensure.That(command, () => command).IsNotNull(); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 5be8e1611..1d4b39820 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -17,14 +17,16 @@ namespace NzbDrone.Core.MetadataSource.SkyHook { private readonly IHttpClient _httpClient; private readonly Logger _logger; - + private readonly ISeriesService _seriesService; private readonly IHttpRequestBuilderFactory _requestBuilder; - public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger) + public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ISeriesService seriesService, Logger logger) { _httpClient = httpClient; _requestBuilder = requestBuilder.SkyHookTvdb; _logger = logger; + _seriesService = seriesService; + _requestBuilder = requestBuilder.SkyHookTvdb; } public Tuple> GetSeriesInfo(int tvdbSeriesId) @@ -76,6 +78,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook try { + var existingSeries = _seriesService.FindByTvdbId(tvdbId); + if (existingSeries != null) + { + return new List { existingSeries }; + } + return new List { GetSeriesInfo(tvdbId).Item1 }; } catch (SeriesNotFoundException) @@ -91,7 +99,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var httpResponse = _httpClient.Get>(httpRequest); - return httpResponse.Resource.SelectList(MapSeries); + return httpResponse.Resource.SelectList(MapSearhResult); } catch (HttpException) { @@ -104,8 +112,22 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - private static Series MapSeries(ShowResource show) + private Series MapSearhResult(ShowResource show) { + var series = _seriesService.FindByTvdbId(show.TvdbId); + + if (series == null) + { + series = MapSeries(show); + } + + return series; + } + + private Series MapSeries(ShowResource show) + { + + var series = new Series(); series.TvdbId = show.TvdbId; diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index 5c2d10045..9097b9b12 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -1,15 +1,9 @@ -using System.Collections.Generic; -using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Notifications { public class NotificationDefinition : ProviderDefinition { - public NotificationDefinition() - { - Tags = new HashSet(); - } - public bool OnGrab { get; set; } public bool OnDownload { get; set; } public bool OnUpgrade { get; set; } @@ -18,7 +12,6 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnDownload { get; set; } public bool SupportsOnUpgrade { get; set; } public bool SupportsOnRename { get; set; } - public HashSet Tags { get; set; } public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade); } diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 6820aee8c..c964e12dc 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -69,22 +69,19 @@ namespace NzbDrone.Core.Notifications private bool ShouldHandleSeries(ProviderDefinition definition, Series series) { - var notificationDefinition = (NotificationDefinition)definition; - - if (notificationDefinition.Tags.Empty()) + if (definition.Tags.Empty()) { _logger.Debug("No tags set for this notification."); return true; } - if (notificationDefinition.Tags.Intersect(series.Tags).Any()) + if (definition.Tags.Intersect(series.Tags).Any()) { - _logger.Debug("Notification and series have one or more matching tags."); + _logger.Debug("Notification and series have one or more intersecting tags."); return true; } - //TODO: this message could be more clear - _logger.Debug("{0} does not have any tags that match {1}'s tags", notificationDefinition.Name, series.Title); + _logger.Debug("{0} does not have any intersecting tags with {1}. Notification will not be sent.", definition.Name, series.Title); return false; } diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs index 36b18285a..4e17b4ab1 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Twitter public TwitterSettings() { DirectMessage = true; - AuthorizeNotification = "step1"; + AuthorizeNotification = "startOAuth"; } [FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")] @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Notifications.Twitter [FieldDefinition(5, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")] public bool DirectMessage { get; set; } - [FieldDefinition(6, Label = "Connect to twitter", Type = FieldType.Action)] + [FieldDefinition(6, Label = "Connect to Twitter", Type = FieldType.OAuth)] public string AuthorizeNotification { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e92508e37..9c1904db0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -146,6 +146,11 @@ + + + + + @@ -235,6 +240,7 @@ + @@ -523,6 +529,7 @@ + @@ -642,6 +649,7 @@ + @@ -982,6 +990,8 @@ + + @@ -1138,6 +1148,7 @@ + @@ -1164,6 +1175,7 @@ + @@ -1175,6 +1187,7 @@ + @@ -1189,6 +1202,7 @@ + diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs index 7afffee41..282a5c04b 100644 --- a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs @@ -13,8 +13,10 @@ namespace NzbDrone.Core.Profiles.Delay void Delete(int id); List All(); DelayProfile Get(int id); + List AllForTag(int tagId); List AllForTags(HashSet tagIds); DelayProfile BestForTags(HashSet tagIds); + List Reorder(int id, int? afterId); } public class DelayProfileService : IDelayProfileService @@ -29,9 +31,12 @@ namespace NzbDrone.Core.Profiles.Delay } public DelayProfile Add(DelayProfile profile) - { + { + profile.Order = _repo.Count(); + var result = _repo.Insert(profile); _bestForTagsCache.Clear(); + return result; } @@ -69,9 +74,15 @@ namespace NzbDrone.Core.Profiles.Delay return _repo.Get(id); } + public List AllForTag(int tagId) + { + return All().Where(r => r.Tags.Contains(tagId)) + .ToList(); + } + public List AllForTags(HashSet tagIds) { - return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); + return All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); } public DelayProfile BestForTags(HashSet tagIds) @@ -86,5 +97,72 @@ namespace NzbDrone.Core.Profiles.Delay .Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()) .OrderBy(d => d.Order).First(); } + + public List Reorder(int id, int? afterId) + { + var all = All().OrderBy(d => d.Order) + .ToList(); + + var moving = all.SingleOrDefault(d => d.Id == id); + var after = afterId.HasValue ? all.SingleOrDefault(d => d.Id == afterId) : null; + + if (moving == null) + { + // TODO: This should throw + return all; + } + + var afterOrder = GetAfterOrder(moving, after); + var afterCount = afterOrder + 2; + var movingOrder = moving.Order; + + foreach (var delayProfile in all) + { + if (delayProfile.Id == 1) + { + continue; + } + + if (delayProfile.Id == id) + { + delayProfile.Order = afterOrder + 1; + } + + else if (delayProfile.Id == after?.Id) + { + delayProfile.Order = afterOrder; + } + + else if (delayProfile.Order > afterOrder) + { + delayProfile.Order = afterCount; + afterCount++; + } + + else if (delayProfile.Order > movingOrder) + { + delayProfile.Order--; + } + } + + _repo.UpdateMany(all); + + return All(); + } + + private int GetAfterOrder(DelayProfile moving, DelayProfile after) + { + if (after == null) + { + return 0; + } + + if (moving.Order < after.Order) + { + return after.Order - 1; + } + + return after.Order; + } } } diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index d2fc46e3c..2b53417ff 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Qualities public interface IQualityDefinitionService { void Update(QualityDefinition qualityDefinition); + void UpdateMany(List qualityDefinitions); List All(); QualityDefinition GetById(int id); QualityDefinition Get(Quality quality); @@ -41,6 +42,11 @@ namespace NzbDrone.Core.Qualities _cache.Clear(); } + public void UpdateMany(List qualityDefinitions) + { + _repo.UpdateMany(qualityDefinitions); + } + public List All() { return GetAll().Values.OrderBy(d => d.Weight).ToList(); diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index b4954207a..a4c17fc39 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -1,10 +1,11 @@ using System; +using System.Linq; using Newtonsoft.Json; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities { - public class QualityModel : IEmbeddedDocument, IEquatable + public class QualityModel : IEmbeddedDocument, IEquatable, IComparable { public Quality Quality { get; set; } public Revision Revision { get; set; } @@ -40,6 +41,45 @@ namespace NzbDrone.Core.Qualities } } + public int CompareTo(object obj) + { + var other = (QualityModel) obj; + var definition = Quality.DefaultQualityDefinitions.First(q => q.Quality == Quality); + var otherDefinition = Quality.DefaultQualityDefinitions.First(q => q.Quality == other.Quality); + + if (definition.Weight > otherDefinition.Weight) + { + return 1; + } + + if(definition.Weight < otherDefinition.Weight) + { + return -1; + } + + if (Revision.Real > other.Revision.Real) + { + return 1; + } + + if (Revision.Real < other.Revision.Real) + { + return -1; + } + + if (Revision.Version > other.Revision.Version) + { + return 1; + } + + if (Revision.Version < other.Revision.Version) + { + return -1; + } + + return 0; + } + public bool Equals(QualityModel other) { if (ReferenceEquals(null, other)) return false; diff --git a/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs b/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs new file mode 100644 index 000000000..e8c52e1ab --- /dev/null +++ b/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Queue +{ + public class EstimatedCompletionTimeComparer : IComparer + { + public int Compare(DateTime? x, DateTime? y) + { + if (!x.HasValue && !y.HasValue) + { + return 0; + } + + if (!x.HasValue && y.HasValue) + { + return 1; + } + + if (x.HasValue && !y.HasValue) + { + return -1; + } + + if (x.Value > y.Value) + { + return 1; + } + + if (x.Value < y.Value) + { + return -1; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 7164a17ae..13629d363 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -25,5 +25,8 @@ namespace NzbDrone.Core.Queue public string DownloadId { get; set; } public RemoteEpisode RemoteEpisode { get; set; } public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + public string ErrorMessage { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 264645ed8..0f417837c 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Queue { List GetQueue(); Queue Find(int id); + void Remove(int id); } public class QueueService : IQueueService, IHandle @@ -34,6 +35,11 @@ namespace NzbDrone.Core.Queue return _queue.SingleOrDefault(q => q.Id == id); } + public void Remove(int id) + { + _queue.Remove(Find(id)); + } + public void Handle(TrackedDownloadRefreshedEvent message) { _queue = message.TrackedDownloads.OrderBy(c => c.DownloadItem.RemainingTime).SelectMany(MapQueue) @@ -72,9 +78,12 @@ namespace NzbDrone.Core.Queue Status = trackedDownload.DownloadItem.Status.ToString(), TrackedDownloadStatus = trackedDownload.Status.ToString(), StatusMessages = trackedDownload.StatusMessages.ToList(), + ErrorMessage = trackedDownload.DownloadItem.Message, RemoteEpisode = trackedDownload.RemoteEpisode, DownloadId = trackedDownload.DownloadItem.DownloadId, - Protocol = trackedDownload.Protocol + Protocol = trackedDownload.Protocol, + DownloadClient = trackedDownload.DownloadItem.DownloadClient, + Indexer = trackedDownload.Indexer }; if (queue.Timeleft.HasValue) diff --git a/src/NzbDrone.Core/Queue/TimeleftComparer.cs b/src/NzbDrone.Core/Queue/TimeleftComparer.cs new file mode 100644 index 000000000..2c051deeb --- /dev/null +++ b/src/NzbDrone.Core/Queue/TimeleftComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Queue +{ + public class TimeleftComparer : IComparer + { + public int Compare(TimeSpan? x, TimeSpan? y) + { + if (!x.HasValue && !y.HasValue) + { + return 0; + } + + if (!x.HasValue && y.HasValue) + { + return 1; + } + + if (x.HasValue && !y.HasValue) + { + return -1; + } + + if (x.Value > y.Value) + { + return 1; + } + + if (x.Value < y.Value) + { + return -1; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Core/Restrictions/RestrictionService.cs b/src/NzbDrone.Core/Restrictions/RestrictionService.cs index 5d7cfba8d..90b9d4a6f 100644 --- a/src/NzbDrone.Core/Restrictions/RestrictionService.cs +++ b/src/NzbDrone.Core/Restrictions/RestrictionService.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Restrictions public List AllForTag(int tagId) { - return _repo.All().Where(r => r.Tags.Contains(tagId) || r.Tags.Empty()).ToList(); + return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList(); } public List AllForTags(HashSet tagIds) diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 99d1499f4..7f0d971bd 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -6,7 +6,6 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Tv; namespace NzbDrone.Core.RootFolders @@ -18,6 +17,7 @@ namespace NzbDrone.Core.RootFolders RootFolder Add(RootFolder rootDir); void Remove(int id); RootFolder Get(int id); + string GetBestRootFolderPath(string path); } public class RootFolderService : IRootFolderService @@ -25,7 +25,6 @@ namespace NzbDrone.Core.RootFolders private readonly IRootFolderRepository _rootFolderRepository; private readonly IDiskProvider _diskProvider; private readonly ISeriesRepository _seriesRepository; - private readonly IConfigService _configService; private readonly Logger _logger; private static readonly HashSet SpecialFolders = new HashSet @@ -45,13 +44,11 @@ namespace NzbDrone.Core.RootFolders public RootFolderService(IRootFolderRepository rootFolderRepository, IDiskProvider diskProvider, ISeriesRepository seriesRepository, - IConfigService configService, Logger logger) { _rootFolderRepository = rootFolderRepository; _diskProvider = diskProvider; _seriesRepository = seriesRepository; - _configService = configService; _logger = logger; } @@ -173,5 +170,19 @@ namespace NzbDrone.Core.RootFolders rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path); return rootFolder; } + + public string GetBestRootFolderPath(string path) + { + var possibleRootFolder = All().Where(r => r.Path.IsParentPath(path)) + .OrderByDescending(r => r.Path.Length) + .FirstOrDefault(); + + if (possibleRootFolder == null) + { + return Path.GetDirectoryName(path); + } + + return possibleRootFolder.Path; + } } } diff --git a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs index 3b3731ed5..0eb2d63c0 100644 --- a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.SeriesStats public string PreviousAiringString { get; set; } public int EpisodeFileCount { get; set; } public int EpisodeCount { get; set; } + public int AvailableEpisodeCount { get; set; } public int TotalEpisodeCount { get; set; } public long SizeOnDisk { get; set; } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs index 73e4e8b4b..e5d50a6bb 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs @@ -58,7 +58,8 @@ namespace NzbDrone.Core.SeriesStats (SELECT Episodes.SeriesId, Episodes.SeasonNumber, - SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS TotalEpisodeCount, + COUNT(*) AS TotalEpisodeCount, + SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS AvailableEpisodeCount, SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString, diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs new file mode 100644 index 000000000..cd223f81c --- /dev/null +++ b/src/NzbDrone.Core/Tags/TagDetails.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Tags +{ + public class TagDetails : ModelBase + { + public string Label { get; set; } + public List SeriesIds { get; set; } + public List NotificationIds { get; set; } + public List RestrictionIds { get; set; } + public List DelayProfileIds { get; set; } + } +} diff --git a/src/NzbDrone.Core/Tags/TagRepository.cs b/src/NzbDrone.Core/Tags/TagRepository.cs index 500502843..2921ca7c8 100644 --- a/src/NzbDrone.Core/Tags/TagRepository.cs +++ b/src/NzbDrone.Core/Tags/TagRepository.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Tags public interface ITagRepository : IBasicRepository { Tag GetByLabel(string label); + Tag FindByLabel(string label); } public class TagRepository : BasicRepository, ITagRepository @@ -28,5 +29,10 @@ namespace NzbDrone.Core.Tags return model; } + + public Tag FindByLabel(string label) + { + return Query.Where(c => c.Label == label).SingleOrDefault(); + } } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index b7637a6f8..fdf0129b1 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -1,6 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Tags { @@ -8,6 +12,8 @@ namespace NzbDrone.Core.Tags { Tag GetTag(int tagId); Tag GetTag(string tag); + TagDetails Details(int tagId); + List Details(); List All(); Tag Add(Tag tag); Tag Update(Tag tag); @@ -18,11 +24,24 @@ namespace NzbDrone.Core.Tags { private readonly ITagRepository _repo; private readonly IEventAggregator _eventAggregator; + private readonly IDelayProfileService _delayProfileService; + private readonly INotificationFactory _notificationFactory; + private readonly IRestrictionService _restrictionService; + private readonly ISeriesService _seriesService; - public TagService(ITagRepository repo, IEventAggregator eventAggregator) + public TagService(ITagRepository repo, + IEventAggregator eventAggregator, + IDelayProfileService delayProfileService, + INotificationFactory notificationFactory, + IRestrictionService restrictionService, + ISeriesService seriesService) { _repo = repo; _eventAggregator = eventAggregator; + _delayProfileService = delayProfileService; + _notificationFactory = notificationFactory; + _restrictionService = restrictionService; + _seriesService = seriesService; } public Tag GetTag(int tagId) @@ -42,6 +61,52 @@ namespace NzbDrone.Core.Tags } } + public TagDetails Details(int tagId) + { + var tag = GetTag(tagId); + var delayProfiles = _delayProfileService.AllForTag(tagId); + var notifications = _notificationFactory.AllForTag(tagId); + var restrictions = _restrictionService.AllForTag(tagId); + var series = _seriesService.AllForTag(tagId); + + return new TagDetails + { + Id = tagId, + Label = tag.Label, + DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(), + NotificationIds = notifications.Select(c => c.Id).ToList(), + RestrictionIds = restrictions.Select(c => c.Id).ToList(), + SeriesIds = series.Select(c => c.Id).ToList() + }; + } + + public List Details() + { + var tags = All(); + var delayProfiles = _delayProfileService.All(); + var notifications = _notificationFactory.All(); + var restrictions = _restrictionService.All(); + var series = _seriesService.GetAllSeries(); + + var details = new List(); + + foreach (var tag in tags) + { + details.Add(new TagDetails + { + Id = tag.Id, + Label = tag.Label, + DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + SeriesIds = series.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList() + } + ); + } + + return details; + } + public List All() { return _repo.All().OrderBy(t => t.Label).ToList(); @@ -49,7 +114,12 @@ namespace NzbDrone.Core.Tags public Tag Add(Tag tag) { - //TODO: check for duplicate tag by label and return that tag instead? + var existingTag = _repo.FindByLabel(tag.Label); + + if (existingTag != null) + { + return existingTag; + } tag.Label = tag.Label.ToLowerInvariant(); diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs index ce6519e1b..2627cec14 100644 --- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs @@ -20,5 +20,6 @@ namespace NzbDrone.Core.ThingiProvider TProvider GetInstance(TProviderDefinition definition); ValidationResult Test(TProviderDefinition definition); object RequestAction(TProviderDefinition definition, string action, IDictionary query); + List AllForTag(int tagId); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs index 45bd5a25a..d83c7dfda 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs @@ -1,9 +1,15 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ThingiProvider { public abstract class ProviderDefinition : ModelBase { + protected ProviderDefinition() + { + Tags = new HashSet(); + } + private IProviderConfig _settings; public string Name { get; set; } @@ -12,6 +18,7 @@ namespace NzbDrone.Core.ThingiProvider public string ConfigContract { get; set; } public virtual bool Enable { get; set; } public ProviderMessage Message { get; set; } + public HashSet Tags { get; set; } public IProviderConfig Settings { diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index 57c6d4e88..db98416d3 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -167,5 +167,11 @@ namespace NzbDrone.Core.ThingiProvider _providerRepository.Delete(invalidDefinition); } } + + public List AllForTag(int tagId) + { + return All().Where(p => p.Tags.Contains(tagId)) + .ToList(); + } } } diff --git a/src/NzbDrone.Core/Tv/AddSeriesService.cs b/src/NzbDrone.Core/Tv/AddSeriesService.cs index f8a049777..a44c79a7a 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesService.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesService.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Tv public interface IAddSeriesService { Series AddSeries(Series newSeries); + List AddSeries(List newSeries); } public class AddSeriesService : IAddSeriesService @@ -44,23 +45,7 @@ namespace NzbDrone.Core.Tv Ensure.That(newSeries, () => newSeries).IsNotNull(); newSeries = AddSkyhookData(newSeries); - - if (string.IsNullOrWhiteSpace(newSeries.Path)) - { - var folderName = _fileNameBuilder.GetSeriesFolder(newSeries); - newSeries.Path = Path.Combine(newSeries.RootFolderPath, folderName); - } - - newSeries.CleanTitle = newSeries.Title.CleanSeriesTitle(); - newSeries.SortTitle = SeriesTitleNormalizer.Normalize(newSeries.Title, newSeries.TvdbId); - newSeries.Added = DateTime.UtcNow; - - var validationResult = _addSeriesValidator.Validate(newSeries); - - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } + newSeries = SetPropertiesAndValidate(newSeries); _logger.Info("Adding Series {0} Path: [{1}]", newSeries, newSeries.Path); _seriesService.AddSeries(newSeries); @@ -68,6 +53,23 @@ namespace NzbDrone.Core.Tv return newSeries; } + public List AddSeries(List newSeries) + { + var added = DateTime.UtcNow; + var seriesToAdd = new List(); + + foreach (var s in newSeries) + { + // TODO: Verify if adding skyhook data will be slow + var series = AddSkyhookData(s); + series = SetPropertiesAndValidate(series); + series.Added = added; + seriesToAdd.Add(series); + } + + return _seriesService.AddSeries(seriesToAdd); + } + private Series AddSkyhookData(Series newSeries) { Tuple> tuple; @@ -95,5 +97,27 @@ namespace NzbDrone.Core.Tv return series; } + + private Series SetPropertiesAndValidate(Series newSeries) + { + if (string.IsNullOrWhiteSpace(newSeries.Path)) + { + var folderName = _fileNameBuilder.GetSeriesFolder(newSeries); + newSeries.Path = Path.Combine(newSeries.RootFolderPath, folderName); + } + + newSeries.CleanTitle = newSeries.Title.CleanSeriesTitle(); + newSeries.SortTitle = SeriesTitleNormalizer.Normalize(newSeries.Title, newSeries.TvdbId); + newSeries.Added = DateTime.UtcNow; + + var validationResult = _addSeriesValidator.Validate(newSeries); + + if (!validationResult.IsValid) + { + throw new ValidationException(validationResult.Errors); + } + + return newSeries; + } } } diff --git a/src/NzbDrone.Core/Tv/Commands/BulkMoveSeriesCommand.cs b/src/NzbDrone.Core/Tv/Commands/BulkMoveSeriesCommand.cs new file mode 100644 index 000000000..d4a263ba2 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Commands/BulkMoveSeriesCommand.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Tv.Commands +{ + public class BulkMoveSeriesCommand : Command + { + public List Series { get; set; } + public string DestinationRootFolder { get; set; } + + public override bool SendUpdatesToClient => true; + } + + public class BulkMoveSeries : IEquatable + { + public int SeriesId { get; set; } + public string SourcePath { get; set; } + + public bool Equals(BulkMoveSeries other) + { + if (other == null) + { + return false; + } + + return SeriesId.Equals(other.SeriesId); + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return SeriesId.Equals(((BulkMoveSeries)obj).SeriesId); + } + + public override int GetHashCode() + { + return SeriesId.GetHashCode(); + } + } +} diff --git a/src/NzbDrone.Core/Tv/Commands/MoveSeriesCommand.cs b/src/NzbDrone.Core/Tv/Commands/MoveSeriesCommand.cs index 1a283e80d..9971c6b9b 100644 --- a/src/NzbDrone.Core/Tv/Commands/MoveSeriesCommand.cs +++ b/src/NzbDrone.Core/Tv/Commands/MoveSeriesCommand.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Tv.Commands { @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Tv.Commands public int SeriesId { get; set; } public string SourcePath { get; set; } public string DestinationPath { get; set; } - public string DestinationRootFolder { get; set; } + + public override bool SendUpdatesToClient => true; } } diff --git a/src/NzbDrone.Core/Tv/Episode.cs b/src/NzbDrone.Core/Tv/Episode.cs index 6b34e65fe..15efc7996 100644 --- a/src/NzbDrone.Core/Tv/Episode.cs +++ b/src/NzbDrone.Core/Tv/Episode.cs @@ -7,7 +7,7 @@ using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Tv { - public class Episode : ModelBase + public class Episode : ModelBase, IComparable { public Episode() { @@ -46,5 +46,32 @@ namespace NzbDrone.Core.Tv { return string.Format("[{0}]{1}", Id, Title.NullSafe()); } + + public int CompareTo(object obj) + { + var other = (Episode)obj; + + if (SeasonNumber > other.SeasonNumber) + { + return 1; + } + + if (SeasonNumber < other.SeasonNumber) + { + return -1; + } + + if (EpisodeNumber > other.EpisodeNumber) + { + return 1; + } + + if (EpisodeNumber < other.EpisodeNumber) + { + return -1; + } + + return 0; + } } } diff --git a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs index c680116c6..f4ef80513 100644 --- a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -26,63 +26,158 @@ namespace NzbDrone.Core.Tv public void SetEpisodeMonitoredStatus(Series series, MonitoringOptions monitoringOptions) { - if (monitoringOptions != null) + // Update the series without changing the episodes + if (monitoringOptions == null) { - _logger.Debug("[{0}] Setting episode monitored status.", series.Title); - - var episodes = _episodeService.GetEpisodeBySeries(series.Id); - - if (monitoringOptions.IgnoreEpisodesWithFiles) - { - _logger.Debug("Unmonitoring Episodes with Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), false); - } - else - { - _logger.Debug("Monitoring Episodes with Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), true); - } - - if (monitoringOptions.IgnoreEpisodesWithoutFiles) - { - _logger.Debug("Unmonitoring Episodes without Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), false); - } - else - { - _logger.Debug("Monitoring Episodes without Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), true); - } - - var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault(); - - foreach (var s in series.Seasons) - { - var season = s; - - // If the season is unmonitored we should unmonitor all episodes in that season - - if (!season.Monitored) - { - _logger.Debug("Unmonitoring all episodes in season {0}", season.SeasonNumber); - ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), false); - } - - // If the season is not the latest season and all it's episodes are unmonitored the season will be unmonitored - - if (season.SeasonNumber < lastSeason) - { - if (episodes.Where(e => e.SeasonNumber == season.SeasonNumber).All(e => !e.Monitored)) - { - _logger.Debug("Unmonitoring season {0} because all episodes are not monitored", season.SeasonNumber); - season.Monitored = false; - } - } - } - - _episodeService.UpdateEpisodes(episodes); + _seriesService.UpdateSeries(series, false); + return; } + // Fallback for v2 endpoints + if (monitoringOptions.Monitor == MonitorTypes.Unknown) + { + LegacySetEpisodeMonitoredStatus(series, monitoringOptions); + return; + } + + var firstSeason = series.Seasons.Select(s => s.SeasonNumber).Where(s => s > 0).MinOrDefault(); + var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault(); + var episodes = _episodeService.GetEpisodeBySeries(series.Id); + + switch (monitoringOptions.Monitor) + { + case MonitorTypes.All: + _logger.Debug("[{0}] Monitoring all episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0); + + break; + + case MonitorTypes.Future: + _logger.Debug("[{0}] Monitoring future episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && (!e.AirDateUtc.HasValue || e.AirDateUtc >= DateTime.UtcNow)); + + break; + + case MonitorTypes.Missing: + _logger.Debug("[{0}] Monitoring missing episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && !e.HasFile); + + break; + + case MonitorTypes.Existing: + _logger.Debug("[{0}] Monitoring existing episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && e.HasFile); + + break; + + case MonitorTypes.FirstSeason: + _logger.Debug("[{0}] Monitoring first season episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && e.SeasonNumber == firstSeason); + + break; + + case MonitorTypes.LatestSeason: + _logger.Debug("[{0}] Monitoring latest season episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && e.SeasonNumber == lastSeason); + + break; + + case MonitorTypes.None: + _logger.Debug("[{0}] Unmonitoring all episodes", series.Title); + ToggleEpisodesMonitoredState(episodes, e => false); + + break; + } + + var monitoredSeasons = episodes.Where(e => e.Monitored) + .Select(e => e.SeasonNumber) + .Distinct() + .ToList(); + + foreach (var season in series.Seasons) + { + var seasonNumber = season.SeasonNumber; + + // Monitor the season when: + // - Not specials + // - The latest season + // - Not only supposed to monitor the first season + if (seasonNumber > 0 && seasonNumber == lastSeason && monitoringOptions.Monitor != MonitorTypes.FirstSeason) + { + season.Monitored = true; + } + // Monitor the season if it has any monitor episodes + else if (monitoredSeasons.Contains(seasonNumber)) + { + season.Monitored = true; + } + // Don't monitor the season + else + { + season.Monitored = false; + } + } + + _episodeService.UpdateEpisodes(episodes); + _seriesService.UpdateSeries(series, false); + } + + private void LegacySetEpisodeMonitoredStatus(Series series, MonitoringOptions monitoringOptions) + { + _logger.Debug("[{0}] Setting episode monitored status.", series.Title); + + var episodes = _episodeService.GetEpisodeBySeries(series.Id); + + if (monitoringOptions.IgnoreEpisodesWithFiles) + { + _logger.Debug("Unmonitoring Episodes with Files"); + ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), false); + } + else + { + _logger.Debug("Monitoring Episodes with Files"); + ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), true); + } + + if (monitoringOptions.IgnoreEpisodesWithoutFiles) + { + _logger.Debug("Unmonitoring Episodes without Files"); + ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), false); + } + else + { + _logger.Debug("Monitoring Episodes without Files"); + ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), true); + } + + var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault(); + + foreach (var s in series.Seasons) + { + var season = s; + + // If the season is unmonitored we should unmonitor all episodes in that season + + if (!season.Monitored) + { + _logger.Debug("Unmonitoring all episodes in season {0}", season.SeasonNumber); + ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), false); + } + + // If the season is not the latest season and all it's episodes are unmonitored the season will be unmonitored + + if (season.SeasonNumber < lastSeason) + { + if (episodes.Where(e => e.SeasonNumber == season.SeasonNumber).All(e => !e.Monitored)) + { + _logger.Debug("Unmonitoring season {0} because all episodes are not monitored", season.SeasonNumber); + season.Monitored = false; + } + } + } + + _episodeService.UpdateEpisodes(episodes); + _seriesService.UpdateSeries(series, false); } @@ -93,5 +188,12 @@ namespace NzbDrone.Core.Tv episode.Monitored = monitored; } } + + private void ToggleEpisodesMonitoredState(List episodes, Func predicate) + { + ToggleEpisodesMonitoredState(episodes.Where(predicate), true); + ToggleEpisodesMonitoredState(episodes.Where(e => !predicate(e)), false); + + } } } diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index a0602e307..16c548faa 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Tv List EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); void SetMonitoredFlat(Episode episode, bool monitored); void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); void SetFileId(int episodeId, int fileId); } @@ -183,6 +184,19 @@ namespace NzbDrone.Core.Tv mapper.ExecuteNonQuery(sql); } + public void SetMonitored(IEnumerable ids, bool monitored) + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("monitored", monitored); + + var sql = "UPDATE Episodes " + + "SET Monitored = @monitored " + + $"WHERE Id IN ({string.Join(", ", ids)})"; + + mapper.ExecuteNonQuery(sql); + } + public void SetFileId(int episodeId, int fileId) { SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId); @@ -191,7 +205,7 @@ namespace NzbDrone.Core.Tv private SortBuilder GetMissingEpisodesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber) { return Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) - .Where(pagingSpec.FilterExpression) + .Where(pagingSpec.FilterExpressions.FirstOrDefault()) .AndWhere(e => e.EpisodeFileId == 0) .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) .AndWhere(BuildAirDateUtcCutoffWhereClause(currentTime)) @@ -204,7 +218,7 @@ namespace NzbDrone.Core.Tv { return Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) .Join(JoinType.Left, e => e.EpisodeFile, (e, s) => e.EpisodeFileId == s.Id) - .Where(pagingSpec.FilterExpression) + .Where(pagingSpec.FilterExpressions.FirstOrDefault()) .AndWhere(e => e.EpisodeFileId != 0) .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) .AndWhere(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)) diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index acb756bd8..2e308ff8d 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Tv List GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); void UpdateEpisodes(List episodes); List EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); void InsertMany(List episodes); @@ -159,6 +160,11 @@ namespace NzbDrone.Core.Tv _logger.Debug("Monitored flag for Episode:{0} was set to {1}", episodeId, monitored); } + public void SetMonitored(IEnumerable ids, bool monitored) + { + _episodeRepository.SetMonitored(ids, monitored); + } + public void SetEpisodeMonitoredBySeason(int seriesId, int seasonNumber, bool monitored) { _episodeRepository.SetMonitoredBySeason(seriesId, seasonNumber, monitored); diff --git a/src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs new file mode 100644 index 000000000..2810c05ed --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class SeriesImportedEvent : IEvent + { + public List SeriesIds { get; private set; } + + public SeriesImportedEvent(List seriesIds) + { + SeriesIds = seriesIds; + } + } +} diff --git a/src/NzbDrone.Core/Tv/MonitoringOptions.cs b/src/NzbDrone.Core/Tv/MonitoringOptions.cs index 2cda68b1c..34d1704ec 100644 --- a/src/NzbDrone.Core/Tv/MonitoringOptions.cs +++ b/src/NzbDrone.Core/Tv/MonitoringOptions.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Tv { @@ -6,5 +6,18 @@ namespace NzbDrone.Core.Tv { public bool IgnoreEpisodesWithFiles { get; set; } public bool IgnoreEpisodesWithoutFiles { get; set; } + public MonitorTypes Monitor { get; set; } + } + + public enum MonitorTypes + { + Unknown, + All, + Future, + Missing, + Existing, + FirstSeason, + LatestSeason, + None } } diff --git a/src/NzbDrone.Core/Tv/MoveSeriesService.cs b/src/NzbDrone.Core/Tv/MoveSeriesService.cs index a6ffb0578..d5d89ac3f 100644 --- a/src/NzbDrone.Core/Tv/MoveSeriesService.cs +++ b/src/NzbDrone.Core/Tv/MoveSeriesService.cs @@ -1,7 +1,6 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -11,59 +10,94 @@ using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Tv { - public class MoveSeriesService : IExecute + public class MoveSeriesService : IExecute, IExecute { private readonly ISeriesService _seriesService; private readonly IBuildFileNames _filenameBuilder; + private readonly IDiskProvider _diskProvider; private readonly IDiskTransferService _diskTransferService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public MoveSeriesService(ISeriesService seriesService, IBuildFileNames filenameBuilder, + IDiskProvider diskProvider, IDiskTransferService diskTransferService, IEventAggregator eventAggregator, Logger logger) { _seriesService = seriesService; _filenameBuilder = filenameBuilder; + _diskProvider = diskProvider; _diskTransferService = diskTransferService; _eventAggregator = eventAggregator; _logger = logger; } - public void Execute(MoveSeriesCommand message) + private void MoveSingleSeries(Series series, string sourcePath, string destinationPath, int? index = null, int? total = null) { - var series = _seriesService.GetSeries(message.SeriesId); - var source = message.SourcePath; - var destination = message.DestinationPath; - - if (!message.DestinationRootFolder.IsNullOrWhiteSpace()) + if (!_diskProvider.FolderExists(sourcePath)) { - _logger.Debug("Buiding destination path using root folder: {0} and the series title", message.DestinationRootFolder); - destination = Path.Combine(message.DestinationRootFolder, _filenameBuilder.GetSeriesFolder(series)); + _logger.Debug("Folder '{0}' for '{1}' does not exist, not moving.", sourcePath, series.Title); + return; } - _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", series.Title, source, destination); + if (index != null && total != null) + { + _logger.ProgressInfo("Moving {0} from '{1}' to '{2}' ({3}/{4})", series.Title, sourcePath, destinationPath, index + 1, total); + } + else + { + _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", series.Title, sourcePath, destinationPath); + } - //TODO: Move to transactional disk operations try { - _diskTransferService.TransferFolder(source, destination, TransferMode.Move); + _diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move); } catch (IOException ex) { - _logger.Error(ex, "Unable to move series from '{0}' to '{1}'", source, destination); - throw; + _logger.Error(ex, "Unable to move series from '{0}' to '{1}'. Try moving files manually", sourcePath, destinationPath); + + RevertPath(series.Id, sourcePath); } _logger.ProgressInfo("{0} moved successfully to {1}", series.Title, series.Path); - //Update the series path to the new path - series.Path = destination; - series = _seriesService.UpdateSeries(series); + _eventAggregator.PublishEvent(new SeriesMovedEvent(series, sourcePath, destinationPath)); + } - _eventAggregator.PublishEvent(new SeriesMovedEvent(series, source, destination)); + private void RevertPath(int seriesId, string path) + { + var series = _seriesService.GetSeries(seriesId); + + series.Path = path; + _seriesService.UpdateSeries(series); + } + + public void Execute(MoveSeriesCommand message) + { + var series = _seriesService.GetSeries(message.SeriesId); + MoveSingleSeries(series, message.SourcePath, message.DestinationPath); + } + + public void Execute(BulkMoveSeriesCommand message) + { + var seriesToMove = message.Series; + var destinationRootFolder = message.DestinationRootFolder; + + _logger.ProgressInfo("Moving {0} series to '{1}'", seriesToMove.Count, destinationRootFolder); + + for (var index = 0; index < seriesToMove.Count; index++) + { + var s = seriesToMove[index]; + var series = _seriesService.GetSeries(s.SeriesId); + var destinationPath = Path.Combine(destinationRootFolder, _filenameBuilder.GetSeriesFolder(series)); + + MoveSingleSeries(series, s.SourcePath, destinationPath, index, seriesToMove.Count); + } + + _logger.ProgressInfo("Finished moving {0} series to '{1}'", seriesToMove.Count, destinationRootFolder); } } } diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index a7f42f554..71eb6089f 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Tv private void RefreshSeriesInfo(Series series) { - _logger.ProgressInfo("Updating Info for {0}", series.Title); + _logger.ProgressInfo("Updating {0}", series.Title); Tuple> tuple; diff --git a/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs b/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs index 2e7ee8005..1b8f59653 100644 --- a/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs +++ b/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs @@ -1,11 +1,13 @@ -using NzbDrone.Core.Messaging.Commands; +using System.Linq; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Tv { - public class SeriesAddedHandler : IHandle + public class SeriesAddedHandler : IHandle, + IHandle { private readonly IManageCommandQueue _commandQueueManager; @@ -18,5 +20,10 @@ namespace NzbDrone.Core.Tv { _commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id)); } + + public void Handle(SeriesImportedEvent message) + { + _commandQueueManager.PushMany(message.SeriesIds.Select(s => new RefreshSeriesCommand(s)).ToList()); + } } } diff --git a/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs b/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs new file mode 100644 index 000000000..5a222774f --- /dev/null +++ b/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.RootFolders; + +namespace NzbDrone.Core.Tv +{ + public interface IBuildSeriesPaths + { + string BuildPath(Series series, bool useExistingRelativeFolder); + } + + public class SeriesPathBuilder : IBuildSeriesPaths + { + private readonly IBuildFileNames _fileNameBuilder; + private readonly IRootFolderService _rootFolderService; + + public SeriesPathBuilder(IBuildFileNames fileNameBuilder, IRootFolderService rootFolderService) + { + _fileNameBuilder = fileNameBuilder; + _rootFolderService = rootFolderService; + } + + public string BuildPath(Series series, bool useExistingRelativeFolder) + { + if (series.RootFolderPath.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Root folder was not provided", nameof(series)); + } + + if (useExistingRelativeFolder && series.Path.IsNotNullOrWhiteSpace()) + { + var relativePath = GetExistingRelativePath(series); + return Path.Combine(series.RootFolderPath, relativePath); + } + + return Path.Combine(series.RootFolderPath, _fileNameBuilder.GetSeriesFolder(series)); + } + + private string GetExistingRelativePath(Series series) + { + var rootFolderPath = _rootFolderService.GetBestRootFolderPath(series.Path); + + return rootFolderPath.GetRelativePath(series.Path); + } + } +} diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 79166d843..e02195e5d 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Tv Series GetSeries(int seriesId); List GetSeries(IEnumerable seriesIds); Series AddSeries(Series newSeries); + List AddSeries(List newSeries); Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); Series FindByTitle(string title); @@ -22,8 +23,9 @@ namespace NzbDrone.Core.Tv Series FindByTitleInexact(string title); void DeleteSeries(int seriesId, bool deleteFiles); List GetAllSeries(); + List AllForTag(int tagId); Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true); - List UpdateSeries(List series); + List UpdateSeries(List series, bool useExistingRelativeFolder); bool SeriesPathExists(string folder); void RemoveAddOptions(Series series); } @@ -33,19 +35,19 @@ namespace NzbDrone.Core.Tv private readonly ISeriesRepository _seriesRepository; private readonly IEventAggregator _eventAggregator; private readonly IEpisodeService _episodeService; - private readonly IBuildFileNames _fileNameBuilder; + private readonly IBuildSeriesPaths _seriesPathBuilder; private readonly Logger _logger; public SeriesService(ISeriesRepository seriesRepository, IEventAggregator eventAggregator, IEpisodeService episodeService, - IBuildFileNames fileNameBuilder, + IBuildSeriesPaths seriesPathBuilder, Logger logger) { _seriesRepository = seriesRepository; _eventAggregator = eventAggregator; _episodeService = episodeService; - _fileNameBuilder = fileNameBuilder; + _seriesPathBuilder = seriesPathBuilder; _logger = logger; } @@ -67,6 +69,14 @@ namespace NzbDrone.Core.Tv return newSeries; } + public List AddSeries(List newSeries) + { + _seriesRepository.InsertMany(newSeries); + _eventAggregator.PublishEvent(new SeriesImportedEvent(newSeries.Select(s => s.Id).ToList())); + + return newSeries; + } + public Series FindByTvdbId(int tvRageId) { return _seriesRepository.FindByTvdbId(tvRageId); @@ -141,6 +151,12 @@ namespace NzbDrone.Core.Tv return _seriesRepository.All().ToList(); } + public List AllForTag(int tagId) + { + return GetAllSeries().Where(s => s.Tags.Contains(tagId)) + .ToList(); + } + // updateEpisodesToMatchSeason is an override for EpisodeMonitoredService to use so a change via Season pass doesn't get nuked by the seasons loop. // TODO: Remove when seasons are split from series (or we come up with a better way to address this) public Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true) @@ -163,19 +179,20 @@ namespace NzbDrone.Core.Tv return updatedSeries; } - public List UpdateSeries(List series) + public List UpdateSeries(List series, bool useExistingRelativeFolder) { _logger.Debug("Updating {0} series", series.Count); + foreach (var s in series) { _logger.Trace("Updating: {0}", s.Title); + if (!s.RootFolderPath.IsNullOrWhiteSpace()) { - var folderName = new DirectoryInfo(s.Path).Name; - s.Path = Path.Combine(s.RootFolderPath, folderName); + s.Path = _seriesPathBuilder.BuildPath(s, useExistingRelativeFolder); + _logger.Trace("Changing path for {0} to {1}", s.Title, s.Path); } - else { _logger.Trace("Not changing path for: {0}", s.Title); diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index 23ba6a0dd..63dfdabf1 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Nancy.Bootstrapper; -using NzbDrone.Api; +using Sonarr.Http; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http.Dispatchers; @@ -17,7 +17,9 @@ namespace NzbDrone.Host "NzbDrone.Host", "NzbDrone.Core", "NzbDrone.Api", - "NzbDrone.SignalR" + "NzbDrone.SignalR", + "Sonarr.Api.V3", + "Sonarr.Http" }; return new MainAppContainerBuilder(args, assemblies).Container; @@ -28,7 +30,7 @@ namespace NzbDrone.Host { AutoRegisterImplementations(); - Container.Register(); + Container.Register(); Container.Register(); } } diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index dfab01e41..a4f1d2842 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -190,6 +190,14 @@ {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} NzbDrone.SignalR + + {7140ff1f-79be-492f-9188-b21a050bf708} + Sonarr.Api.V3 + + + {5370bff7-1bd7-46bc-af06-7d9ea5cda1d6} + Sonarr.Http + diff --git a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs index 0df60a326..a671e3c15 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs @@ -14,6 +14,8 @@ namespace NzbDrone.Host.Owin.MiddleWare { SignalrDependencyResolver.Register(container); + // Half the default time (110s) to get under nginx's default 60 proxy_read_timeout + GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(55); GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromMinutes(3); } diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 884fe992a..7d36fcbbc 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -3,10 +3,11 @@ using System.Net; using FluentAssertions; using NLog; using NzbDrone.Api; -using NzbDrone.Api.REST; +using Sonarr.Http.REST; using NzbDrone.Common.Serializer; using RestSharp; using System.Linq; +using Sonarr.Http; namespace NzbDrone.Integration.Test.Client { diff --git a/src/NzbDrone.Integration.Test/CorsFixture.cs b/src/NzbDrone.Integration.Test/CorsFixture.cs index a37936a2a..fc2857453 100644 --- a/src/NzbDrone.Integration.Test/CorsFixture.cs +++ b/src/NzbDrone.Integration.Test/CorsFixture.cs @@ -1,7 +1,8 @@ using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; using RestSharp; +using Sonarr.Http.Extensions; namespace NzbDrone.Integration.Test { diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index 87a71ec7a..f9bf92a33 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; -using NLog; +using NLog; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Test.Common; +using Sonarr.Http.ClientSchema; namespace NzbDrone.Integration.Test { @@ -33,7 +33,7 @@ namespace NzbDrone.Integration.Test Implementation = nameof(Newznab), Name = "NewznabTest", Protocol = Core.Indexers.DownloadProtocol.Usenet, - Fields = Api.ClientSchema.SchemaBuilder.ToSchema(new NewznabSettings()) + Fields = SchemaBuilder.ToSchema(new NewznabSettings()) }); } @@ -42,4 +42,4 @@ namespace NzbDrone.Integration.Test _runner.KillAll(); } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 39e5a981a..981d0d591 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -69,6 +69,8 @@ ..\packages\Nancy.1.4.3\lib\net40\Nancy.dll True + + ..\packages\Nancy.1.4.4\lib\net40\Nancy.dll ..\packages\Nancy.Owin.1.4.1\lib\net40\Nancy.Owin.dll @@ -171,6 +173,10 @@ {CADDFCE0-7509-4430-8364-2074E1EEFCA2} NzbDrone.Test.Common + + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Sonarr.Http + diff --git a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs index b3342ffba..ff1a2dca8 100644 --- a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs +++ b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs @@ -1,7 +1,12 @@ -using Microsoft.AspNet.SignalR; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Infrastructure; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Serializer; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore.Events; namespace NzbDrone.SignalR { @@ -15,17 +20,31 @@ namespace NzbDrone.SignalR private IPersistentConnectionContext Context => ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType()); private static string API_KEY; + private readonly Dictionary _messageHistory; public NzbDronePersistentConnection(IConfigFileProvider configFileProvider) { API_KEY = configFileProvider.ApiKey; + _messageHistory = new Dictionary(); } + public void BroadcastMessage(SignalRMessage message) { + string lastMessage; + if (_messageHistory.TryGetValue(message.Name, out lastMessage)) + { + if (message.Action == ModelAction.Updated && message.Body.ToJson() == lastMessage) + { + return; + } + } + + _messageHistory[message.Name] = message.Body.ToJson(); + Context.Connection.Broadcast(message); } - + protected override bool AuthorizeRequest(IRequest request) { var apiKey = request.QueryString["apiKey"]; @@ -37,5 +56,27 @@ namespace NzbDrone.SignalR return false; } + + protected override Task OnConnected(IRequest request, string connectionId) + { + return SendVersion(connectionId); + } + + protected override Task OnReconnected(IRequest request, string connectionId) + { + return SendVersion(connectionId); + } + + private Task SendVersion(string connectionId) + { + return Context.Connection.Send(connectionId, new SignalRMessage + { + Name = "version", + Body = new + { + Version = BuildInfo.Version.ToString() + } + }); + } } } \ No newline at end of file diff --git a/src/NzbDrone.SignalR/SignalRMessage.cs b/src/NzbDrone.SignalR/SignalRMessage.cs index e8993c286..17a7d4187 100644 --- a/src/NzbDrone.SignalR/SignalRMessage.cs +++ b/src/NzbDrone.SignalR/SignalRMessage.cs @@ -1,8 +1,14 @@ +using Newtonsoft.Json; +using NzbDrone.Core.Datastore.Events; + namespace NzbDrone.SignalR { public class SignalRMessage { public object Body { get; set; } public string Name { get; set; } + + [JsonIgnore] + public ModelAction Action { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index 1acdb9d16..22e987cb8 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -90,192 +90,359 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesNLog", "Logentrie EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CurlSharp", "ExternalModules\CurlSharp\CurlSharp\CurlSharp.csproj", "{74420A79-CC16-442C-8B1E-7C1B913844F0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sonarr.Api.V3", "Sonarr.Api.V3\Sonarr.Api.V3.csproj", "{7140FF1F-79BE-492F-9188-B21A050BF708}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sonarr.Http", "Sonarr.Http\Sonarr.Http.csproj", "{5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x86 = Debug|x86 + Mono|Any CPU = Mono|Any CPU Mono|x86 = Mono|x86 + Release|Any CPU = Release|Any CPU Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|Any CPU.ActiveCfg = Debug|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.ActiveCfg = Debug|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.Build.0 = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|Any CPU.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|Any CPU.Build.0 = Release|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.ActiveCfg = Release|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.Build.0 = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|Any CPU.ActiveCfg = Release|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.ActiveCfg = Release|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.Build.0 = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|Any CPU.ActiveCfg = Debug|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.ActiveCfg = Debug|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|Any CPU.ActiveCfg = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|Any CPU.Build.0 = Release|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.ActiveCfg = Debug|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|Any CPU.ActiveCfg = Release|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.ActiveCfg = Release|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.Build.0 = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|Any CPU.ActiveCfg = Debug|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.ActiveCfg = Debug|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|Any CPU.ActiveCfg = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|Any CPU.Build.0 = Release|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.ActiveCfg = Debug|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|Any CPU.ActiveCfg = Release|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.ActiveCfg = Release|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.Build.0 = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|Any CPU.ActiveCfg = Debug|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.ActiveCfg = Debug|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|Any CPU.ActiveCfg = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|Any CPU.Build.0 = Release|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.ActiveCfg = Debug|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|Any CPU.ActiveCfg = Release|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.ActiveCfg = Release|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.Build.0 = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|Any CPU.ActiveCfg = Debug|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.ActiveCfg = Debug|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|Any CPU.ActiveCfg = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|Any CPU.Build.0 = Release|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.ActiveCfg = Debug|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|Any CPU.ActiveCfg = Release|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.ActiveCfg = Release|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.Build.0 = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|Any CPU.ActiveCfg = Debug|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.ActiveCfg = Debug|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|Any CPU.ActiveCfg = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|Any CPU.Build.0 = Release|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.ActiveCfg = Debug|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|Any CPU.ActiveCfg = Release|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.ActiveCfg = Release|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|Any CPU.ActiveCfg = Debug|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.ActiveCfg = Debug|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.Build.0 = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|Any CPU.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|Any CPU.Build.0 = Release|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.ActiveCfg = Release|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|Any CPU.ActiveCfg = Release|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.ActiveCfg = Release|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.Build.0 = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|Any CPU.ActiveCfg = Debug|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.ActiveCfg = Debug|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|Any CPU.ActiveCfg = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|Any CPU.Build.0 = Release|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.ActiveCfg = Debug|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|Any CPU.ActiveCfg = Release|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.ActiveCfg = Release|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.Build.0 = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|Any CPU.ActiveCfg = Debug|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.ActiveCfg = Debug|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|Any CPU.ActiveCfg = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|Any CPU.Build.0 = Release|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.ActiveCfg = Debug|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|Any CPU.ActiveCfg = Release|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.ActiveCfg = Release|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.Build.0 = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|Any CPU.ActiveCfg = Debug|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.ActiveCfg = Debug|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|Any CPU.ActiveCfg = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|Any CPU.Build.0 = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.ActiveCfg = Debug|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|Any CPU.ActiveCfg = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.ActiveCfg = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.Build.0 = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|Any CPU.ActiveCfg = Debug|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.ActiveCfg = Debug|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.Build.0 = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|Any CPU.ActiveCfg = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|Any CPU.Build.0 = Release|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|x86.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|Any CPU.ActiveCfg = Release|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.ActiveCfg = Release|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.Build.0 = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|Any CPU.ActiveCfg = Debug|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.ActiveCfg = Debug|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.Build.0 = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|Any CPU.ActiveCfg = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|Any CPU.Build.0 = Release|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|x86.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|Any CPU.ActiveCfg = Release|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.ActiveCfg = Release|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|Any CPU.ActiveCfg = Debug|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.ActiveCfg = Debug|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.Build.0 = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|Any CPU.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|Any CPU.Build.0 = Release|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.ActiveCfg = Release|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|Any CPU.ActiveCfg = Release|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.ActiveCfg = Release|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.Build.0 = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|Any CPU.ActiveCfg = Debug|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.ActiveCfg = Debug|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|Any CPU.ActiveCfg = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|Any CPU.Build.0 = Release|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.ActiveCfg = Debug|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|Any CPU.ActiveCfg = Release|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.ActiveCfg = Release|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|Any CPU.ActiveCfg = Debug|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.ActiveCfg = Debug|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.Build.0 = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|Any CPU.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|Any CPU.Build.0 = Release|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.ActiveCfg = Release|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|Any CPU.ActiveCfg = Release|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.ActiveCfg = Release|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.Build.0 = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|Any CPU.ActiveCfg = Debug|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.ActiveCfg = Debug|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.Build.0 = Debug|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|Any CPU.ActiveCfg = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|Any CPU.Build.0 = Release|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|x86.ActiveCfg = Release|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|x86.Build.0 = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|Any CPU.ActiveCfg = Release|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.ActiveCfg = Release|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.Build.0 = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|Any CPU.ActiveCfg = Debug|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.ActiveCfg = Debug|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|Any CPU.ActiveCfg = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|Any CPU.Build.0 = Release|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.ActiveCfg = Debug|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|Any CPU.ActiveCfg = Release|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.ActiveCfg = Release|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.Build.0 = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|Any CPU.ActiveCfg = Debug|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.ActiveCfg = Debug|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|Any CPU.ActiveCfg = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|Any CPU.Build.0 = Release|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.ActiveCfg = Debug|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|Any CPU.ActiveCfg = Release|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.ActiveCfg = Release|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.Build.0 = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|Any CPU.ActiveCfg = Debug|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.ActiveCfg = Debug|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.Build.0 = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|Any CPU.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|Any CPU.Build.0 = Release|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|x86.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|Any CPU.ActiveCfg = Release|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.ActiveCfg = Release|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.Build.0 = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|Any CPU.ActiveCfg = Debug|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.ActiveCfg = Debug|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|Any CPU.ActiveCfg = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|Any CPU.Build.0 = Release|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.ActiveCfg = Debug|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|Any CPU.ActiveCfg = Release|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.ActiveCfg = Release|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.Build.0 = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Any CPU.ActiveCfg = Debug|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.ActiveCfg = Debug|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.Build.0 = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|Any CPU.ActiveCfg = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|Any CPU.Build.0 = Release|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|x86.ActiveCfg = Debug|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|x86.Build.0 = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Any CPU.ActiveCfg = Release|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.ActiveCfg = Release|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.Build.0 = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Any CPU.ActiveCfg = Debug|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.ActiveCfg = Debug|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.Build.0 = Debug|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|Any CPU.ActiveCfg = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|Any CPU.Build.0 = Release|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|x86.ActiveCfg = Release|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|x86.Build.0 = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Any CPU.ActiveCfg = Release|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.ActiveCfg = Release|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|Any CPU.ActiveCfg = Debug|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.ActiveCfg = Debug|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.Build.0 = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|Any CPU.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|Any CPU.Build.0 = Release|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.ActiveCfg = Release|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|Any CPU.ActiveCfg = Release|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.ActiveCfg = Release|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.Build.0 = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Any CPU.Build.0 = Debug|Any CPU {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.ActiveCfg = Debug|x86 {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.Build.0 = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|Any CPU.Build.0 = Release|Any CPU {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|x86.ActiveCfg = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Any CPU.Build.0 = Release|Any CPU {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.ActiveCfg = Release|x86 {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.Build.0 = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Any CPU.Build.0 = Debug|Any CPU {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.ActiveCfg = Debug|x86 {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.Build.0 = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|Any CPU.Build.0 = Release|Any CPU {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|x86.ActiveCfg = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Any CPU.Build.0 = Release|Any CPU {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.ActiveCfg = Release|x86 {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.Build.0 = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.Build.0 = Debug|Any CPU {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.ActiveCfg = Debug|x86 {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.Build.0 = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|Any CPU.Build.0 = Release|Any CPU {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|x86.ActiveCfg = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Any CPU.Build.0 = Release|Any CPU {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.ActiveCfg = Release|x86 {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.Build.0 = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.Build.0 = Debug|Any CPU {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.ActiveCfg = Debug|x86 {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.Build.0 = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|Any CPU.Build.0 = Release|Any CPU {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|x86.ActiveCfg = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Any CPU.Build.0 = Release|Any CPU {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.ActiveCfg = Release|x86 {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.Build.0 = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|Any CPU.ActiveCfg = Debug|x86 {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.ActiveCfg = Debug|x86 {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.Build.0 = Debug|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|Any CPU.ActiveCfg = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|Any CPU.Build.0 = Release|x86 {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.ActiveCfg = Release|x86 {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.Build.0 = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|Any CPU.ActiveCfg = Release|x86 {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.ActiveCfg = Release|x86 {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.Build.0 = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|Any CPU.Build.0 = Debug|Any CPU {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.ActiveCfg = Debug|x86 {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.Build.0 = Debug|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|Any CPU.Build.0 = Release|Any CPU {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.ActiveCfg = Release|x86 {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.Build.0 = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|Any CPU.Build.0 = Release|Any CPU {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.ActiveCfg = Release|x86 {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.Build.0 = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.ActiveCfg = Debug|x86 {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.Build.0 = Debug|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|Any CPU.Build.0 = Release|Any CPU {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.ActiveCfg = Release|x86 {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.Build.0 = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|Any CPU.Build.0 = Release|Any CPU {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.ActiveCfg = Release|x86 {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.Build.0 = Release|x86 + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.ActiveCfg = Debug|Any CPU {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.Build.0 = Debug|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|Any CPU.Build.0 = Release|Any CPU {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.ActiveCfg = Release|Any CPU {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.Build.0 = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|Any CPU.Build.0 = Release|Any CPU {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.ActiveCfg = Release|Any CPU {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.Build.0 = Release|Any CPU + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|Any CPU.ActiveCfg = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|x86.ActiveCfg = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|x86.Build.0 = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|Any CPU.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|Any CPU.Build.0 = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|x86.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|x86.Build.0 = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|Any CPU.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|x86.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|x86.Build.0 = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|x86.Build.0 = Debug|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|Any CPU.ActiveCfg = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|Any CPU.Build.0 = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|x86.ActiveCfg = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|x86.Build.0 = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|Any CPU.Build.0 = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|x86.ActiveCfg = Release|Any CPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs new file mode 100644 index 000000000..2bd563d7a --- /dev/null +++ b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs @@ -0,0 +1,36 @@ +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Datastore; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Blacklist +{ + public class BlacklistModule : SonarrRestModule + { + private readonly IBlacklistService _blacklistService; + + public BlacklistModule(IBlacklistService blacklistService) + { + _blacklistService = blacklistService; + GetResourcePaged = GetBlacklist; + DeleteResource = DeleteBlacklist; + } + + private PagingResource GetBlacklist(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + return ApplyToPage(_blacklistService.Paged, pagingSpec, BlacklistResourceMapper.MapToResource); + } + + private void DeleteBlacklist(int id) + { + _blacklistService.Delete(id); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs new file mode 100644 index 000000000..615a49e85 --- /dev/null +++ b/src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Series; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Blacklist +{ + public class BlacklistResource : RestResource + { + public int SeriesId { get; set; } + public List EpisodeIds { get; set; } + public string SourceTitle { get; set; } + public QualityModel Quality { get; set; } + public DateTime Date { get; set; } + public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public string Message { get; set; } + + public SeriesResource Series { get; set; } + } + + public static class BlacklistResourceMapper + { + public static BlacklistResource MapToResource(this NzbDrone.Core.Blacklisting.Blacklist model) + { + if (model == null) return null; + + return new BlacklistResource + { + Id = model.Id, + + SeriesId = model.SeriesId, + EpisodeIds = model.EpisodeIds, + SourceTitle = model.SourceTitle, + Quality = model.Quality, + Date = model.Date, + Protocol = model.Protocol, + Indexer = model.Indexer, + Message = model.Message, + + Series = model.Series.ToResource() + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs new file mode 100644 index 000000000..d6a7fdf3e --- /dev/null +++ b/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ical.Net; +using Ical.Net.DataTypes; +using Ical.Net.General; +using Ical.Net.Interfaces.Serialization; +using Ical.Net.Serialization; +using Ical.Net.Serialization.iCalendar.Factory; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tags; +using NzbDrone.Core.Tv; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Calendar +{ + public class CalendarFeedModule : SonarrV3FeedModule + { + private readonly IEpisodeService _episodeService; + private readonly ITagService _tagService; + + public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService) + : base("calendar") + { + _episodeService = episodeService; + _tagService = tagService; + + Get["/Sonarr.ics"] = options => GetCalendarFeed(); + } + + private Response GetCalendarFeed() + { + var pastDays = 7; + var futureDays = 28; + var start = DateTime.Today.AddDays(-pastDays); + var end = DateTime.Today.AddDays(futureDays); + var unmonitored = Request.GetBooleanQueryParameter("unmonitored"); + var premiersOnly = Request.GetBooleanQueryParameter("premiersOnly"); + var asAllDay = Request.GetBooleanQueryParameter("asAllDay"); + var tags = new List(); + + var queryPastDays = Request.Query.PastDays; + var queryFutureDays = Request.Query.FutureDays; + var queryTags = Request.Query.Tags; + + if (queryPastDays.HasValue) + { + pastDays = int.Parse(queryPastDays.Value); + start = DateTime.Today.AddDays(-pastDays); + } + + if (queryFutureDays.HasValue) + { + futureDays = int.Parse(queryFutureDays.Value); + end = DateTime.Today.AddDays(futureDays); + } + + if (queryTags.HasValue) + { + var tagInput = (string)queryTags.Value.ToString(); + tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); + } + + var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored); + var calendar = new Ical.Net.Calendar + { + ProductId = "-//sonarr.tv//Sonarr//EN" + }; + + var calendarName = "Sonarr TV Schedule"; + calendar.AddProperty(new CalendarProperty("NAME", calendarName)); + calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName)); + + foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value)) + { + if (premiersOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1)) + { + continue; + } + + if (tags.Any() && tags.None(episode.Series.Tags.Contains)) + { + continue; + } + + var occurrence = calendar.Create(); + occurrence.Uid = "NzbDrone_episode_" + episode.Id; + occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; + occurrence.Description = episode.Overview; + occurrence.Categories = new List() { episode.Series.Network }; + + if (asAllDay) + { + occurrence.Start = new CalDateTime(episode.AirDateUtc.Value.ToLocalTime()) { HasTime = false }; + } + else + { + occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true }; + occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true }; + } + + switch (episode.Series.SeriesType) + { + case SeriesTypes.Daily: + occurrence.Summary = $"{episode.Series.Title} - {episode.Title}"; + break; + default: + occurrence.Summary = $"{episode.Series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}"; + break; + } + } + + var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); + var icalendar = serializer.SerializeToString(calendar); + + return new TextResponse(icalendar, "text/calendar"); + } + } +} diff --git a/src/Sonarr.Api.V3/Calendar/CalendarModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs new file mode 100644 index 000000000..6aeb8220b --- /dev/null +++ b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.DecisionEngine; +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, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "calendar") + { + GetResourceAll = GetCalendar; + } + + private List 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(); + } + } +} diff --git a/src/Sonarr.Api.V3/Commands/CommandModule.cs b/src/Sonarr.Api.V3/Commands/CommandModule.cs new file mode 100644 index 000000000..0d9a21906 --- /dev/null +++ b/src/Sonarr.Api.V3/Commands/CommandModule.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ProgressMessaging; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.Validation; + +namespace Sonarr.Api.V3.Commands +{ + public class CommandModule : SonarrRestModuleWithSignalR, IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + private readonly IServiceFactory _serviceFactory; + + public CommandModule(IManageCommandQueue commandQueueManager, + IBroadcastSignalRMessage signalRBroadcaster, + IServiceFactory serviceFactory) + : base(signalRBroadcaster) + { + _commandQueueManager = commandQueueManager; + _serviceFactory = serviceFactory; + + GetResourceById = GetCommand; + CreateResource = StartCommand; + GetResourceAll = GetStartedCommands; + + PostValidator.RuleFor(c => c.Name).NotBlank(); + } + + private CommandResource GetCommand(int id) + { + return _commandQueueManager.Get(id).ToResource(); + } + + private int StartCommand(CommandResource commandResource) + { + var commandType = + _serviceFactory.GetImplementations(typeof (Command)) + .Single(c => c.Name.Replace("Command", "") + .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); + + dynamic command = Request.Body.FromJson(commandType); + command.Trigger = CommandTrigger.Manual; + command.SuppressMessages = !command.SendUpdatesToClient; + command.SendUpdatesToClient = true; + + var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + return trackedCommand.Id; + } + + private List GetStartedCommands() + { + return _commandQueueManager.GetStarted().ToResource(); + } + + public void Handle(CommandUpdatedEvent message) + { + if (message.Command.Body.SendUpdatesToClient) + { + BroadcastResourceChange(ModelAction.Updated, message.Command.ToResource()); + } + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Commands/CommandResource.cs b/src/Sonarr.Api.V3/Commands/CommandResource.cs new file mode 100644 index 000000000..98c089f20 --- /dev/null +++ b/src/Sonarr.Api.V3/Commands/CommandResource.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Messaging.Commands; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Commands +{ + public class CommandResource : RestResource + { + public string Name { get; set; } + public string Message { get; set; } + public Command Body { get; set; } + public CommandPriority Priority { get; set; } + public CommandStatus Status { get; set; } + public DateTime Queued { get; set; } + public DateTime? Started { get; set; } + public DateTime? Ended { get; set; } + public TimeSpan? Duration { get; set; } + public string Exception { get; set; } + public CommandTrigger Trigger { get; set; } + + [JsonIgnore] + public string CompletionMessage { get; set; } + + //Legacy + public CommandStatus State + { + get + { + return Status; + } + + set { } + } + + public bool Manual + { + get + { + return Trigger == CommandTrigger.Manual; + } + + set { } + } + + public DateTime StartedOn + { + get + { + return Queued; + } + + set { } + } + + public DateTime? StateChangeTime + { + get + { + + if (Started.HasValue) return Started.Value; + + return Ended; + } + + set { } + } + + public bool SendUpdatesToClient + { + get + { + if (Body != null) return Body.SendUpdatesToClient; + + return false; + } + + set { } + } + + public bool UpdateScheduledTask + { + get + { + if (Body != null) return Body.UpdateScheduledTask; + + return false; + } + + set { } + } + + public DateTime? LastExecutionTime { get; set; } + } + + public static class CommandResourceMapper + { + public static CommandResource ToResource(this CommandModel model) + { + if (model == null) return null; + + return new CommandResource + { + Id = model.Id, + + Name = model.Name, + Message = model.Message, + Body = model.Body, + Priority = model.Priority, + Status = model.Status, + Queued = model.QueuedAt, + Started = model.StartedAt, + Ended = model.EndedAt, + Duration = model.Duration, + Exception = model.Exception, + Trigger = model.Trigger, + + CompletionMessage = model.Body.CompletionMessage, + LastExecutionTime = model.Body.LastExecutionTime + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs b/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs new file mode 100644 index 000000000..08a2d5d06 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation.Paths; + +namespace Sonarr.Api.V3.Config +{ + public class DownloadClientConfigModule : SonarrConfigModule + { + public DownloadClientConfigModule(IConfigService configService, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator) + : base(configService) + { + SharedValidator.RuleFor(c => c.DownloadedEpisodesFolder) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(pathExistsValidator) + .When(c => !string.IsNullOrWhiteSpace(c.DownloadedEpisodesFolder)); + } + + protected override DownloadClientConfigResource ToResource(IConfigService model) + { + return DownloadClientConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs b/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs new file mode 100644 index 000000000..2bd371d5a --- /dev/null +++ b/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs @@ -0,0 +1,37 @@ +using NzbDrone.Core.Configuration; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class DownloadClientConfigResource : RestResource + { + public string DownloadedEpisodesFolder { get; set; } + public string DownloadClientWorkingFolders { get; set; } + public int DownloadedEpisodesScanInterval { get; set; } + + public bool EnableCompletedDownloadHandling { get; set; } + public bool RemoveCompletedDownloads { get; set; } + + public bool AutoRedownloadFailed { get; set; } + public bool RemoveFailedDownloads { get; set; } + } + + public static class DownloadClientConfigResourceMapper + { + public static DownloadClientConfigResource ToResource(IConfigService model) + { + return new DownloadClientConfigResource + { + DownloadedEpisodesFolder = model.DownloadedEpisodesFolder, + DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, + DownloadedEpisodesScanInterval = model.DownloadedEpisodesScanInterval, + + EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, + RemoveCompletedDownloads = model.RemoveCompletedDownloads, + + AutoRedownloadFailed = model.AutoRedownloadFailed, + RemoveFailedDownloads = model.RemoveFailedDownloads + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/HostConfigModule.cs b/src/Sonarr.Api.V3/Config/HostConfigModule.cs new file mode 100644 index 000000000..64eb27319 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/HostConfigModule.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Reflection; +using FluentValidation; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Config +{ + public class HostConfigModule : SonarrRestModule + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IConfigService _configService; + private readonly IUserService _userService; + + public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService configService, IUserService userService) + : base("/config/host") + { + _configFileProvider = configFileProvider; + _configService = configService; + _userService = userService; + + GetResourceSingle = GetHostConfig; + GetResourceById = GetHostConfig; + UpdateResource = SaveHostConfig; + + SharedValidator.RuleFor(c => c.BindAddress) + .ValidIp4Address() + .NotListenAllIp4Address() + .When(c => c.BindAddress != "*"); + + SharedValidator.RuleFor(c => c.Port).ValidPort(); + + SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase(); + + SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None); + SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None); + + SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows); + + SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); + SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); + } + + private HostConfigResource GetHostConfig() + { + var resource = _configFileProvider.ToResource(_configService); + resource.Id = 1; + + var user = _userService.FindUser(); + if (user != null) + { + resource.Username = user.Username; + resource.Password = user.Password; + } + + return resource; + } + + private HostConfigResource GetHostConfig(int id) + { + return GetHostConfig(); + } + + private void SaveHostConfig(HostConfigResource resource) + { + var dictionary = resource.GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); + + _configFileProvider.SaveConfigDictionary(dictionary); + + if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace()) + { + _userService.Upsert(resource.Username, resource.Password); + } + } + } +} diff --git a/src/Sonarr.Api.V3/Config/HostConfigResource.cs b/src/Sonarr.Api.V3/Config/HostConfigResource.cs new file mode 100644 index 000000000..a33bbedcc --- /dev/null +++ b/src/Sonarr.Api.V3/Config/HostConfigResource.cs @@ -0,0 +1,73 @@ +using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class HostConfigResource : RestResource + { + public string BindAddress { get; set; } + public int Port { get; set; } + public int SslPort { get; set; } + public bool EnableSsl { get; set; } + public bool LaunchBrowser { get; set; } + public AuthenticationType AuthenticationMethod { get; set; } + public bool AnalyticsEnabled { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string LogLevel { get; set; } + public string Branch { get; set; } + public string ApiKey { get; set; } + public string SslCertHash { get; set; } + public string UrlBase { get; set; } + public bool UpdateAutomatically { get; set; } + public UpdateMechanism UpdateMechanism { get; set; } + public string UpdateScriptPath { get; set; } + public bool ProxyEnabled { get; set; } + public ProxyType ProxyType { get; set; } + public string ProxyHostname { get; set; } + public int ProxyPort { get; set; } + public string ProxyUsername { get; set; } + public string ProxyPassword { get; set; } + public string ProxyBypassFilter { get; set; } + public bool ProxyBypassLocalAddresses { get; set; } + } + + public static class HostConfigResourceMapper + { + public static HostConfigResource ToResource(this IConfigFileProvider model, IConfigService configService) + { + // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? + return new HostConfigResource + { + BindAddress = model.BindAddress, + Port = model.Port, + SslPort = model.SslPort, + EnableSsl = model.EnableSsl, + LaunchBrowser = model.LaunchBrowser, + AuthenticationMethod = model.AuthenticationMethod, + AnalyticsEnabled = model.AnalyticsEnabled, + //Username + //Password + LogLevel = model.LogLevel, + Branch = model.Branch, + ApiKey = model.ApiKey, + SslCertHash = model.SslCertHash, + UrlBase = model.UrlBase, + UpdateAutomatically = model.UpdateAutomatically, + UpdateMechanism = model.UpdateMechanism, + UpdateScriptPath = model.UpdateScriptPath, + ProxyEnabled = configService.ProxyEnabled, + ProxyType = configService.ProxyType, + ProxyHostname = configService.ProxyHostname, + ProxyPort = configService.ProxyPort, + ProxyUsername = configService.ProxyUsername, + ProxyPassword = configService.ProxyPassword, + ProxyBypassFilter = configService.ProxyBypassFilter, + ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs b/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs new file mode 100644 index 000000000..438fa477a --- /dev/null +++ b/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using Sonarr.Http.Validation; + +namespace Sonarr.Api.V3.Config +{ + public class IndexerConfigModule : SonarrConfigModule + { + + public IndexerConfigModule(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(c => c.MinimumAge) + .GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(c => c.Retention) + .GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(c => c.RssSyncInterval) + .IsValidRssSyncInterval(); + } + + protected override IndexerConfigResource ToResource(IConfigService model) + { + return IndexerConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs new file mode 100644 index 000000000..6082d18b1 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs @@ -0,0 +1,27 @@ +using NzbDrone.Core.Configuration; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class IndexerConfigResource : RestResource + { + public int MinimumAge { get; set; } + public int Retention { get; set; } + public int MaximumSize { get; set; } + public int RssSyncInterval { get; set; } + } + + public static class IndexerConfigResourceMapper + { + public static IndexerConfigResource ToResource(IConfigService model) + { + return new IndexerConfigResource + { + MinimumAge = model.MinimumAge, + Retention = model.Retention, + MaximumSize = model.MaximumSize, + RssSyncInterval = model.RssSyncInterval + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs new file mode 100644 index 000000000..6d241de86 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation.Paths; + +namespace Sonarr.Api.V3.Config +{ + public class MediaManagementConfigModule : SonarrConfigModule + { + public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator) + : base(configService) + { + SharedValidator.RuleFor(c => c.FileChmod).NotEmpty(); + SharedValidator.RuleFor(c => c.FolderChmod).NotEmpty(); + SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin)); + } + + protected override MediaManagementConfigResource ToResource(IConfigService model) + { + return MediaManagementConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs new file mode 100644 index 000000000..21e0c5301 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs @@ -0,0 +1,56 @@ +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class MediaManagementConfigResource : RestResource + { + public bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } + public string RecycleBin { get; set; } + public bool AutoDownloadPropers { get; set; } + public bool CreateEmptySeriesFolders { get; set; } + public bool DeleteEmptyFolders { get; set; } + public FileDateType FileDate { get; set; } + + public bool SetPermissionsLinux { get; set; } + public string FileChmod { get; set; } + public string FolderChmod { get; set; } + public string ChownUser { get; set; } + public string ChownGroup { get; set; } + + public bool SkipFreeSpaceCheckWhenImporting { get; set; } + public bool CopyUsingHardlinks { get; set; } + public bool ImportExtraFiles { get; set; } + public string ExtraFileExtensions { get; set; } + public bool EnableMediaInfo { get; set; } + } + + public static class MediaManagementConfigResourceMapper + { + public static MediaManagementConfigResource ToResource(IConfigService model) + { + return new MediaManagementConfigResource + { + AutoUnmonitorPreviouslyDownloadedEpisodes = model.AutoUnmonitorPreviouslyDownloadedEpisodes, + RecycleBin = model.RecycleBin, + AutoDownloadPropers = model.AutoDownloadPropers, + CreateEmptySeriesFolders = model.CreateEmptySeriesFolders, + DeleteEmptyFolders = model.DeleteEmptyFolders, + FileDate = model.FileDate, + + SetPermissionsLinux = model.SetPermissionsLinux, + FileChmod = model.FileChmod, + FolderChmod = model.FolderChmod, + ChownUser = model.ChownUser, + ChownGroup = model.ChownGroup, + + SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting, + CopyUsingHardlinks = model.CopyUsingHardlinks, + ImportExtraFiles = model.ImportExtraFiles, + ExtraFileExtensions = model.ExtraFileExtensions, + EnableMediaInfo = model.EnableMediaInfo + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/NamingConfigModule.cs b/src/Sonarr.Api.V3/Config/NamingConfigModule.cs new file mode 100644 index 000000000..7bafcf6c6 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/NamingConfigModule.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Nancy.ModelBinding; +using Nancy.Responses; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Organizer; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.Mapping; + +namespace Sonarr.Api.V3.Config +{ + public class NamingConfigModule : SonarrRestModule + { + private readonly INamingConfigService _namingConfigService; + private readonly IFilenameSampleService _filenameSampleService; + private readonly IFilenameValidationService _filenameValidationService; + private readonly IBuildFileNames _filenameBuilder; + + public NamingConfigModule(INamingConfigService namingConfigService, + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService, + IBuildFileNames filenameBuilder) + : base("config/naming") + { + _namingConfigService = namingConfigService; + _filenameSampleService = filenameSampleService; + _filenameValidationService = filenameValidationService; + _filenameBuilder = filenameBuilder; + GetResourceSingle = GetNamingConfig; + GetResourceById = GetNamingConfig; + UpdateResource = UpdateNamingConfig; + + Get["/examples"] = x => GetExamples(this.Bind()); + + SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5); + SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); + SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat(); + SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); + SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); + } + + private void UpdateNamingConfig(NamingConfigResource resource) + { + var nameSpec = resource.ToModel(); + ValidateFormatResult(nameSpec); + + _namingConfigService.Save(nameSpec); + } + + private NamingConfigResource GetNamingConfig() + { + var nameSpec = _namingConfigService.GetConfig(); + var resource = nameSpec.ToResource(); + + if (resource.StandardEpisodeFormat.IsNotNullOrWhiteSpace()) + { + var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); + basicConfig.AddToResource(resource); + } + + return resource; + } + + private NamingConfigResource GetNamingConfig(int id) + { + return GetNamingConfig(); + } + + private JsonResponse GetExamples(NamingConfigResource config) + { + if (config.Id == 0) + { + config = GetNamingConfig(); + } + + var nameSpec = config.ToModel(); + var sampleResource = new NamingExampleResource(); + + var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); + var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); + var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); + + sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null + ? null + : singleEpisodeSampleResult.FileName; + + sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null + ? null + : multiEpisodeSampleResult.FileName; + + sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null + ? null + : dailyEpisodeSampleResult.FileName; + + sampleResource.AnimeEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult) != null + ? null + : animeEpisodeSampleResult.FileName; + + sampleResource.AnimeMultiEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult) != null + ? null + : animeMultiEpisodeSampleResult.FileName; + + sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace() + ? null + : _filenameSampleService.GetSeriesFolderSample(nameSpec); + + sampleResource.SeasonFolderExample = nameSpec.SeasonFolderFormat.IsNullOrWhiteSpace() + ? null + : _filenameSampleService.GetSeasonFolderSample(nameSpec); + + return sampleResource.AsResponse(); + } + + private void ValidateFormatResult(NamingConfig nameSpec) + { + var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); + var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); + var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); + + var singleEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult); + var multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult); + var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult); + var animeEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult); + var animeMultiEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult); + + var validationFailures = new List(); + + validationFailures.AddIfNotNull(singleEpisodeValidationResult); + validationFailures.AddIfNotNull(multiEpisodeValidationResult); + validationFailures.AddIfNotNull(dailyEpisodeValidationResult); + validationFailures.AddIfNotNull(animeEpisodeValidationResult); + validationFailures.AddIfNotNull(animeMultiEpisodeValidationResult); + + if (validationFailures.Any()) + { + throw new ValidationException(validationFailures.DistinctBy(v => v.PropertyName).ToArray()); + } + } + } +} diff --git a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs new file mode 100644 index 000000000..b5e1eb251 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs @@ -0,0 +1,22 @@ +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class NamingConfigResource : RestResource + { + public bool RenameEpisodes { get; set; } + public bool ReplaceIllegalCharacters { get; set; } + public int MultiEpisodeStyle { get; set; } + public string StandardEpisodeFormat { get; set; } + public string DailyEpisodeFormat { get; set; } + public string AnimeEpisodeFormat { get; set; } + public string SeriesFolderFormat { get; set; } + public string SeasonFolderFormat { get; set; } + public bool IncludeSeriesTitle { get; set; } + public bool IncludeEpisodeTitle { get; set; } + public bool IncludeQuality { get; set; } + public bool ReplaceSpaces { get; set; } + public string Separator { get; set; } + public string NumberStyle { get; set; } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs new file mode 100644 index 000000000..167dc6b99 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs @@ -0,0 +1,68 @@ +using NzbDrone.Core.Organizer; + +namespace Sonarr.Api.V3.Config +{ + public class NamingExampleResource + { + public string SingleEpisodeExample { get; set; } + public string MultiEpisodeExample { get; set; } + public string DailyEpisodeExample { get; set; } + public string AnimeEpisodeExample { get; set; } + public string AnimeMultiEpisodeExample { get; set; } + public string SeriesFolderExample { get; set; } + public string SeasonFolderExample { get; set; } + } + + public static class NamingConfigResourceMapper + { + public static NamingConfigResource ToResource(this NamingConfig model) + { + return new NamingConfigResource + { + Id = model.Id, + + RenameEpisodes = model.RenameEpisodes, + ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, + MultiEpisodeStyle = model.MultiEpisodeStyle, + StandardEpisodeFormat = model.StandardEpisodeFormat, + DailyEpisodeFormat = model.DailyEpisodeFormat, + AnimeEpisodeFormat = model.AnimeEpisodeFormat, + SeriesFolderFormat = model.SeriesFolderFormat, + SeasonFolderFormat = model.SeasonFolderFormat + //IncludeSeriesTitle + //IncludeEpisodeTitle + //IncludeQuality + //ReplaceSpaces + //Separator + //NumberStyle + }; + } + + public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource) + { + resource.IncludeSeriesTitle = basicNamingConfig.IncludeSeriesTitle; + resource.IncludeEpisodeTitle = basicNamingConfig.IncludeEpisodeTitle; + resource.IncludeQuality = basicNamingConfig.IncludeQuality; + resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces; + resource.Separator = basicNamingConfig.Separator; + resource.NumberStyle = basicNamingConfig.NumberStyle; + } + + public static NamingConfig ToModel(this NamingConfigResource resource) + { + return new NamingConfig + { + Id = resource.Id, + + RenameEpisodes = resource.RenameEpisodes, + ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, + MultiEpisodeStyle = resource.MultiEpisodeStyle, + StandardEpisodeFormat = resource.StandardEpisodeFormat, + DailyEpisodeFormat = resource.DailyEpisodeFormat, + AnimeEpisodeFormat = resource.AnimeEpisodeFormat, + SeriesFolderFormat = resource.SeriesFolderFormat, + SeasonFolderFormat = resource.SeasonFolderFormat + }; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs b/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs new file mode 100644 index 000000000..81db42608 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs @@ -0,0 +1,52 @@ +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 : SonarrRestModule 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); + } + } +} diff --git a/src/Sonarr.Api.V3/Config/UiConfigModule.cs b/src/Sonarr.Api.V3/Config/UiConfigModule.cs new file mode 100644 index 000000000..657d55f7b --- /dev/null +++ b/src/Sonarr.Api.V3/Config/UiConfigModule.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Config +{ + public class UiConfigModule : SonarrConfigModule + { + public UiConfigModule(IConfigService configService) + : base(configService) + { + + } + + protected override UiConfigResource ToResource(IConfigService model) + { + return UiConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/UiConfigResource.cs b/src/Sonarr.Api.V3/Config/UiConfigResource.cs new file mode 100644 index 000000000..1863987e0 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/UiConfigResource.cs @@ -0,0 +1,39 @@ +using NzbDrone.Core.Configuration; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Config +{ + public class UiConfigResource : RestResource + { + //Calendar + public int FirstDayOfWeek { get; set; } + public string CalendarWeekColumnHeader { get; set; } + + //Dates + public string ShortDateFormat { get; set; } + public string LongDateFormat { get; set; } + public string TimeFormat { get; set; } + public bool ShowRelativeDates { get; set; } + + public bool EnableColorImpairedMode { get; set; } + } + + public static class UiConfigResourceMapper + { + public static UiConfigResource ToResource(IConfigService model) + { + return new UiConfigResource + { + FirstDayOfWeek = model.FirstDayOfWeek, + CalendarWeekColumnHeader = model.CalendarWeekColumnHeader, + + ShortDateFormat = model.ShortDateFormat, + LongDateFormat = model.LongDateFormat, + TimeFormat = model.TimeFormat, + ShowRelativeDates = model.ShowRelativeDates, + + EnableColorImpairedMode = model.EnableColorImpairedMode, + }; + } + } +} diff --git a/src/Sonarr.Api.V3/CustomFilters/CustomFilterModule.cs b/src/Sonarr.Api.V3/CustomFilters/CustomFilterModule.cs new file mode 100644 index 000000000..473b4628c --- /dev/null +++ b/src/Sonarr.Api.V3/CustomFilters/CustomFilterModule.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using NzbDrone.Core.CustomFilters; +using Sonarr.Http; + +namespace Sonarr.Api.V3.CustomFilters +{ + public class CustomFilterModule : SonarrRestModule + { + private readonly ICustomFilterService _customFilterService; + + public CustomFilterModule(ICustomFilterService customFilterService) + { + _customFilterService = customFilterService; + + GetResourceById = GetCustomFilter; + GetResourceAll = GetCustomFilters; + CreateResource = AddCustomFilter; + UpdateResource = UpdateCustomFilter; + DeleteResource = DeleteCustomResource; + } + + private CustomFilterResource GetCustomFilter(int id) + { + return _customFilterService.Get(id).ToResource(); + } + + private List GetCustomFilters() + { + return _customFilterService.All().ToResource(); + } + + private int AddCustomFilter(CustomFilterResource resource) + { + var customFilter = _customFilterService.Add(resource.ToModel()); + + return customFilter.Id; + } + + private void UpdateCustomFilter(CustomFilterResource resource) + { + _customFilterService.Update(resource.ToModel()); + } + + private void DeleteCustomResource(int id) + { + _customFilterService.Delete(id); + } + } +} diff --git a/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs b/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs new file mode 100644 index 000000000..b1079591f --- /dev/null +++ b/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.CustomFilters; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.CustomFilters +{ + public class CustomFilterResource : RestResource + { + public string Type { get; set; } + public string Label { get; set; } + public List Filters { get; set; } + } + + public static class CustomFilterResourceMapper + { + public static CustomFilterResource ToResource(this CustomFilter model) + { + if (model == null) return null; + + return new CustomFilterResource + { + Id = model.Id, + Type = model.Type, + Label = model.Label, + Filters = Json.Deserialize>(model.Filters) + }; + } + + public static CustomFilter ToModel(this CustomFilterResource resource) + { + if (resource == null) return null; + + return new CustomFilter + { + Id = resource.Id, + Type = resource.Type, + Label = resource.Label, + Filters = Json.ToJson(resource.Filters) + }; + } + + public static List ToResource(this IEnumerable filters) + { + return filters.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs new file mode 100644 index 000000000..dedc59431 --- /dev/null +++ b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using NzbDrone.Core.DiskSpace; +using Sonarr.Http; + +namespace Sonarr.Api.V3.DiskSpace +{ + public class DiskSpaceModule :SonarrRestModule + { + private readonly IDiskSpaceService _diskSpaceService; + + public DiskSpaceModule(IDiskSpaceService diskSpaceService) + :base("diskspace") + { + _diskSpaceService = diskSpaceService; + GetResourceAll = GetFreeSpace; + } + + public List GetFreeSpace() + { + return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); + } + } +} diff --git a/src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs new file mode 100644 index 000000000..18750eebd --- /dev/null +++ b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs @@ -0,0 +1,28 @@ +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.DiskSpace +{ + public class DiskSpaceResource : RestResource + { + public string Path { get; set; } + public string Label { get; set; } + public long FreeSpace { get; set; } + public long TotalSpace { get; set; } + } + + public static class DiskSpaceResourceMapper + { + public static DiskSpaceResource MapToResource(this NzbDrone.Core.DiskSpace.DiskSpace model) + { + if (model == null) return null; + + return new DiskSpaceResource + { + Path = model.Path, + Label = model.Label, + FreeSpace = model.FreeSpace, + TotalSpace = model.TotalSpace + }; + } + } +} diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs new file mode 100644 index 000000000..7db21fd69 --- /dev/null +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Download; + +namespace Sonarr.Api.V3.DownloadClient +{ + public class DownloadClientModule : ProviderModuleBase + { + public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); + + public DownloadClientModule(IDownloadClientFactory downloadClientFactory) + : base(downloadClientFactory, "downloadclient", ResourceMapper) + { + } + + protected override void Validate(DownloadClientDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs new file mode 100644 index 000000000..71fe480d3 --- /dev/null +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs @@ -0,0 +1,38 @@ +using NzbDrone.Core.Download; +using NzbDrone.Core.Indexers; + +namespace Sonarr.Api.V3.DownloadClient +{ + public class DownloadClientResource : ProviderResource + { + public bool Enable { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public class DownloadClientResourceMapper : ProviderResourceMapper + { + public override DownloadClientResource ToResource(DownloadClientDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + resource.Protocol = definition.Protocol; + + return resource; + } + + public override DownloadClientDefinition ToModel(DownloadClientResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + definition.Protocol = resource.Protocol; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileListResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileListResource.cs new file mode 100644 index 000000000..3f6e13afc --- /dev/null +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileListResource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Qualities; + +namespace Sonarr.Api.V3.EpisodeFiles +{ + public class EpisodeFileListResource + { + public List EpisodeFileIds { get; set; } + public QualityModel Quality { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs new file mode 100644 index 000000000..4b0b3a053 --- /dev/null +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nancy; +using NLog; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Api.V3.Series; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using BadRequestException = Sonarr.Http.REST.BadRequestException; + +namespace Sonarr.Api.V3.EpisodeFiles +{ + public class EpisodeFileModule : SonarrRestModuleWithSignalR, + IHandle, + IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IDeleteMediaFiles _mediaFileDeletionService; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly ISeriesService _seriesService; + private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + + public EpisodeFileModule(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IDeleteMediaFiles mediaFileDeletionService, + ISeriesService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _mediaFileDeletionService = mediaFileDeletionService; + _seriesService = seriesService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetEpisodeFile; + GetResourceAll = GetEpisodeFiles; + UpdateResource = SetQuality; + DeleteResource = DeleteEpisodeFile; + + Put["/editor"] = episodeFiles => SetQuality(); + Delete["/bulk"] = episodeFiles => DeleteEpisodeFiles(); + } + + private EpisodeFileResource GetEpisodeFile(int id) + { + var episodeFile = _mediaFileService.Get(id); + var series = _seriesService.GetSeries(episodeFile.SeriesId); + + return episodeFile.ToResource(series, _qualityUpgradableSpecification); + } + + private List GetEpisodeFiles() + { + var seriesIdQuery = Request.Query.SeriesId; + var episodeFileIdsQuery = Request.Query.EpisodeFileIds; + + if (!seriesIdQuery.HasValue && !episodeFileIdsQuery.HasValue) + { + throw new BadRequestException("seriesId or episodeFileIds must be provided"); + } + + if (seriesIdQuery.HasValue) + { + int seriesId = Convert.ToInt32(seriesIdQuery.Value); + var series = _seriesService.GetSeries(seriesId); + + return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); + } + + else + { + string episodeFileIdsValue = episodeFileIdsQuery.Value.ToString(); + + var episodeFileIds = episodeFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + var episodeFiles = _mediaFileService.Get(episodeFileIds); + + return episodeFiles.GroupBy(e => e.SeriesId) + .SelectMany(f => f.ToList() + .ConvertAll( e => e.ToResource(_seriesService.GetSeries(f.Key), _qualityUpgradableSpecification))) + .ToList(); + } + } + + private void SetQuality(EpisodeFileResource episodeFileResource) + { + var episodeFile = _mediaFileService.Get(episodeFileResource.Id); + episodeFile.Quality = episodeFileResource.Quality; + _mediaFileService.Update(episodeFile); + } + + private Response SetQuality() + { + var resource = Request.Body.FromJson(); + var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds); + + foreach (var episodeFile in episodeFiles) + { + episodeFile.Quality = resource.Quality; + } + + _mediaFileService.Update(episodeFiles); + + var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); + + return episodeFiles.ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)) + .AsResponse(HttpStatusCode.Accepted); + } + + private void DeleteEpisodeFile(int id) + { + var episodeFile = _mediaFileService.Get(id); + + if (episodeFile == null) + { + throw new NzbDroneClientException(global::System.Net.HttpStatusCode.NotFound, "Episode file not found"); + } + + var series = _seriesService.GetSeries(episodeFile.SeriesId); + + _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); + } + + private Response DeleteEpisodeFiles() + { + var resource = Request.Body.FromJson(); + var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds); + var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); + + foreach (var episodeFile in episodeFiles) + { + _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); + } + + return new object().AsResponse(); + } + + public void Handle(EpisodeFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); + } + + public void Handle(EpisodeFileDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.EpisodeFile.Id); + } + } +} diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs new file mode 100644 index 000000000..0920c3a5a --- /dev/null +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.EpisodeFiles +{ + public class EpisodeFileResource : RestResource + { + public int SeriesId { get; set; } + public int SeasonNumber { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string SceneName { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoResource MediaInfo { get; set; } + + public bool QualityCutoffNotMet { get; set; } + } + + public static class EpisodeFileResourceMapper + { + private static EpisodeFileResource ToResource(this EpisodeFile model) + { + if (model == null) return null; + + return new EpisodeFileResource + { + Id = model.Id, + + SeriesId = model.SeriesId, + SeasonNumber = model.SeasonNumber, + RelativePath = model.RelativePath, + //Path + Size = model.Size, + DateAdded = model.DateAdded, + SceneName = model.SceneName, + Quality = model.Quality, + MediaInfo = model.MediaInfo.ToResource(model.SceneName) + //QualityCutoffNotMet + }; + + } + + public static EpisodeFileResource ToResource(this EpisodeFile model, NzbDrone.Core.Tv.Series series, IQualityUpgradableSpecification qualityUpgradableSpecification) + { + if (model == null) return null; + + return new EpisodeFileResource + { + Id = model.Id, + + SeriesId = model.SeriesId, + SeasonNumber = model.SeasonNumber, + RelativePath = model.RelativePath, + Path = Path.Combine(series.Path, model.RelativePath), + Size = model.Size, + DateAdded = model.DateAdded, + SceneName = model.SceneName, + Quality = model.Quality, + MediaInfo = model.MediaInfo.ToResource(model.SceneName), + QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality) + }; + } + } +} diff --git a/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs new file mode 100644 index 000000000..77fa66584 --- /dev/null +++ b/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs @@ -0,0 +1,67 @@ +using System; +using System.Text; +using NzbDrone.Core.MediaFiles.MediaInfo; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.EpisodeFiles +{ + public class MediaInfoResource : RestResource + { + public int AudioBitrate { get; set; } + public decimal AudioChannels { get; set; } + public string AudioCodec { get; set; } + public string AudioLanguages { get; set; } + public int AudioStreamCount { get; set; } + public int VideoBitDepth { get; set; } + public int VideoBitrate { get; set; } + public string VideoCodec { get; set; } + public decimal VideoFps { get; set; } + public string Resolution { get; set; } + public string RunTime { get; set; } + public string ScanType { get; set; } + public string Subtitles { get; set; } + } + + public static class MediaInfoResourceMapper + { + public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName) + { + if (model == null) + { + return null; + } + + return new MediaInfoResource + { + AudioBitrate = model.AudioBitrate, + AudioChannels = MediaInfoFormatter.FormatAudioChannels(model), + AudioLanguages = model.AudioLanguages, + AudioStreamCount = model.AudioStreamCount, + AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName), + VideoBitDepth = model.VideoBitDepth, + VideoBitrate = model.VideoBitrate, + VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName), + VideoFps = model.VideoFps, + Resolution = $"{model.Width}x{model.Height}", + RunTime = FormatRuntime(model.RunTime), + ScanType = model.ScanType, + Subtitles = model.Subtitles + }; + } + + + private static string FormatRuntime(TimeSpan runTime) + { + var formattedRuntime = ""; + + if (runTime.Hours > 0) + { + formattedRuntime += $"{runTime.Hours}:"; + } + + formattedRuntime += $"{runTime.Minutes}:{runTime.Seconds}"; + + return formattedRuntime; + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs new file mode 100644 index 000000000..bae18ec00 --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Episodes +{ + public class EpisodeModule : EpisodeModuleWithSignalR + { + public EpisodeModule(ISeriesService seriesService, + IEpisodeService episodeService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster) + { + GetResourceAll = GetEpisodes; + Put[@"/(?[\d]{1,10})"] = x => SetEpisodeMonitored(x.Id); + Put["/monitor"] = x => SetEpisodesMonitored(); + } + + private List GetEpisodes() + { + var seriesIdQuery = Request.Query.SeriesId; + var episodeIdsQuery = Request.Query.EpisodeIds; + var includeImages = Request.GetBooleanQueryParameter("includeImages", false); + + if (!seriesIdQuery.HasValue && !episodeIdsQuery.HasValue) + { + throw new BadRequestException("seriesId or episodeIds must be provided"); + } + + if (seriesIdQuery.HasValue) + { + int seriesId = Convert.ToInt32(seriesIdQuery.Value); + var seasonNumber = Request.Query.SeasonNumber.HasValue ? (int)Request.Query.SeasonNumber : (int?)null; + + if (seasonNumber.HasValue) + { + return MapToResource(_episodeService.GetEpisodesBySeason(seriesId, seasonNumber.Value), false, false, includeImages); + } + + return MapToResource(_episodeService.GetEpisodeBySeries(seriesId), false, false, includeImages); + } + + string episodeIdsValue = episodeIdsQuery.Value.ToString(); + + var episodeIds = episodeIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return MapToResource(_episodeService.GetEpisodes(episodeIds), false, false, includeImages); + } + + private Response SetEpisodeMonitored(int id) + { + var resource = Request.Body.FromJson(); + _episodeService.SetEpisodeMonitored(id, resource.Monitored); + + return MapToResource(_episodeService.GetEpisode(id), false, false, false).AsResponse(HttpStatusCode.Accepted); + } + + private Response SetEpisodesMonitored() + { + var includeImages = Request.GetBooleanQueryParameter("includeImages", false); + var resource = Request.Body.FromJson(); + + _episodeService.SetMonitored(resource.EpisodeIds, resource.Monitored); + + return MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages) + .AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs new file mode 100644 index 000000000..692ad319d --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Api.V3.EpisodeFiles; +using Sonarr.Api.V3.Series; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.Mapping; + +namespace Sonarr.Api.V3.Episodes +{ + public abstract class EpisodeModuleWithSignalR : SonarrRestModuleWithSignalR, + IHandle, + IHandle, + IHandle + { + protected readonly IEpisodeService _episodeService; + protected readonly ISeriesService _seriesService; + protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + + protected EpisodeModuleWithSignalR(IEpisodeService episodeService, + ISeriesService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) + { + _episodeService = episodeService; + _seriesService = seriesService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetEpisode; + } + + protected EpisodeModuleWithSignalR(IEpisodeService episodeService, + ISeriesService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster, + string resource) + : base(signalRBroadcaster, resource) + { + _episodeService = episodeService; + _seriesService = seriesService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetEpisode; + } + + protected EpisodeResource GetEpisode(int id) + { + var episode = _episodeService.GetEpisode(id); + var resource = MapToResource(episode, true, true, true); + return resource; + } + + protected EpisodeResource MapToResource(Episode episode, bool includeSeries, bool includeEpisodeFile, bool includeImages) + { + var resource = episode.ToResource(); + + if (includeSeries || includeEpisodeFile || includeImages) + { + var series = episode.Series ?? _seriesService.GetSeries(episode.SeriesId); + + if (includeSeries) + { + resource.Series = series.ToResource(); + } + + if (includeEpisodeFile && episode.EpisodeFileId != 0) + { + resource.EpisodeFile = episode.EpisodeFile.Value.ToResource(series, _qualityUpgradableSpecification); + } + + if (includeImages) + { + resource.Images = episode.Images; + } + } + + return resource; + } + + protected List MapToResource(List episodes, bool includeSeries, bool includeEpisodeFile, bool includeImages) + { + var result = episodes.ToResource(); + + if (includeSeries || includeEpisodeFile || includeImages) + { + var seriesDict = new Dictionary(); + for (var i = 0; i < episodes.Count; i++) + { + var episode = episodes[i]; + var resource = result[i]; + + var series = episode.Series ?? seriesDict.GetValueOrDefault(episodes[i].SeriesId) ?? _seriesService.GetSeries(episodes[i].SeriesId); + seriesDict[series.Id] = series; + + if (includeSeries) + { + resource.Series = series.ToResource(); + } + + if (includeEpisodeFile && episode.EpisodeFileId != 0) + { + resource.EpisodeFile = episode.EpisodeFile.Value.ToResource(series, _qualityUpgradableSpecification); + } + + if (includeImages) + { + resource.Images = episode.Images; + } + } + } + + return result; + } + + public void Handle(EpisodeGrabbedEvent message) + { + foreach (var episode in message.Episode.Episodes) + { + var resource = episode.ToResource(); + resource.Grabbed = true; + + BroadcastResourceChange(ModelAction.Updated, resource); + } + } + + public void Handle(EpisodeImportedEvent message) + { + foreach (var episode in message.EpisodeInfo.Episodes) + { + BroadcastResourceChange(ModelAction.Updated, episode.Id); + } + } + + public void Handle(EpisodeFileDeletedEvent message) + { + foreach (var episode in message.EpisodeFile.Episodes.Value) + { + BroadcastResourceChange(ModelAction.Updated, episode.Id); + } + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs new file mode 100644 index 000000000..63ec04c35 --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/EpisodeResource.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using Sonarr.Api.V3.EpisodeFiles; +using Sonarr.Api.V3.Series; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Episodes +{ + public class EpisodeResource : RestResource + { + public int SeriesId { get; set; } + public int EpisodeFileId { get; set; } + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + public string Title { get; set; } + public string AirDate { get; set; } + public DateTime? AirDateUtc { get; set; } + public string Overview { get; set; } + public EpisodeFileResource EpisodeFile { get; set; } + + public bool HasFile { get; set; } + public bool Monitored { get; set; } + public int? AbsoluteEpisodeNumber { get; set; } + public int? SceneAbsoluteEpisodeNumber { get; set; } + public int? SceneEpisodeNumber { get; set; } + public int? SceneSeasonNumber { get; set; } + public bool UnverifiedSceneNumbering { get; set; } + public DateTime? EndTime { get; set; } + public DateTime? GrabDate { get; set; } + public string SeriesTitle { get; set; } + public SeriesResource Series { get; set; } + + public List Images { get; set; } + + //Hiding this so people don't think its usable (only used to set the initial state) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Grabbed { get; set; } + } + + public static class EpisodeResourceMapper + { + public static EpisodeResource ToResource(this Episode model) + { + if (model == null) return null; + + return new EpisodeResource + { + Id = model.Id, + + SeriesId = model.SeriesId, + EpisodeFileId = model.EpisodeFileId, + SeasonNumber = model.SeasonNumber, + EpisodeNumber = model.EpisodeNumber, + Title = model.Title, + AirDate = model.AirDate, + AirDateUtc = model.AirDateUtc, + Overview = model.Overview, + //EpisodeFile + + HasFile = model.HasFile, + Monitored = model.Monitored, + AbsoluteEpisodeNumber = model.AbsoluteEpisodeNumber, + SceneAbsoluteEpisodeNumber = model.SceneAbsoluteEpisodeNumber, + SceneEpisodeNumber = model.SceneEpisodeNumber, + SceneSeasonNumber = model.SceneSeasonNumber, + UnverifiedSceneNumbering = model.UnverifiedSceneNumbering, + SeriesTitle = model.SeriesTitle, + //Series = model.Series.MapToResource(), + }; + } + + public static List ToResource(this IEnumerable models) + { + if (models == null) return null; + + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/EpisodesMonitoredResource.cs b/src/Sonarr.Api.V3/Episodes/EpisodesMonitoredResource.cs new file mode 100644 index 000000000..26f1db664 --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/EpisodesMonitoredResource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Sonarr.Api.V3.Episodes +{ + public class EpisodesMonitoredResource + { + public List EpisodeIds { get; set; } + public bool Monitored { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/RenameEpisodeModule.cs b/src/Sonarr.Api.V3/Episodes/RenameEpisodeModule.cs new file mode 100644 index 000000000..d714369d7 --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/RenameEpisodeModule.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using NzbDrone.Core.MediaFiles; +using Sonarr.Http; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Episodes +{ + public class RenameEpisodeModule : SonarrRestModule + { + private readonly IRenameEpisodeFileService _renameEpisodeFileService; + + public RenameEpisodeModule(IRenameEpisodeFileService renameEpisodeFileService) + : base("rename") + { + _renameEpisodeFileService = renameEpisodeFileService; + + GetResourceAll = GetEpisodes; + } + + private List GetEpisodes() + { + int seriesId; + + if (Request.Query.SeriesId.HasValue) + { + seriesId = (int)Request.Query.SeriesId; + } + + else + { + throw new BadRequestException("seriesId is missing"); + } + + if (Request.Query.SeasonNumber.HasValue) + { + var seasonNumber = (int)Request.Query.SeasonNumber; + return _renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber).ToResource(); + } + + return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs new file mode 100644 index 000000000..3f6ef14e0 --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Episodes +{ + public class RenameEpisodeResource : RestResource + { + public int SeriesId { get; set; } + public int SeasonNumber { get; set; } + public List EpisodeNumbers { get; set; } + public int EpisodeFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } + + public static class RenameEpisodeResourceMapper + { + public static RenameEpisodeResource ToResource(this NzbDrone.Core.MediaFiles.RenameEpisodeFilePreview model) + { + if (model == null) return null; + + return new RenameEpisodeResource + { + SeriesId = model.SeriesId, + SeasonNumber = model.SeasonNumber, + EpisodeNumbers = model.EpisodeNumbers.ToList(), + EpisodeFileId = model.EpisodeFileId, + ExistingPath = model.ExistingPath, + NewPath = model.NewPath + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs b/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs new file mode 100644 index 000000000..ddc5e3611 --- /dev/null +++ b/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using Nancy; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.FileSystem +{ + public class FileSystemModule : SonarrV3Module + { + private readonly IFileSystemLookupService _fileSystemLookupService; + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + + public FileSystemModule(IFileSystemLookupService fileSystemLookupService, + IDiskProvider diskProvider, + IDiskScanService diskScanService) + : base("/filesystem") + { + _fileSystemLookupService = fileSystemLookupService; + _diskProvider = diskProvider; + _diskScanService = diskScanService; + Get["/"] = x => GetContents(); + Get["/type"] = x => GetEntityType(); + Get["/mediafiles"] = x => GetMediaFiles(); + } + + private Response GetContents() + { + var pathQuery = Request.Query.path; + var includeFiles = Request.GetBooleanQueryParameter("includeFiles"); + var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes"); + + return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes).AsResponse(); + } + + private Response GetEntityType() + { + var pathQuery = Request.Query.path; + var path = (string)pathQuery.Value; + + if (_diskProvider.FileExists(path)) + { + return new { type = "file" }.AsResponse(); + } + + //Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system + return new { type = "folder" }.AsResponse(); + } + + private Response GetMediaFiles() + { + var pathQuery = Request.Query.path; + var path = (string)pathQuery.Value; + + if (!_diskProvider.FolderExists(path)) + { + return new string[0].AsResponse(); + } + + return _diskScanService.GetVideoFiles(path).Select(f => new { + Path = f, + RelativePath = path.GetRelativePath(f), + Name = Path.GetFileName(f) + }).AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Health/HealthModule.cs b/src/Sonarr.Api.V3/Health/HealthModule.cs new file mode 100644 index 000000000..bc5cece82 --- /dev/null +++ b/src/Sonarr.Api.V3/Health/HealthModule.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.HealthCheck; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Health +{ + public class HealthModule : SonarrRestModuleWithSignalR, + IHandle + { + private readonly IHealthCheckService _healthCheckService; + + public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) + : base(signalRBroadcaster) + { + _healthCheckService = healthCheckService; + GetResourceAll = GetHealth; + } + + private List GetHealth() + { + return _healthCheckService.Results().ToResource(); + } + + public void Handle(HealthCheckCompleteEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Health/HealthResource.cs b/src/Sonarr.Api.V3/Health/HealthResource.cs new file mode 100644 index 000000000..702c2cc10 --- /dev/null +++ b/src/Sonarr.Api.V3/Health/HealthResource.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Core.HealthCheck; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Health +{ + public class HealthResource : RestResource + { + public string Source { get; set; } + public HealthCheckResult Type { get; set; } + public string Message { get; set; } + public HttpUri WikiUrl { get; set; } + } + + public static class HealthResourceMapper + { + public static HealthResource ToResource(this HealthCheck model) + { + if (model == null) return null; + + return new HealthResource + { + Id = model.Id, + Source = model.Source.Name, + Type = model.Type, + Message = model.Message, + WikiUrl = model.WikiUrl + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/History/HistoryModule.cs b/src/Sonarr.Api.V3/History/HistoryModule.cs new file mode 100644 index 000000000..f4276746b --- /dev/null +++ b/src/Sonarr.Api.V3/History/HistoryModule.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using Sonarr.Api.V3.Episodes; +using Sonarr.Api.V3.Series; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.History +{ + public class HistoryModule : SonarrRestModule + { + private readonly IHistoryService _historyService; + private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IFailedDownloadService _failedDownloadService; + + public HistoryModule(IHistoryService historyService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IFailedDownloadService failedDownloadService) + { + _historyService = historyService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + _failedDownloadService = failedDownloadService; + GetResourcePaged = GetHistory; + + Get["/since"] = x => GetHistorySince(); + Get["/series"] = x => GetSeriesHistory(); + Post["/failed"] = x => MarkAsFailed(); + } + + protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeSeries, bool includeEpisode) + { + var resource = model.ToResource(); + + if (includeSeries) + { + resource.Series = model.Series.ToResource(); + } + + if (includeEpisode) + { + resource.Episode = model.Episode.ToResource(); + } + + if (model.Series != null) + { + resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Series.Profile.Value, model.Quality); + } + + return resource; + } + + private PagingResource GetHistory(PagingResource pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); + + var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType"); + var episodeIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "episodeId"); + + if (eventTypeFilter != null) + { + var filterValue = (HistoryEventType)Convert.ToInt32(eventTypeFilter.Value); + pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue); + } + + if (episodeIdFilter != null) + { + var episodeId = Convert.ToInt32(episodeIdFilter.Value); + pagingSpec.FilterExpressions.Add(h => h.EpisodeId == episodeId); + } + + return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeSeries, includeEpisode)); + } + + private List GetHistorySince() + { + var queryDate = Request.Query.Date; + var queryEventType = Request.Query.EventType; + + if (!queryDate.HasValue) + { + throw new BadRequestException("date is missing"); + } + + DateTime date = DateTime.Parse(queryDate.Value); + HistoryEventType? eventType = null; + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); + + if (queryEventType.HasValue) + { + eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); + } + + return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); + } + + private List GetSeriesHistory() + { + var querySeriesId = Request.Query.SeriesId; + var querySeasonNumber = Request.Query.SeasonNumber; + var queryEventType = Request.Query.EventType; + + if (!querySeriesId.HasValue) + { + throw new BadRequestException("seriesId is missing"); + } + + int seriesId = Convert.ToInt32(querySeriesId.Value); + HistoryEventType? eventType = null; + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); + + if (queryEventType.HasValue) + { + eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); + } + + if (querySeasonNumber.HasValue) + { + int seasonNumber = Convert.ToInt32(querySeasonNumber.Value); + + return _historyService.GetBySeason(seriesId, seasonNumber, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); + } + + return _historyService.GetBySeries(seriesId, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); + } + + private Response MarkAsFailed() + { + var id = (int)Request.Form.Id; + _failedDownloadService.MarkAsFailed(id); + return new object().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/History/HistoryResource.cs b/src/Sonarr.Api.V3/History/HistoryResource.cs new file mode 100644 index 000000000..85d421e37 --- /dev/null +++ b/src/Sonarr.Api.V3/History/HistoryResource.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.History; +using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Episodes; +using Sonarr.Api.V3.Series; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.History +{ + public class HistoryResource : RestResource + { + public int EpisodeId { get; set; } + public int SeriesId { get; set; } + public string SourceTitle { get; set; } + public QualityModel Quality { get; set; } + public bool QualityCutoffNotMet { get; set; } + public DateTime Date { get; set; } + public string DownloadId { get; set; } + + public HistoryEventType EventType { get; set; } + + public Dictionary Data { get; set; } + + public EpisodeResource Episode { get; set; } + public SeriesResource Series { get; set; } + } + + public static class HistoryResourceMapper + { + public static HistoryResource ToResource(this NzbDrone.Core.History.History model) + { + if (model == null) return null; + + return new HistoryResource + { + Id = model.Id, + + EpisodeId = model.EpisodeId, + SeriesId = model.SeriesId, + SourceTitle = model.SourceTitle, + Quality = model.Quality, + //QualityCutoffNotMet + Date = model.Date, + DownloadId = model.DownloadId, + + EventType = model.EventType, + + Data = model.Data + //Episode + //Series + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/IndexerModule.cs b/src/Sonarr.Api.V3/Indexers/IndexerModule.cs new file mode 100644 index 000000000..be93f4537 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Indexers; + +namespace Sonarr.Api.V3.Indexers +{ + public class IndexerModule : ProviderModuleBase + { + public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); + + public IndexerModule(IndexerFactory indexerFactory) + : base(indexerFactory, "indexer", ResourceMapper) + { + } + + protected override void Validate(IndexerDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Indexers/IndexerResource.cs b/src/Sonarr.Api.V3/Indexers/IndexerResource.cs new file mode 100644 index 000000000..a505e9367 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerResource.cs @@ -0,0 +1,43 @@ +using NzbDrone.Core.Indexers; + +namespace Sonarr.Api.V3.Indexers +{ + public class IndexerResource : ProviderResource + { + public bool EnableRss { get; set; } + public bool EnableSearch { get; set; } + public bool SupportsRss { get; set; } + public bool SupportsSearch { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public class IndexerResourceMapper : ProviderResourceMapper + { + public override IndexerResource ToResource(IndexerDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.EnableRss = definition.EnableRss; + resource.EnableSearch = definition.EnableSearch; + resource.SupportsRss = definition.SupportsRss; + resource.SupportsSearch = definition.SupportsSearch; + resource.Protocol = definition.Protocol; + + return resource; + } + + public override IndexerDefinition ToModel(IndexerResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.EnableRss = resource.EnableRss; + definition.EnableSearch = resource.EnableSearch; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs b/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs new file mode 100644 index 000000000..88875b8b9 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using Nancy; +using Nancy.ModelBinding; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; +using Sonarr.Http.Extensions; +using HttpStatusCode = System.Net.HttpStatusCode; + +namespace Sonarr.Api.V3.Indexers +{ + public class ReleaseModule : ReleaseModuleBase + { + private readonly IFetchAndParseRss _rssFetcherAndParser; + private readonly ISearchForNzb _nzbSearchService; + private readonly IMakeDownloadDecision _downloadDecisionMaker; + private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision; + private readonly IDownloadService _downloadService; + private readonly Logger _logger; + + private readonly ICached _remoteEpisodeCache; + + public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, + ISearchForNzb nzbSearchService, + IMakeDownloadDecision downloadDecisionMaker, + IPrioritizeDownloadDecision prioritizeDownloadDecision, + IDownloadService downloadService, + ICacheManager cacheManager, + Logger logger) + { + _rssFetcherAndParser = rssFetcherAndParser; + _nzbSearchService = nzbSearchService; + _downloadDecisionMaker = downloadDecisionMaker; + _prioritizeDownloadDecision = prioritizeDownloadDecision; + _downloadService = downloadService; + _logger = logger; + + GetResourceAll = GetReleases; + Post["/"] = x => DownloadRelease(this.Bind()); + + PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true); + PostValidator.RuleFor(s => s.IndexerId).ValidId(); + PostValidator.RuleFor(s => s.Guid).NotEmpty(); + + _remoteEpisodeCache = cacheManager.GetCache(GetType(), "remoteEpisodes"); + } + + private Response DownloadRelease(ReleaseResource release) + { + var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release)); + + if (remoteEpisode == null) + { + _logger.Debug("Couldn't find requested release in cache, cache timeout probably expired."); + + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Couldn't find requested release in cache, try searching again"); + } + + try + { + _downloadService.DownloadReport(remoteEpisode); + } + catch (ReleaseDownloadException ex) + { + _logger.ErrorException(ex.Message, ex); + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); + } + + return release.AsResponse(); + } + + private List GetReleases() + { + if (Request.Query.episodeId.HasValue) + { + return GetEpisodeReleases(Request.Query.episodeId); + } + + return GetRss(); + } + + private List GetEpisodeReleases(int episodeId) + { + try + { + var decisions = _nzbSearchService.EpisodeSearch(episodeId, true); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); + + return MapDecisions(prioritizedDecisions); + } + catch (Exception ex) + { + _logger.ErrorException("Episode search failed: " + ex.Message, ex); + } + + return new List(); + } + + private List GetRss() + { + var reports = _rssFetcherAndParser.Fetch(); + var decisions = _downloadDecisionMaker.GetRssDecision(reports); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); + + return MapDecisions(prioritizedDecisions); + } + + protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) + { + var resource = base.MapDecision(decision, initialWeight); + _remoteEpisodeCache.Set(GetCacheKey(resource), decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + + return resource; + } + + private string GetCacheKey(ReleaseResource resource) + { + return string.Concat(resource.IndexerId, "_", resource.Guid); + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs b/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs new file mode 100644 index 000000000..58c553862 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using NzbDrone.Core.DecisionEngine; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Indexers +{ + public abstract class ReleaseModuleBase : SonarrRestModule + { + protected virtual List MapDecisions(IEnumerable decisions) + { + var result = new List(); + + foreach (var downloadDecision in decisions) + { + var release = MapDecision(downloadDecision, result.Count); + + result.Add(release); + } + + return result; + } + + protected virtual ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) + { + var release = decision.ToResource(); + + release.ReleaseWeight = initialWeight; + + if (decision.RemoteEpisode.Series != null) + { + release.QualityWeight = decision.RemoteEpisode.Series + .Profile.Value + .Items.FindIndex(v => v.Quality == release.Quality.Quality) * 100; + } + + release.QualityWeight += release.Quality.Revision.Real * 10; + release.QualityWeight += release.Quality.Revision.Version; + + return release; + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs new file mode 100644 index 000000000..04a29c7f9 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using Nancy; +using Nancy.ModelBinding; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Indexers +{ + class ReleasePushModule : ReleaseModuleBase + { + private readonly IMakeDownloadDecision _downloadDecisionMaker; + private readonly IProcessDownloadDecisions _downloadDecisionProcessor; + private readonly Logger _logger; + + public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker, + IProcessDownloadDecisions downloadDecisionProcessor, + Logger logger) + { + _downloadDecisionMaker = downloadDecisionMaker; + _downloadDecisionProcessor = downloadDecisionProcessor; + _logger = logger; + + Post["/push"] = x => ProcessRelease(this.Bind()); + + PostValidator.RuleFor(s => s.Title).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadProtocol).NotEmpty(); + PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); + } + + private Response ProcessRelease(ReleaseResource release) + { + _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl); + + var info = release.ToModel(); + + info.Guid = "PUSH-" + info.DownloadUrl; + + var decisions = _downloadDecisionMaker.GetRssDecision(new List { info }); + _downloadDecisionProcessor.ProcessDecisions(decisions); + + return MapDecisions(decisions).First().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs new file mode 100644 index 000000000..62296ab8e --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Indexers +{ + public class ReleaseResource : RestResource + { + public string Guid { get; set; } + public QualityModel Quality { get; set; } + public int QualityWeight { get; set; } + public int Age { get; set; } + public double AgeHours { get; set; } + public double AgeMinutes { get; set; } + public long Size { get; set; } + public int IndexerId { get; set; } + public string Indexer { get; set; } + public string ReleaseGroup { get; set; } + public string SubGroup { get; set; } + public string ReleaseHash { get; set; } + public string Title { get; set; } + public bool FullSeason { get; set; } + public bool SceneSource { get; set; } + public int SeasonNumber { get; set; } + public Language Language { get; set; } + public string AirDate { get; set; } + public string SeriesTitle { get; set; } + public int[] EpisodeNumbers { get; set; } + public int[] AbsoluteEpisodeNumbers { get; set; } + public bool Approved { get; set; } + public bool TemporarilyRejected { get; set; } + public bool Rejected { get; set; } + public int TvdbId { get; set; } + public int TvRageId { get; set; } + public IEnumerable Rejections { get; set; } + public DateTime PublishDate { get; set; } + public string CommentUrl { get; set; } + public string DownloadUrl { get; set; } + public string InfoUrl { get; set; } + public bool DownloadAllowed { get; set; } + public int ReleaseWeight { get; set; } + + public string MagnetUrl { get; set; } + public string InfoHash { get; set; } + public int? Seeders { get; set; } + public int? Leechers { get; set; } + public DownloadProtocol Protocol { get; set; } + + //TODO: besides a test I don't think this is used... + public DownloadProtocol DownloadProtocol { get; set; } + + public bool IsDaily { get; set; } + public bool IsAbsoluteNumbering { get; set; } + public bool IsPossibleSpecialEpisode { get; set; } + public bool Special { get; set; } + } + + public static class ReleaseResourceMapper + { + public static ReleaseResource ToResource(this DownloadDecision model) + { + var releaseInfo = model.RemoteEpisode.Release; + var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo; + var remoteEpisode = model.RemoteEpisode; + var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo(); + + // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) + return new ReleaseResource + { + Guid = releaseInfo.Guid, + Quality = parsedEpisodeInfo.Quality, + //QualityWeight + Age = releaseInfo.Age, + AgeHours = releaseInfo.AgeHours, + AgeMinutes = releaseInfo.AgeMinutes, + Size = releaseInfo.Size, + IndexerId = releaseInfo.IndexerId, + Indexer = releaseInfo.Indexer, + ReleaseGroup = parsedEpisodeInfo.ReleaseGroup, + ReleaseHash = parsedEpisodeInfo.ReleaseHash, + Title = releaseInfo.Title, + FullSeason = parsedEpisodeInfo.FullSeason, + SeasonNumber = parsedEpisodeInfo.SeasonNumber, + Language = parsedEpisodeInfo.Language, + AirDate = parsedEpisodeInfo.AirDate, + SeriesTitle = parsedEpisodeInfo.SeriesTitle, + EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers, + AbsoluteEpisodeNumbers = parsedEpisodeInfo.AbsoluteEpisodeNumbers, + Approved = model.Approved, + TemporarilyRejected = model.TemporarilyRejected, + Rejected = model.Rejected, + TvdbId = releaseInfo.TvdbId, + TvRageId = releaseInfo.TvRageId, + Rejections = model.Rejections.Select(r => r.Reason).ToList(), + PublishDate = releaseInfo.PublishDate, + CommentUrl = releaseInfo.CommentUrl, + DownloadUrl = releaseInfo.DownloadUrl, + InfoUrl = releaseInfo.InfoUrl, + DownloadAllowed = remoteEpisode.DownloadAllowed, + //ReleaseWeight + + + MagnetUrl = torrentInfo.MagnetUrl, + InfoHash = torrentInfo.InfoHash, + Seeders = torrentInfo.Seeders, + Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, + Protocol = releaseInfo.DownloadProtocol, + + IsDaily = parsedEpisodeInfo.IsDaily, + IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering, + IsPossibleSpecialEpisode = parsedEpisodeInfo.IsPossibleSpecialEpisode, + Special = parsedEpisodeInfo.Special, + }; + + } + + public static ReleaseInfo ToModel(this ReleaseResource resource) + { + ReleaseInfo model; + + if (resource.Protocol == DownloadProtocol.Torrent) + { + model = new TorrentInfo + { + MagnetUrl = resource.MagnetUrl, + InfoHash = resource.InfoHash, + Seeders = resource.Seeders, + Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null + }; + } + else + { + model = new ReleaseInfo(); + } + + model.Guid = resource.Guid; + model.Title = resource.Title; + model.Size = resource.Size; + model.DownloadUrl = resource.DownloadUrl; + model.InfoUrl = resource.InfoUrl; + model.CommentUrl = resource.CommentUrl; + model.IndexerId = resource.IndexerId; + model.Indexer = resource.Indexer; + model.DownloadProtocol = resource.DownloadProtocol; + model.TvdbId = resource.TvdbId; + model.TvRageId = resource.TvRageId; + model.PublishDate = resource.PublishDate.ToUniversalTime(); + + return model; + } + } +} diff --git a/src/Sonarr.Api.V3/Logs/LogFileModule.cs b/src/Sonarr.Api.V3/Logs/LogFileModule.cs new file mode 100644 index 000000000..549d1b595 --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogFileModule.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Api.V3.Logs +{ + public class LogFileModule : LogFileModuleBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public LogFileModule(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider) + : base(diskProvider, configFileProvider, "") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + } + + protected override IEnumerable GetLogFiles() + { + return _diskProvider.GetFiles(_appFolderInfo.GetLogFolder(), SearchOption.TopDirectoryOnly); + } + + protected override string GetLogFilePath(string filename) + { + return Path.Combine(_appFolderInfo.GetLogFolder(), filename); + } + + protected override string DownloadUrlRoot + { + get + { + return "logfile"; + } + } + + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs b/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs new file mode 100644 index 000000000..adc5d6cbb --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Logs +{ + public abstract class LogFileModuleBase : SonarrRestModule + { + protected const string LOGFILE_ROUTE = @"/(?[-.a-zA-Z0-9]+?\.txt)"; + + private readonly IDiskProvider _diskProvider; + private readonly IConfigFileProvider _configFileProvider; + + public LogFileModuleBase(IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + string route) + : base("log/file" + route) + { + _diskProvider = diskProvider; + _configFileProvider = configFileProvider; + GetResourceAll = GetLogFilesResponse; + + Get[LOGFILE_ROUTE] = options => GetLogFileResponse(options.filename); + } + + private List GetLogFilesResponse() + { + var result = new List(); + + var files = GetLogFiles().ToList(); + + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + var filename = Path.GetFileName(file); + + result.Add(new LogFileResource + { + Id = i + 1, + Filename = filename, + LastWriteTime = _diskProvider.FileGetLastWrite(file), + ContentsUrl = string.Format("{0}/api/v3/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), + DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename) + }); + } + + return result.OrderByDescending(l => l.LastWriteTime).ToList(); + } + + private Response GetLogFileResponse(string filename) + { + var filePath = GetLogFilePath(filename); + + if (!_diskProvider.FileExists(filePath)) + return new NotFoundResponse(); + + var data = _diskProvider.ReadAllText(filePath); + + return new TextResponse(data); + } + + protected abstract IEnumerable GetLogFiles(); + protected abstract string GetLogFilePath(string filename); + + protected abstract string DownloadUrlRoot { get; } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Logs/LogFileResource.cs b/src/Sonarr.Api.V3/Logs/LogFileResource.cs new file mode 100644 index 000000000..7683dc368 --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogFileResource.cs @@ -0,0 +1,13 @@ +using System; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Logs +{ + public class LogFileResource : RestResource + { + public string Filename { get; set; } + public DateTime LastWriteTime { get; set; } + public string ContentsUrl { get; set; } + public string DownloadUrl { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Logs/LogModule.cs b/src/Sonarr.Api.V3/Logs/LogModule.cs new file mode 100644 index 000000000..c0800a8ca --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogModule.cs @@ -0,0 +1,63 @@ +using System.Linq; +using NzbDrone.Core.Instrumentation; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Logs +{ + public class LogModule : SonarrRestModule + { + private readonly ILogService _logService; + + public LogModule(ILogService logService) + { + _logService = logService; + GetResourcePaged = GetLogs; + } + + private PagingResource GetLogs(PagingResource pagingResource) + { + var pageSpec = pagingResource.MapToPagingSpec(); + + if (pageSpec.SortKey == "time") + { + pageSpec.SortKey = "id"; + } + + var levelFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "level"); + + if (levelFilter != null) + { + switch (levelFilter.Value) + { + case "fatal": + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal"); + break; + case "error": + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error"); + break; + case "warn": + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn"); + break; + case "info": + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info"); + break; + case "debug": + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug"); + break; + case "trace": + pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace"); + break; + } + } + + var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); + + if (pageSpec.SortKey == "id") + { + response.SortKey = "time"; + } + + return response; + } + } +} diff --git a/src/Sonarr.Api.V3/Logs/LogResource.cs b/src/Sonarr.Api.V3/Logs/LogResource.cs new file mode 100644 index 000000000..86d835dfc --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogResource.cs @@ -0,0 +1,36 @@ +using System; +using NzbDrone.Core.Instrumentation; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Logs +{ + public class LogResource : RestResource + { + public DateTime Time { get; set; } + public string Exception { get; set; } + public string ExceptionType { get; set; } + public string Level { get; set; } + public string Logger { get; set; } + public string Message { get; set; } + public string Method { get; set; } + } + + public static class LogResourceMapper + { + public static LogResource ToResource(this Log model) + { + if (model == null) return null; + + return new LogResource + { + Id = model.Id, + Time = model.Time, + Exception = model.Exception, + ExceptionType = model.ExceptionType, + Level = model.Level.ToLowerInvariant(), + Logger = model.Logger, + Message = model.Message + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs b/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs new file mode 100644 index 000000000..0b39844bc --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Api.V3.Logs +{ + public class UpdateLogFileModule : LogFileModuleBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public UpdateLogFileModule(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider) + : base(diskProvider, configFileProvider, "/update") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + } + + protected override IEnumerable GetLogFiles() + { + if (!_diskProvider.FolderExists(_appFolderInfo.GetUpdateLogFolder())) return Enumerable.Empty(); + + return _diskProvider.GetFiles(_appFolderInfo.GetUpdateLogFolder(), SearchOption.TopDirectoryOnly) + .Where(f => Regex.IsMatch(Path.GetFileName(f), LOGFILE_ROUTE.TrimStart('/'), RegexOptions.IgnoreCase)) + .ToList(); + } + + protected override string GetLogFilePath(string filename) + { + return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), filename); + } + + protected override string DownloadUrlRoot + { + get + { + return "updatelogfile"; + } + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs new file mode 100644 index 000000000..f3b189f40 --- /dev/null +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; +using NzbDrone.Core.Qualities; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.ManualImport +{ + public class ManualImportModule : SonarrRestModule + { + private readonly IManualImportService _manualImportService; + + public ManualImportModule(IManualImportService manualImportService) + : base("/manualimport") + { + _manualImportService = manualImportService; + + GetResourceAll = GetMediaFiles; + } + + private List GetMediaFiles() + { + var folder = (string)Request.Query.folder; + var downloadId = (string)Request.Query.downloadId; + var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true); + + return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); + } + + private ManualImportResource AddQualityWeight(ManualImportResource item) + { + if (item.Quality != null) + { + item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight; + item.QualityWeight += item.Quality.Revision.Real * 10; + item.QualityWeight += item.Quality.Revision.Version; + } + + return item; + } + } +} diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs new file mode 100644 index 000000000..593e98cee --- /dev/null +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Crypto; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; +using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Episodes; +using Sonarr.Api.V3.Series; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.ManualImport +{ + public class ManualImportResource : RestResource + { + public string Path { get; set; } + public string RelativePath { get; set; } + public string FolderName { get; set; } + public string Name { get; set; } + public long Size { get; set; } + public SeriesResource Series { get; set; } + public int? SeasonNumber { get; set; } + public List Episodes { get; set; } + public QualityModel Quality { get; set; } + public Language Language { get; set; } + public int QualityWeight { get; set; } + public string DownloadId { get; set; } + public IEnumerable Rejections { get; set; } + } + + public static class ManualImportResourceMapper + { + public static ManualImportResource ToResource(this ManualImportItem model) + { + if (model == null) return null; + + return new ManualImportResource + { + Id = HashConverter.GetHashInt31(model.Path), + Path = model.Path, + RelativePath = model.RelativePath, + FolderName = model.FolderName, + Name = model.Name, + Size = model.Size, + Series = model.Series.ToResource(), + SeasonNumber = model.SeasonNumber, + Episodes = model.Episodes.ToResource(), + Quality = model.Quality, + Language = model.Language, + //QualityWeight + DownloadId = model.DownloadId, + Rejections = model.Rejections + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs b/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs new file mode 100644 index 000000000..ef4f1438a --- /dev/null +++ b/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Text.RegularExpressions; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; + +namespace Sonarr.Api.V3.MediaCovers +{ + public class MediaCoverModule : SonarrV3Module + { + private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private const string MEDIA_COVER_ROUTE = @"/(?\d+)/(?(.+)\.(jpg|png|gif))"; + + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) : base("MediaCover") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + + Get[MEDIA_COVER_ROUTE] = options => GetMediaCover(options.seriesId, options.filename); + } + + private Response GetMediaCover(int seriesId, string filename) + { + var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", seriesId.ToString(), filename); + + if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0) + { + // Return the full sized image if someone requests a non-existing resized one. + // TODO: This code can be removed later once everyone had the update for a while. + var basefilePath = RegexResizedImage.Replace(filePath, ".jpg"); + if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) + { + return new NotFoundResponse(); + } + filePath = basefilePath; + } + + return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); + } + } +} diff --git a/src/Sonarr.Api.V3/Metadata/MetadataModule.cs b/src/Sonarr.Api.V3/Metadata/MetadataModule.cs new file mode 100644 index 000000000..107588f57 --- /dev/null +++ b/src/Sonarr.Api.V3/Metadata/MetadataModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Sonarr.Api.V3.Metadata +{ + public class MetadataModule : ProviderModuleBase + { + public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); + + public MetadataModule(IMetadataFactory metadataFactory) + : base(metadataFactory, "metadata", ResourceMapper) + { + } + + protected override void Validate(MetadataDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Metadata/MetadataResource.cs b/src/Sonarr.Api.V3/Metadata/MetadataResource.cs new file mode 100644 index 000000000..4a1faa20c --- /dev/null +++ b/src/Sonarr.Api.V3/Metadata/MetadataResource.cs @@ -0,0 +1,34 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Sonarr.Api.V3.Metadata +{ + public class MetadataResource : ProviderResource + { + public bool Enable { get; set; } + } + + public class MetadataResourceMapper : ProviderResourceMapper + { + public override MetadataResource ToResource(MetadataDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + + return resource; + } + + public override MetadataDefinition ToModel(MetadataResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Notifications/NotificationModule.cs b/src/Sonarr.Api.V3/Notifications/NotificationModule.cs new file mode 100644 index 000000000..2b9a3541c --- /dev/null +++ b/src/Sonarr.Api.V3/Notifications/NotificationModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Notifications; + +namespace Sonarr.Api.V3.Notifications +{ + public class NotificationModule : ProviderModuleBase + { + public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); + + public NotificationModule(NotificationFactory notificationFactory) + : base(notificationFactory, "notification", ResourceMapper) + { + } + + protected override void Validate(NotificationDefinition definition, bool includeWarnings) + { + if (!definition.OnGrab && !definition.OnDownload) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs new file mode 100644 index 000000000..bcc09637f --- /dev/null +++ b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs @@ -0,0 +1,57 @@ +using NzbDrone.Core.Notifications; + +namespace Sonarr.Api.V3.Notifications +{ + public class NotificationResource : ProviderResource + { + public string Link { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public string TestCommand { get; set; } + } + + public class NotificationResourceMapper : ProviderResourceMapper + { + public override NotificationResource ToResource(NotificationDefinition definition) + { + if (definition == null) return default(NotificationResource); + + var resource = base.ToResource(definition); + + resource.OnGrab = definition.OnGrab; + resource.OnDownload = definition.OnDownload; + resource.OnUpgrade = definition.OnUpgrade; + resource.OnRename = definition.OnRename; + resource.SupportsOnGrab = definition.SupportsOnGrab; + resource.SupportsOnDownload = definition.SupportsOnDownload; + resource.SupportsOnUpgrade = definition.SupportsOnUpgrade; + resource.SupportsOnRename = definition.SupportsOnRename; + + return resource; + } + + public override NotificationDefinition ToModel(NotificationResource resource) + { + if (resource == null) return default(NotificationDefinition); + + var definition = base.ToModel(resource); + + definition.OnGrab = resource.OnGrab; + definition.OnDownload = resource.OnDownload; + definition.OnUpgrade = resource.OnUpgrade; + definition.OnRename = resource.OnRename; + definition.SupportsOnGrab = resource.SupportsOnGrab; + definition.SupportsOnDownload = resource.SupportsOnDownload; + definition.SupportsOnUpgrade = resource.SupportsOnUpgrade; + definition.SupportsOnRename = resource.SupportsOnRename; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Parse/ParseModule.cs b/src/Sonarr.Api.V3/Parse/ParseModule.cs new file mode 100644 index 000000000..05cef8534 --- /dev/null +++ b/src/Sonarr.Api.V3/Parse/ParseModule.cs @@ -0,0 +1,53 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Parser; +using Sonarr.Api.V3.Episodes; +using Sonarr.Api.V3.Series; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Parse +{ + public class ParseModule : SonarrRestModule + { + private readonly IParsingService _parsingService; + + public ParseModule(IParsingService parsingService) + { + _parsingService = parsingService; + + GetResourceSingle = Parse; + } + + private ParseResource Parse() + { + var title = Request.Query.Title.Value as string; + var path = Request.Query.Path.Value as string; + var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParsePath(path) : Parser.ParseTitle(title); + + if (parsedEpisodeInfo == null) + { + return null; + } + + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); + + if (remoteEpisode != null) + { + return new ParseResource + { + Title = title, + ParsedEpisodeInfo = remoteEpisode.ParsedEpisodeInfo, + Series = remoteEpisode.Series.ToResource(), + Episodes = remoteEpisode.Episodes.ToResource() + }; + } + else + { + return new ParseResource + { + Title = title, + ParsedEpisodeInfo = parsedEpisodeInfo + }; + } + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Parse/ParseResource.cs b/src/Sonarr.Api.V3/Parse/ParseResource.cs new file mode 100644 index 000000000..8dc8b9cc7 --- /dev/null +++ b/src/Sonarr.Api.V3/Parse/ParseResource.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using NzbDrone.Core.Parser.Model; +using Sonarr.Api.V3.Episodes; +using Sonarr.Api.V3.Series; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Parse +{ + public class ParseResource : RestResource + { + public string Title { get; set; } + public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } + public SeriesResource Series { get; set; } + public List Episodes { get; set; } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs new file mode 100644 index 000000000..9613c75bd --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using FluentValidation.Results; +using Nancy; +using NzbDrone.Core.Profiles.Delay; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.Validation; + +namespace Sonarr.Api.V3.Profiles.Delay +{ + public class DelayProfileModule : SonarrRestModule + { + private readonly IDelayProfileService _delayProfileService; + + public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) + { + _delayProfileService = delayProfileService; + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + Put[@"/reorder/(?[\d]{1,10})"] = options => Reorder(options.Id); + + SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); + SharedValidator.RuleFor(d => d.Tags).EmptyCollection().When(d => d.Id == 1); + SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator); + SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0); + SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0); + + SharedValidator.Custom(delayProfile => + { + if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent) + { + return new ValidationFailure("", "Either Usenet or Torrent should be enabled"); + } + + return null; + }); + } + + private int Create(DelayProfileResource resource) + { + var model = resource.ToModel(); + model = _delayProfileService.Add(model); + + return model.Id; + } + + private void DeleteProfile(int id) + { + if (id == 1) + { + throw new MethodNotAllowedException("Cannot delete global delay profile"); + } + + _delayProfileService.Delete(id); + } + + private void Update(DelayProfileResource resource) + { + var model = resource.ToModel(); + _delayProfileService.Update(model); + } + + private DelayProfileResource GetById(int id) + { + return _delayProfileService.Get(id).ToResource(); + } + + private List GetAll() + { + return _delayProfileService.All().ToResource(); + } + + private Response Reorder(int id) + { + ValidateId(id); + + var afterIdQuery = Request.Query.After; + int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null; + + return _delayProfileService.Reorder(id, afterId).ToResource().AsResponse(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs new file mode 100644 index 000000000..4ce6ec189 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Profiles.Delay; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Profiles.Delay +{ + public class DelayProfileResource : RestResource + { + public bool EnableUsenet { get; set; } + public bool EnableTorrent { get; set; } + public DownloadProtocol PreferredProtocol { get; set; } + public int UsenetDelay { get; set; } + public int TorrentDelay { get; set; } + public int Order { get; set; } + public HashSet Tags { get; set; } + } + + public static class DelayProfileResourceMapper + { + public static DelayProfileResource ToResource(this DelayProfile model) + { + if (model == null) return null; + + return new DelayProfileResource + { + Id = model.Id, + + EnableUsenet = model.EnableUsenet, + EnableTorrent = model.EnableTorrent, + PreferredProtocol = model.PreferredProtocol, + UsenetDelay = model.UsenetDelay, + TorrentDelay = model.TorrentDelay, + Order = model.Order, + Tags = new HashSet(model.Tags) + }; + } + + public static DelayProfile ToModel(this DelayProfileResource resource) + { + if (resource == null) return null; + + return new DelayProfile + { + Id = resource.Id, + + EnableUsenet = resource.EnableUsenet, + EnableTorrent = resource.EnableTorrent, + PreferredProtocol = resource.PreferredProtocol, + UsenetDelay = resource.UsenetDelay, + TorrentDelay = resource.TorrentDelay, + Order = resource.Order, + Tags = new HashSet(resource.Tags) + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Languages/LanguageModule.cs b/src/Sonarr.Api.V3/Profiles/Languages/LanguageModule.cs new file mode 100644 index 000000000..9d5a09ff0 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Languages/LanguageModule.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Parser; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Profiles.Languages +{ + public class LanguageModule : SonarrRestModule + { + public LanguageModule() + { + GetResourceAll = GetAll; + GetResourceById = GetById; + } + + private LanguageResource GetById(int id) + { + var language = (Language)id; + + return new LanguageResource + { + Id = (int)language, + Name = language.ToString() + }; + } + + private List GetAll() + { + return ((Language[])Enum.GetValues(typeof (Language))) + .Select(l => new LanguageResource + { + Id = (int) l, + Name = l.ToString() + }) + .OrderBy(l => l.Name) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Languages/LanguageResource.cs b/src/Sonarr.Api.V3/Profiles/Languages/LanguageResource.cs new file mode 100644 index 000000000..a4452ae29 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Languages/LanguageResource.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Profiles.Languages +{ + public class LanguageResource : RestResource + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public int Id { get; set; } + public string Name { get; set; } + public string NameLower { get { return Name.ToLowerInvariant(); } } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs new file mode 100644 index 000000000..359c91306 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Validation; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + public class ProfileModule : SonarrRestModule + { + private readonly IProfileService _profileService; + + public ProfileModule(IProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); + SharedValidator.RuleFor(c => c.Language).ValidLanguage(); + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + } + + private int Create(QualityProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return model.Id; + } + + private void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + private void Update(QualityProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + } + + private QualityProfileResource GetById(int id) + { + return _profileService.Get(id).ToResource(); + } + + private List GetAll() + { + return _profileService.All().ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs new file mode 100644 index 000000000..809b286a0 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Profiles; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + public class QualityProfileResource : RestResource + { + public string Name { get; set; } + public NzbDrone.Core.Qualities.Quality Cutoff { get; set; } + public List Items { get; set; } + public Language Language { get; set; } + } + + public class QualityProfileQualityItemResource : RestResource + { + public NzbDrone.Core.Qualities.Quality Quality { get; set; } + public bool Allowed { get; set; } + } + + public static class ProfileResourceMapper + { + public static QualityProfileResource ToResource(this Profile model) + { + if (model == null) return null; + + return new QualityProfileResource + { + Id = model.Id, + + Name = model.Name, + Cutoff = model.Cutoff, + Items = model.Items.ConvertAll(ToResource), + Language = model.Language + }; + } + + public static QualityProfileQualityItemResource ToResource(this ProfileQualityItem model) + { + if (model == null) return null; + + return new QualityProfileQualityItemResource + { + Quality = model.Quality, + Allowed = model.Allowed + }; + } + + public static Profile ToModel(this QualityProfileResource resource) + { + if (resource == null) return null; + + return new Profile + { + Id = resource.Id, + + Name = resource.Name, + Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id, + Items = resource.Items.ConvertAll(ToModel), + Language = resource.Language + }; + } + + public static ProfileQualityItem ToModel(this QualityProfileQualityItemResource resource) + { + if (resource == null) return null; + + return new ProfileQualityItem + { + Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id, + Allowed = resource.Allowed + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs new file mode 100644 index 000000000..4a02612bf --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs @@ -0,0 +1,36 @@ +using System.Linq; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + public class QualityProfileSchemaModule : SonarrRestModule + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) + : base("/qualityprofile/schema") + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceSingle = GetSchema; + } + + private QualityProfileResource GetSchema() + { + var items = _qualityDefinitionService.All() + .OrderBy(v => v.Weight) + .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) + .ToList(); + + var qualityProfile = new Profile(); + qualityProfile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown; + qualityProfile.Items = items; + profile.Language = Language.English; + + return qualityProfile.ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs new file mode 100644 index 000000000..f1f62fa89 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + public static class QualityProfileValidation + { + public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + + return ruleBuilder.SetValidator(new AllowedValidator()); + } + } + + public class AllowedValidator : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Sonarr.Api.V3/Properties/AssemblyInfo.cs b/src/Sonarr.Api.V3/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6149a06c4 --- /dev/null +++ b/src/Sonarr.Api.V3/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("NzbDrone.Api")] + +[assembly: Guid("4c0922d7-979e-4ff7-b44b-b8ac2100eeb5")] + +[assembly: AssemblyVersion("10.0.0.*")] + +[assembly: InternalsVisibleTo("NzbDrone.Core")] diff --git a/src/Sonarr.Api.V3/ProviderModuleBase.cs b/src/Sonarr.Api.V3/ProviderModuleBase.cs new file mode 100644 index 000000000..b4c750a79 --- /dev/null +++ b/src/Sonarr.Api.V3/ProviderModuleBase.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Nancy; +using Newtonsoft.Json; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3 +{ + public abstract class ProviderModuleBase : SonarrRestModule + where TProviderDefinition : ProviderDefinition, new() + where TProvider : IProvider + where TProviderResource : ProviderResource, new() + { + private readonly IProviderFactory _providerFactory; + private readonly ProviderResourceMapper _resourceMapper; + + protected ProviderModuleBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) + : base(resource) + { + _providerFactory = providerFactory; + _resourceMapper = resourceMapper; + + Get["schema"] = x => GetTemplates(); + Post["test"] = x => Test(ReadResourceFromRequest(true)); + Post["testall"] = x => TestAll(); + Post["action/{action}"] = x => RequestAction(x.action, ReadResourceFromRequest(true)); + + GetResourceAll = GetAll; + GetResourceById = GetProviderById; + CreateResource = CreateProvider; + UpdateResource = UpdateProvider; + DeleteResource = DeleteProvider; + + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); + SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); + SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); + + PostValidator.RuleFor(c => c.Fields).NotNull(); + } + + private TProviderResource GetProviderById(int id) + { + var definition = _providerFactory.Get(id); + _providerFactory.SetProviderCharacteristics(definition); + + return _resourceMapper.ToResource(definition); + } + + private List GetAll() + { + var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); + + var result = new List(providerDefinitions.Count()); + + foreach (var definition in providerDefinitions) + { + _providerFactory.SetProviderCharacteristics(definition); + + result.Add(_resourceMapper.ToResource(definition)); + } + + return result.OrderBy(p => p.Name).ToList(); + } + + private int CreateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + providerDefinition = _providerFactory.Create(providerDefinition); + + return providerDefinition.Id; + } + + private void UpdateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + _providerFactory.Update(providerDefinition); + } + + private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) + { + var definition = _resourceMapper.ToModel(providerResource); + + if (validate) + { + Validate(definition, includeWarnings); + } + + return definition; + } + + private void DeleteProvider(int id) + { + _providerFactory.Delete(id); + } + + private Response GetTemplates() + { + var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); + + var result = new List(defaultDefinitions.Count()); + + foreach (var providerDefinition in defaultDefinitions) + { + var providerResource = _resourceMapper.ToResource(providerDefinition); + var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); + + providerResource.Presets = presetDefinitions.Select(v => + { + var presetResource = _resourceMapper.ToResource(v); + + return presetResource as ProviderResource; + }).ToList(); + + result.Add(providerResource); + } + + return result.AsResponse(); + } + + private Response Test(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true); + + Test(providerDefinition, true); + + return "{}"; + } + + private Response TestAll() + { + var providerDefinitions = _providerFactory.All() + .Where(c => c.Settings.Validate().IsValid && c.Enable) + .ToList(); + var result = new List(); + + foreach (var definition in providerDefinitions) + { + var validationResult = _providerFactory.Test(definition); + + result.Add(new ProviderTestAllResult + { + Id = definition.Id, + ValidationFailures = validationResult.Errors.ToList() + }); + } + + return result.AsResponse(result.Any(c => !c.IsValid) ? HttpStatusCode.BadRequest : HttpStatusCode.OK); + } + + private Response RequestAction(string action, TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true, false); + + var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); + + var data = _providerFactory.RequestAction(providerDefinition, action, query); + Response resp = JsonConvert.SerializeObject(data); + resp.ContentType = "application/json"; + return resp; + } + + protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = definition.Settings.Validate(); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected virtual void Test(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = _providerFactory.Test(definition); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) + { + var result = new NzbDroneValidationResult(validationResult.Errors); + + if (includeWarnings && (!result.IsValid || result.HasWarnings)) + { + throw new ValidationException(result.Failures); + } + + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + } + } +} diff --git a/src/Sonarr.Api.V3/ProviderResource.cs b/src/Sonarr.Api.V3/ProviderResource.cs new file mode 100644 index 000000000..db239cecd --- /dev/null +++ b/src/Sonarr.Api.V3/ProviderResource.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.ThingiProvider; +using Sonarr.Http.ClientSchema; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3 +{ + public class ProviderResource : RestResource + { + public string Name { get; set; } + public List Fields { get; set; } + public string ImplementationName { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public string InfoLink { get; set; } + public ProviderMessage Message { get; set; } + public HashSet Tags { get; set; } + + public List Presets { get; set; } + } + + public class ProviderResourceMapper + where TProviderResource : ProviderResource, new() + where TProviderDefinition : ProviderDefinition, new() + { + public virtual TProviderResource ToResource(TProviderDefinition definition) + + { + return new TProviderResource + { + Id = definition.Id, + + Name = definition.Name, + ImplementationName = definition.ImplementationName, + Implementation = definition.Implementation, + ConfigContract = definition.ConfigContract, + Message = definition.Message, + Tags = definition.Tags, + Fields = SchemaBuilder.ToSchema(definition.Settings), + + InfoLink = string.Format("https://github.com/Sonarr/Sonarr/wiki/Supported-{0}#{1}", + typeof(TProviderResource).Name.Replace("Resource", "s"), + definition.Implementation.ToLower()) + }; + } + + public virtual TProviderDefinition ToModel(TProviderResource resource) + { + if (resource == null) return default(TProviderDefinition); + + var definition = new TProviderDefinition + { + Id = resource.Id, + + Name = resource.Name, + ImplementationName = resource.ImplementationName, + Implementation = resource.Implementation, + ConfigContract = resource.ConfigContract, + Message = resource.Message, + Tags = resource.Tags + }; + + var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract); + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/ProviderTestAllResult.cs b/src/Sonarr.Api.V3/ProviderTestAllResult.cs new file mode 100644 index 000000000..7450f631f --- /dev/null +++ b/src/Sonarr.Api.V3/ProviderTestAllResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; + +namespace Sonarr.Api.V3 +{ + public class ProviderTestAllResult + { + public int Id { get; set; } + public bool IsValid => ValidationFailures.Empty(); + public List ValidationFailures { get; set; } + + public ProviderTestAllResult() + { + ValidationFailures = new List(); + } + } +} diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs new file mode 100644 index 000000000..d38df05fb --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.Qualities; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Qualities +{ + public class QualityDefinitionModule : SonarrRestModule + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + Put["/update"] = d => UpdateMany(); + } + + private void Update(QualityDefinitionResource resource) + { + var model = resource.ToModel(); + _qualityDefinitionService.Update(model); + } + + private QualityDefinitionResource GetById(int id) + { + return _qualityDefinitionService.GetById(id).ToResource(); + } + + private List GetAll() + { + return _qualityDefinitionService.All().ToResource(); + } + + private Response UpdateMany() + { + //Read from request + var qualityDefinitions = Request.Body.FromJson>() + .ToModel() + .ToList(); + + _qualityDefinitionService.UpdateMany(qualityDefinitions); + + return _qualityDefinitionService.All() + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs new file mode 100644 index 000000000..85c5310a6 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Qualities; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Qualities +{ + public class QualityDefinitionResource : RestResource + { + public Quality Quality { get; set; } + + public string Title { get; set; } + + public int Weight { get; set; } + + public double? MinSize { get; set; } + public double? MaxSize { get; set; } + } + + public static class QualityDefinitionResourceMapper + { + public static QualityDefinitionResource ToResource(this QualityDefinition model) + { + if (model == null) return null; + + return new QualityDefinitionResource + { + Id = model.Id, + Quality = model.Quality, + Title = model.Title, + Weight = model.Weight, + MinSize = model.MinSize, + MaxSize = model.MaxSize + }; + } + + public static QualityDefinition ToModel(this QualityDefinitionResource resource) + { + if (resource == null) return null; + + return new QualityDefinition + { + Id = resource.Id, + Quality = resource.Quality, + Title = resource.Title, + Weight = resource.Weight, + MinSize = resource.MinSize, + MaxSize = resource.MaxSize + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Queue/QueueActionModule.cs b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs new file mode 100644 index 000000000..a3b0e57ca --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Queue; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueActionModule : SonarrRestModule + { + private readonly IQueueService _queueService; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly IDownloadService _downloadService; + + public QueueActionModule(IQueueService queueService, + ITrackedDownloadService trackedDownloadService, + IFailedDownloadService failedDownloadService, + IProvideDownloadClient downloadClientProvider, + IPendingReleaseService pendingReleaseService, + IDownloadService downloadService) + { + _queueService = queueService; + _trackedDownloadService = trackedDownloadService; + _failedDownloadService = failedDownloadService; + _downloadClientProvider = downloadClientProvider; + _pendingReleaseService = pendingReleaseService; + _downloadService = downloadService; + + Post[@"/grab/(?[\d]{1,10})"] = x => Grab((int)x.Id); + Post["/grab/bulk"] = x => Grab(); + + Delete[@"/(?[\d]{1,10})"] = x => Remove((int)x.Id); + Delete["/bulk"] = x => Remove(); + } + + private Response Grab(int id) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteEpisode); + + return new object().AsResponse(); + } + + private Response Grab() + { + var resource = Request.Body.FromJson(); + + foreach (var id in resource.Ids) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteEpisode); + } + + return new object().AsResponse(); + } + + private Response Remove(int id) + { + var blacklist = Request.GetBooleanQueryParameter("blacklist"); + + var trackedDownload = Remove(id, blacklist); + + if (trackedDownload != null) + { + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); + } + + return new object().AsResponse(); + } + + private Response Remove() + { + var blacklist = Request.GetBooleanQueryParameter("blacklist"); + + var resource = Request.Body.FromJson(); + var trackedDownloadIds = new List(); + + foreach (var id in resource.Ids) + { + var trackedDownload = Remove(id, blacklist); + + if (trackedDownload != null) + { + trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); + } + } + + _trackedDownloadService.StopTracking(trackedDownloadIds); + + return new object().AsResponse(); + } + + private TrackedDownload Remove(int id, bool blacklist) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease != null) + { + _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); + + return null; + } + + var trackedDownload = GetTrackedDownload(id); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + + if (blacklist) + { + _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); + } + + return trackedDownload; + } + + private TrackedDownload GetTrackedDownload(int queueId) + { + var queueItem = _queueService.Find(queueId); + + if (queueItem == null) + { + throw new NotFoundException(); + } + + var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + return trackedDownload; + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueBulkResource.cs b/src/Sonarr.Api.V3/Queue/QueueBulkResource.cs new file mode 100644 index 000000000..aaf21e5e1 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueBulkResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueBulkResource + { + public List Ids { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs b/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs new file mode 100644 index 000000000..b88656517 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueDetailsModule : SonarrRestModuleWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage, "queue/details") + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + GetResourceAll = GetQueue; + } + + private List GetQueue() + { + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode", true); + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending); + + var seriesIdQuery = Request.Query.SeriesId; + var episodeIdsQuery = Request.Query.EpisodeIds; + + if (seriesIdQuery.HasValue) + { + return fullQueue.Where(q => q.Series.Id == (int)seriesIdQuery).ToResource(includeSeries, includeEpisode); + } + + if (episodeIdsQuery.HasValue) + { + string episodeIdsValue = episodeIdsQuery.Value.ToString(); + + var episodeIds = episodeIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return fullQueue.Where(q => episodeIds.Contains(q.Episode.Id)).ToResource(includeSeries, includeEpisode); + } + + return fullQueue.ToResource(includeSeries, includeEpisode); + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Queue/QueueModule.cs b/src/Sonarr.Api.V3/Queue/QueueModule.cs new file mode 100644 index 000000000..2046a079f --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueModule.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueModule : SonarrRestModuleWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + GetResourcePaged = GetQueue; + } + + private PagingResource GetQueue(PagingResource pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); + + return ApplyToPage(GetQueue, pagingSpec, (q) => MapToResource(q, includeSeries, includeEpisode)); + } + + private PagingSpec GetQueue(PagingSpec pagingSpec) + { + var ascending = pagingSpec.SortDirection == SortDirection.Ascending; + var orderByFunc = GetOrderByFunc(pagingSpec); + + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending).ToList(); + IOrderedEnumerable ordered; + + if (pagingSpec.SortKey == "episode") + { + ordered = ascending ? fullQueue.OrderBy(q => q.Episode.SeasonNumber).ThenBy(q => q.Episode.EpisodeNumber) : + fullQueue.OrderByDescending(q => q.Episode.SeasonNumber).ThenByDescending(q => q.Episode.EpisodeNumber); + } + + else if (pagingSpec.SortKey == "timeleft") + { + ordered = ascending ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) : + fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer()); + } + + else if (pagingSpec.SortKey == "estimatedCompletionTime") + { + ordered = ascending ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) : + fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()); + } + + else if (pagingSpec.SortKey == "protocol") + { + ordered = ascending ? fullQueue.OrderBy(q => q.Protocol) : + fullQueue.OrderByDescending(q => q.Protocol); + } + + else if (pagingSpec.SortKey == "indexer") + { + ordered = ascending ? fullQueue.OrderBy(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase) : + fullQueue.OrderByDescending(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase); + } + + else if (pagingSpec.SortKey == "downloadClient") + { + ordered = ascending ? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase) : + fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase); + } + + else + { + ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); + } + + ordered = ordered.ThenByDescending(q => q.Size == 0 ? 0 : 100 - q.Sizeleft / q.Size * 100); + + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + pagingSpec.TotalRecords = fullQueue.Count; + + if (pagingSpec.Records.Empty() && pagingSpec.Page > 1) + { + pagingSpec.Page = (int)Math.Max(Math.Ceiling((decimal)(pagingSpec.TotalRecords / pagingSpec.PageSize)), 1); + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + } + + return pagingSpec; + } + + private Func GetOrderByFunc(PagingSpec pagingSpec) + { + switch (pagingSpec.SortKey) + { + case "series.sortTitle": + return q => q.Series.SortTitle; + case "episode": + return q => q.Episode; + case "episode.airDateUtc": + return q => q.Episode.AirDateUtc; + case "episode.title": + return q => q.Episode.Title; + case "quality": + return q => q.Quality; + case "progress": + // Avoid exploding if a download's size is 0 + return q => 100 - q.Sizeleft / Math.Max(q.Size * 100, 1); + default: + return q => q.Timeleft; + } + } + + private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeSeries, bool includeEpisode) + { + return queueItem.ToResource(includeSeries, includeEpisode); + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs new file mode 100644 index 000000000..02a1bc59a --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Episodes; +using Sonarr.Api.V3.Series; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueResource : RestResource + { + public int SeriesId { get; set; } + public int EpisodeId { get; set; } + public SeriesResource Series { get; set; } + public EpisodeResource Episode { get; set; } + public QualityModel Quality { get; set; } + public decimal Size { get; set; } + public string Title { get; set; } + public decimal Sizeleft { get; set; } + public TimeSpan? Timeleft { get; set; } + public DateTime? EstimatedCompletionTime { get; set; } + public string Status { get; set; } + public string TrackedDownloadStatus { get; set; } + public List StatusMessages { get; set; } + public string ErrorMessage { get; set; } + public string DownloadId { get; set; } + public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + } + + public static class QueueResourceMapper + { + public static QueueResource ToResource(this NzbDrone.Core.Queue.Queue model, bool includeSeries, bool includeEpisode) + { + if (model == null) return null; + + return new QueueResource + { + Id = model.Id, + SeriesId = model.Series.Id, + EpisodeId = model.Episode.Id, + Series = includeSeries ? model.Series.ToResource() : null, + Episode = includeEpisode ? model.Episode.ToResource() : null, + Quality = model.Quality, + Size = model.Size, + Title = model.Title, + Sizeleft = model.Sizeleft, + Timeleft = model.Timeleft, + EstimatedCompletionTime = model.EstimatedCompletionTime, + Status = model.Status, + TrackedDownloadStatus = model.TrackedDownloadStatus, + StatusMessages = model.StatusMessages, + ErrorMessage = model.ErrorMessage, + DownloadId = model.DownloadId, + Protocol = model.Protocol, + DownloadClient = model.DownloadClient, + Indexer = model.Indexer + }; + } + + public static List ToResource(this IEnumerable models, bool includeSeries, bool includeEpisode) + { + return models.Select((m) => ToResource(m, includeSeries, includeEpisode)).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs b/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs new file mode 100644 index 000000000..6b22ed5fb --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using Nancy.Responses; +using NzbDrone.Common.TPL; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueStatusModule : SonarrRestModuleWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly Debouncer _broadcastDebounce; + + + public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage, "queue/status") + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + + _broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5)); + + + Get["/"] = x => GetQueueStatusResponse(); + } + + private JsonResponse GetQueueStatusResponse() + { + return GetQueueStatus().AsResponse(); + } + + private QueueStatusResource GetQueueStatus() + { + _broadcastDebounce.Pause(); + + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + + var resource = new QueueStatusResource + { + Count = queue.Count + pending.Count, + Errors = queue.Any(q => q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), + Warnings = queue.Any(q => q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)) + }; + + _broadcastDebounce.Resume(); + + return resource; + } + + private void BroadcastChange() + { + BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); + } + + public void Handle(QueueUpdatedEvent message) + { + _broadcastDebounce.Execute(); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + _broadcastDebounce.Execute(); + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueStatusResource.cs b/src/Sonarr.Api.V3/Queue/QueueStatusResource.cs new file mode 100644 index 000000000..022c2f295 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueStatusResource.cs @@ -0,0 +1,11 @@ +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Queue +{ + public class QueueStatusResource : RestResource + { + public int Count { get; set; } + public bool Errors { get; set; } + public bool Warnings { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs new file mode 100644 index 000000000..1e6bb7f6e --- /dev/null +++ b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; + +namespace Sonarr.Api.V3.RemotePathMappings +{ + public class RemotePathMappingModule : SonarrRestModule + { + private readonly IRemotePathMappingService _remotePathMappingService; + + public RemotePathMappingModule(IRemotePathMappingService remotePathMappingService, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator) + { + _remotePathMappingService = remotePathMappingService; + + GetResourceAll = GetMappings; + GetResourceById = GetMappingById; + CreateResource = CreateMapping; + DeleteResource = DeleteMapping; + UpdateResource = UpdateMapping; + + SharedValidator.RuleFor(c => c.Host) + .NotEmpty(); + + // We cannot use IsValidPath here, because it's a remote path, possibly other OS. + SharedValidator.RuleFor(c => c.RemotePath) + .NotEmpty(); + + SharedValidator.RuleFor(c => c.LocalPath) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(pathExistsValidator); + } + + private RemotePathMappingResource GetMappingById(int id) + { + return _remotePathMappingService.Get(id).ToResource(); + } + + private int CreateMapping(RemotePathMappingResource resource) + { + var model = resource.ToModel(); + + return _remotePathMappingService.Add(model).Id; + } + + private List GetMappings() + { + return _remotePathMappingService.All().ToResource(); + } + + private void DeleteMapping(int id) + { + _remotePathMappingService.Remove(id); + } + + private void UpdateMapping(RemotePathMappingResource resource) + { + var mapping = resource.ToModel(); + + _remotePathMappingService.Update(mapping); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs new file mode 100644 index 000000000..c371329d6 --- /dev/null +++ b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.RemotePathMappings; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.RemotePathMappings +{ + public class RemotePathMappingResource : RestResource + { + public string Host { get; set; } + public string RemotePath { get; set; } + public string LocalPath { get; set; } + } + + public static class RemotePathMappingResourceMapper + { + public static RemotePathMappingResource ToResource(this RemotePathMapping model) + { + if (model == null) return null; + + return new RemotePathMappingResource + { + Id = model.Id, + + Host = model.Host, + RemotePath = model.RemotePath, + LocalPath = model.LocalPath + }; + } + + public static RemotePathMapping ToModel(this RemotePathMappingResource resource) + { + if (resource == null) return null; + + return new RemotePathMapping + { + Id = resource.Id, + + Host = resource.Host, + RemotePath = resource.RemotePath, + LocalPath = resource.LocalPath + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs b/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs new file mode 100644 index 000000000..11bdb55d6 --- /dev/null +++ b/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Restrictions; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Restrictions +{ + public class RestrictionModule : SonarrRestModule + { + private readonly IRestrictionService _restrictionService; + + + public RestrictionModule(IRestrictionService restrictionService) + { + _restrictionService = restrictionService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + + SharedValidator.Custom(restriction => + { + if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace()) + { + return new ValidationFailure("", "Either 'Must contain' or 'Must not contain' is required"); + } + + return null; + }); + } + + private RestrictionResource Get(int id) + { + return _restrictionService.Get(id).ToResource(); + } + + private List GetAll() + { + return _restrictionService.All().ToResource(); + } + + private int Create(RestrictionResource resource) + { + return _restrictionService.Add(resource.ToModel()).Id; + } + + private void Update(RestrictionResource resource) + { + _restrictionService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _restrictionService.Delete(id); + } + } +} diff --git a/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs b/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs new file mode 100644 index 000000000..d30743fe6 --- /dev/null +++ b/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Restrictions; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Restrictions +{ + public class RestrictionResource : RestResource + { + public string Required { get; set; } + public string Preferred { get; set; } + public string Ignored { get; set; } + public HashSet Tags { get; set; } + + public RestrictionResource() + { + Tags = new HashSet(); + } + } + + public static class RestrictionResourceMapper + { + public static RestrictionResource ToResource(this Restriction model) + { + if (model == null) return null; + + return new RestrictionResource + { + Id = model.Id, + + Required = model.Required, + Preferred = model.Preferred, + Ignored = model.Ignored, + Tags = new HashSet(model.Tags) + }; + } + + public static Restriction ToModel(this RestrictionResource resource) + { + if (resource == null) return null; + + return new Restriction + { + Id = resource.Id, + + Required = resource.Required, + Preferred = resource.Preferred, + Ignored = resource.Ignored, + Tags = new HashSet(resource.Tags) + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs b/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs new file mode 100644 index 000000000..e6e22525c --- /dev/null +++ b/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Sonarr.Http; + +namespace Sonarr.Api.V3.RootFolders +{ + public class RootFolderModule : SonarrRestModuleWithSignalR + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderModule(IRootFolderService rootFolderService, + IBroadcastSignalRMessage signalRBroadcaster, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator, + StartupFolderValidator startupFolderValidator, + SystemFolderValidator systemFolderValidator, + FolderWritableValidator folderWritableValidator + ) + : base(signalRBroadcaster) + { + _rootFolderService = rootFolderService; + + GetResourceAll = GetRootFolders; + GetResourceById = GetRootFolder; + CreateResource = CreateRootFolder; + DeleteResource = DeleteFolder; + + SharedValidator.RuleFor(c => c.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(startupFolderValidator) + .SetValidator(pathExistsValidator) + .SetValidator(systemFolderValidator) + .SetValidator(folderWritableValidator); + } + + private RootFolderResource GetRootFolder(int id) + { + return _rootFolderService.Get(id).ToResource(); + } + + private int CreateRootFolder(RootFolderResource rootFolderResource) + { + var model = rootFolderResource.ToModel(); + + return _rootFolderService.Add(model).Id; + } + + private List GetRootFolders() + { + return _rootFolderService.AllWithUnmappedFolders().ToResource(); + } + + private void DeleteFolder(int id) + { + _rootFolderService.Remove(id); + } + } +} diff --git a/src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs b/src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs new file mode 100644 index 000000000..dba574686 --- /dev/null +++ b/src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.RootFolders; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.RootFolders +{ + public class RootFolderResource : RestResource + { + public string Path { get; set; } + public long? FreeSpace { get; set; } + + public List UnmappedFolders { get; set; } + } + + public static class RootFolderResourceMapper + { + public static RootFolderResource ToResource(this RootFolder model) + { + if (model == null) return null; + + return new RootFolderResource + { + Id = model.Id, + + Path = model.Path, + FreeSpace = model.FreeSpace, + UnmappedFolders = model.UnmappedFolders + }; + } + + public static RootFolder ToModel(this RootFolderResource resource) + { + if (resource == null) return null; + + return new RootFolder + { + Id = resource.Id, + + Path = resource.Path, + //FreeSpace + //UnmappedFolders + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/SeasonPass/SeasonPassModule.cs b/src/Sonarr.Api.V3/SeasonPass/SeasonPassModule.cs new file mode 100644 index 000000000..4ace6e0c3 --- /dev/null +++ b/src/Sonarr.Api.V3/SeasonPass/SeasonPassModule.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Nancy; +using NzbDrone.Core.Tv; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.SeasonPass +{ + public class SeasonPassModule : SonarrV3Module + { + private readonly ISeriesService _seriesService; + private readonly IEpisodeMonitoredService _episodeMonitoredService; + + public SeasonPassModule(ISeriesService seriesService, IEpisodeMonitoredService episodeMonitoredService) + : base("/seasonpass") + { + _seriesService = seriesService; + _episodeMonitoredService = episodeMonitoredService; + Post["/"] = series => UpdateAll(); + } + + private Response UpdateAll() + { + //Read from request + var request = Request.Body.FromJson(); + var seriesToUpdate = _seriesService.GetSeries(request.Series.Select(s => s.Id)); + + foreach (var s in request.Series) + { + var series = seriesToUpdate.Single(c => c.Id == s.Id); + + if (s.Monitored.HasValue) + { + series.Monitored = s.Monitored.Value; + } + + if (s.Seasons != null && s.Seasons.Any()) + { + foreach (var seriesSeason in series.Seasons) + { + var season = s.Seasons.FirstOrDefault(c => c.SeasonNumber == seriesSeason.SeasonNumber); + + if (season != null) + { + seriesSeason.Monitored = season.Monitored; + } + } + } + + _episodeMonitoredService.SetEpisodeMonitoredStatus(series, request.MonitoringOptions); + } + + return "ok".AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/Sonarr.Api.V3/SeasonPass/SeasonPassResource.cs b/src/Sonarr.Api.V3/SeasonPass/SeasonPassResource.cs new file mode 100644 index 000000000..2f9455f99 --- /dev/null +++ b/src/Sonarr.Api.V3/SeasonPass/SeasonPassResource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Tv; + +namespace Sonarr.Api.V3.SeasonPass +{ + public class SeasonPassResource + { + public List Series { get; set; } + public MonitoringOptions MonitoringOptions { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/SeasonPass/SeasonPassSeriesResource.cs b/src/Sonarr.Api.V3/SeasonPass/SeasonPassSeriesResource.cs new file mode 100644 index 000000000..624504d7d --- /dev/null +++ b/src/Sonarr.Api.V3/SeasonPass/SeasonPassSeriesResource.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Sonarr.Api.V3.Series; + +namespace Sonarr.Api.V3.SeasonPass +{ + public class SeasonPassSeriesResource + { + public int Id { get; set; } + public bool? Monitored { get; set; } + public List Seasons { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs b/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs new file mode 100644 index 000000000..67bd5819d --- /dev/null +++ b/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs @@ -0,0 +1,9 @@ +namespace Sonarr.Api.V3.Series +{ + public class AlternateTitleResource + { + public string Title { get; set; } + public int? SeasonNumber { get; set; } + public int? SceneSeasonNumber { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeasonResource.cs b/src/Sonarr.Api.V3/Series/SeasonResource.cs new file mode 100644 index 000000000..6030a5618 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeasonResource.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; + +namespace Sonarr.Api.V3.Series +{ + public class SeasonResource + { + public int SeasonNumber { get; set; } + public bool Monitored { get; set; } + public SeasonStatisticsResource Statistics { get; set; } + public List Images { get; set; } + } + + public static class SeasonResourceMapper + { + public static SeasonResource ToResource(this Season model, bool includeImages = false) + { + if (model == null) return null; + + return new SeasonResource + { + SeasonNumber = model.SeasonNumber, + Monitored = model.Monitored, + Images = includeImages ? model.Images : null + }; + } + + public static Season ToModel(this SeasonResource resource) + { + if (resource == null) return null; + + return new Season + { + SeasonNumber = resource.SeasonNumber, + Monitored = resource.Monitored + }; + } + + public static List ToResource(this IEnumerable models, bool includeImages = false) + { + return models.Select(s => ToResource(s, includeImages)).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeasonStatisticsResource.cs b/src/Sonarr.Api.V3/Series/SeasonStatisticsResource.cs new file mode 100644 index 000000000..12ee9434f --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeasonStatisticsResource.cs @@ -0,0 +1,43 @@ +using System; +using NzbDrone.Core.SeriesStats; + +namespace Sonarr.Api.V3.Series +{ + public class SeasonStatisticsResource + { + public DateTime? NextAiring { get; set; } + public DateTime? PreviousAiring { get; set; } + public int EpisodeFileCount { get; set; } + public int EpisodeCount { get; set; } + public int TotalEpisodeCount { get; set; } + public long SizeOnDisk { get; set; } + + public decimal PercentOfEpisodes + { + get + { + if (EpisodeCount == 0) return 0; + + return (decimal)EpisodeFileCount / (decimal)EpisodeCount * 100; + } + } + } + + public static class SeasonStatisticsResourceMapper + { + public static SeasonStatisticsResource ToResource(this SeasonStatistics model) + { + if (model == null) return null; + + return new SeasonStatisticsResource + { + NextAiring = model.NextAiring, + PreviousAiring = model.PreviousAiring, + EpisodeFileCount = model.EpisodeFileCount, + EpisodeCount = model.EpisodeCount, + TotalEpisodeCount = model.TotalEpisodeCount, + SizeOnDisk = model.SizeOnDisk + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorDeleteResource.cs b/src/Sonarr.Api.V3/Series/SeriesEditorDeleteResource.cs new file mode 100644 index 000000000..12a539692 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesEditorDeleteResource.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesEditorDeleteResource + { + public List SeriesIds { get; set; } + public bool DeleteFiles { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorModule.cs b/src/Sonarr.Api.V3/Series/SeriesEditorModule.cs new file mode 100644 index 000000000..f8ac84e04 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesEditorModule.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Commands; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesEditorModule : SonarrV3Module + { + private readonly ISeriesService _seriesService; + private readonly IManageCommandQueue _commandQueueManager; + + public SeriesEditorModule(ISeriesService seriesService, IManageCommandQueue commandQueueManager) + : base("/series/editor") + { + _seriesService = seriesService; + _commandQueueManager = commandQueueManager; + Put["/"] = series => SaveAll(); + Delete["/"] = series => DeleteSeries(); + } + + private Response SaveAll() + { + var resource = Request.Body.FromJson(); + var seriesToUpdate = _seriesService.GetSeries(resource.SeriesIds); + var seriesToMove = new List(); + + foreach (var series in seriesToUpdate) + { + if (resource.Monitored.HasValue) + { + series.Monitored = resource.Monitored.Value; + } + + if (resource.QualityProfileId.HasValue) + { + series.ProfileId = resource.QualityProfileId.Value; + } + + if (resource.SeriesType.HasValue) + { + series.SeriesType = resource.SeriesType.Value; + } + + if (resource.SeasonFolder.HasValue) + { + series.SeasonFolder = resource.SeasonFolder.Value; + } + + if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) + { + series.RootFolderPath = resource.RootFolderPath; + seriesToMove.Add(new BulkMoveSeries + { + SeriesId = series.Id, + SourcePath = series.Path + }); + } + + if (resource.Tags != null) + { + var newTags = resource.Tags; + var applyTags = resource.ApplyTags; + + switch (applyTags) + { + case ApplyTags.Add: + newTags.ForEach(t => series.Tags.Add(t)); + break; + case ApplyTags.Remove: + newTags.ForEach(t => series.Tags.Remove(t)); + break; + case ApplyTags.Replace: + series.Tags = new HashSet(newTags); + break; + } + } + } + + if (resource.MoveFiles && seriesToMove.Any()) + { + _commandQueueManager.Push(new BulkMoveSeriesCommand + { + DestinationRootFolder = resource.RootFolderPath, + Series = seriesToMove + }); + } + + return _seriesService.UpdateSeries(seriesToUpdate, !resource.MoveFiles) + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + + private Response DeleteSeries() + { + var resource = Request.Body.FromJson(); + + foreach (var seriesId in resource.SeriesIds) + { + _seriesService.DeleteSeries(seriesId, false); + } + + return new object().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs b/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs new file mode 100644 index 000000000..07244de21 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using NzbDrone.Core.Tv; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesEditorResource + { + public List SeriesIds { get; set; } + public bool? Monitored { get; set; } + public int? QualityProfileId { get; set; } + public SeriesTypes? SeriesType { get; set; } + public bool? SeasonFolder { get; set; } + public string RootFolderPath { get; set; } + public List Tags { get; set; } + public ApplyTags ApplyTags { get; set; } + public bool MoveFiles { get; set; } + } + + public enum ApplyTags + { + Add, + Remove, + Replace + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesImportModule.cs b/src/Sonarr.Api.V3/Series/SeriesImportModule.cs new file mode 100644 index 000000000..b36f65634 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesImportModule.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Tv; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesImportModule : SonarrRestModule + { + private readonly IAddSeriesService _addSeriesService; + + public SeriesImportModule(IAddSeriesService addSeriesService) + : base("/series/import") + { + _addSeriesService = addSeriesService; + Post["/"] = x => Import(); + } + + + private Response Import() + { + var resource = Request.Body.FromJson>(); + var newSeries = resource.ToModel(); + + return _addSeriesService.AddSeries(newSeries).ToResource().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesLookupModule.cs b/src/Sonarr.Api.V3/Series/SeriesLookupModule.cs new file mode 100644 index 000000000..df62b052b --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesLookupModule.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.SeriesStats; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesLookupModule : SonarrRestModule + { + private readonly ISearchForNewSeries _searchProxy; + + public SeriesLookupModule(ISearchForNewSeries searchProxy) + : base("/series/lookup") + { + _searchProxy = searchProxy; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var tvDbResults = _searchProxy.SearchForNewSeries((string)Request.Query.term); + return MapToResource(tvDbResults).AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable series) + { + foreach (var currentSeries in series) + { + var resource = currentSeries.ToResource(); + var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + resource.Statistics = new SeriesStatistics().ToResource(resource.Seasons); + + yield return resource; + } + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesModule.cs b/src/Sonarr.Api.V3/Series/SeriesModule.cs new file mode 100644 index 000000000..f133bb9e3 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesModule.cs @@ -0,0 +1,272 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.SeriesStats; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesModule : SonarrRestModuleWithSignalR, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle + + { + private readonly ISeriesService _seriesService; + private readonly IAddSeriesService _addSeriesService; + private readonly ISeriesStatisticsService _seriesStatisticsService; + private readonly ISceneMappingService _sceneMappingService; + private readonly IMapCoversToLocal _coverMapper; + private readonly IManageCommandQueue _commandQueueManager; + private readonly IRootFolderService _rootFolderService; + + public SeriesModule(IBroadcastSignalRMessage signalRBroadcaster, + ISeriesService seriesService, + IAddSeriesService addSeriesService, + ISeriesStatisticsService seriesStatisticsService, + ISceneMappingService sceneMappingService, + IMapCoversToLocal coverMapper, + IManageCommandQueue commandQueueManager, + IRootFolderService rootFolderService, + RootFolderValidator rootFolderValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator, + SeriesPathValidator seriesPathValidator, + SeriesExistsValidator seriesExistsValidator, + DroneFactoryValidator droneFactoryValidator, + SeriesAncestorValidator seriesAncestorValidator, + SystemFolderValidator systemFolderValidator, + ProfileExistsValidator profileExistsValidator + ) + : base(signalRBroadcaster) + { + _seriesService = seriesService; + _addSeriesService = addSeriesService; + _seriesStatisticsService = seriesStatisticsService; + _sceneMappingService = sceneMappingService; + + _coverMapper = coverMapper; + _commandQueueManager = commandQueueManager; + _rootFolderService = rootFolderService; + + GetResourceAll = AllSeries; + GetResourceById = GetSeries; + CreateResource = AddSeries; + UpdateResource = UpdateSeries; + DeleteResource = DeleteSeries; + + Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(seriesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(seriesAncestorValidator) + .SetValidator(systemFolderValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); + + SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator); + + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.Title).NotEmpty(); + PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + private List AllSeries() + { + var includeSeasonImages = Request.GetBooleanQueryParameter("includeSeasonImages"); + var seriesStats = _seriesStatisticsService.SeriesStatistics(); + var seriesResources = _seriesService.GetAllSeries().Select(s => s.ToResource(includeSeasonImages)).ToList(); + + MapCoversToLocal(seriesResources.ToArray()); + LinkSeriesStatistics(seriesResources, seriesStats); + PopulateAlternateTitles(seriesResources); + + return seriesResources; + } + + private SeriesResource GetSeries(int id) + { + var includeSeasonImages = Context != null && Request.GetBooleanQueryParameter("includeSeasonImages"); + var series = _seriesService.GetSeries(id); + + return GetSeriesResource(series, includeSeasonImages); + } + + private int AddSeries(SeriesResource seriesResource) + { + var series = _addSeriesService.AddSeries(seriesResource.ToModel()); + + return series.Id; + } + + private void UpdateSeries(SeriesResource seriesResource) + { + var moveFiles = Request.GetBooleanQueryParameter("moveFiles"); + var series = _seriesService.GetSeries(seriesResource.Id); + + if (moveFiles) + { + var sourcePath = series.Path; + var destinationPath = seriesResource.Path; + + _commandQueueManager.Push(new MoveSeriesCommand + { + SeriesId = series.Id, + SourcePath = sourcePath, + DestinationPath = destinationPath, + Trigger = CommandTrigger.Manual + }); + } + + var model = seriesResource.ToModel(series); + + _seriesService.UpdateSeries(model); + + BroadcastResourceChange(ModelAction.Updated, seriesResource); + } + + private void DeleteSeries(int id) + { + var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); + + _seriesService.DeleteSeries(id, deleteFiles); + } + + private SeriesResource GetSeriesResource(NzbDrone.Core.Tv.Series series, bool includeSeasonImages) + { + if (series == null) return null; + + var resource = series.ToResource(includeSeasonImages); + MapCoversToLocal(resource); + FetchAndLinkSeriesStatistics(resource); + PopulateAlternateTitles(resource); + LinkRootFolderPath(resource); + + return resource; + } + + private void MapCoversToLocal(params SeriesResource[] series) + { + foreach (var seriesResource in series) + { + _coverMapper.ConvertToLocalUrls(seriesResource.Id, seriesResource.Images); + } + } + + private void FetchAndLinkSeriesStatistics(SeriesResource resource) + { + LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id)); + } + + private void LinkSeriesStatistics(List resources, List seriesStatistics) + { + foreach (var series in resources) + { + var stats = seriesStatistics.SingleOrDefault(ss => ss.SeriesId == series.Id); + if (stats == null) continue; + + LinkSeriesStatistics(series, stats); + } + } + + private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seriesStatistics) + { + resource.PreviousAiring = seriesStatistics.PreviousAiring; + resource.NextAiring = seriesStatistics.NextAiring; + resource.Statistics = seriesStatistics.ToResource(resource.Seasons); + + if (seriesStatistics.SeasonStatistics != null) + { + foreach (var season in resource.Seasons) + { + season.Statistics = seriesStatistics.SeasonStatistics.SingleOrDefault(s => s.SeasonNumber == season.SeasonNumber).ToResource(); + } + } + } + + private void PopulateAlternateTitles(List resources) + { + foreach (var resource in resources) + { + PopulateAlternateTitles(resource); + } + } + + private void PopulateAlternateTitles(SeriesResource resource) + { + var mappings = _sceneMappingService.FindByTvdbId(resource.TvdbId); + + if (mappings == null) return; + + resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + } + + private void LinkRootFolderPath(SeriesResource resource) + { + resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); + } + + public void Handle(EpisodeImportedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.SeriesId); + } + + public void Handle(EpisodeFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) return; + + BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.SeriesId); + } + + public void Handle(SeriesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + } + + public void Handle(SeriesEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + } + + public void Handle(SeriesDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Series.ToResource()); + } + + public void Handle(SeriesRenamedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + } + + public void Handle(MediaCoversUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesResource.cs b/src/Sonarr.Api.V3/Series/SeriesResource.cs new file mode 100644 index 000000000..725bf0fe4 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesResource.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesResource : RestResource + { + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + //View Only + public string Title { get; set; } + public List AlternateTitles { get; set; } + public string SortTitle { get; set; } + + // V3: replace with Ended + public SeriesStatusType Status { get; set; } + + public bool Ended => Status == SeriesStatusType.Ended; + + public string ProfileName { get; set; } + public string Overview { get; set; } + public DateTime? NextAiring { get; set; } + public DateTime? PreviousAiring { get; set; } + public string Network { get; set; } + public string AirTime { get; set; } + public List Images { get; set; } + + public string RemotePoster { get; set; } + public List Seasons { get; set; } + public int Year { get; set; } + + //View & Edit + public string Path { get; set; } + public int QualityProfileId { get; set; } + public int LanguageProfileId { get; set; } + + //Editing Only + public bool SeasonFolder { get; set; } + public bool Monitored { get; set; } + + public bool UseSceneNumbering { get; set; } + public int Runtime { get; set; } + public int TvdbId { get; set; } + public int TvRageId { get; set; } + public int TvMazeId { get; set; } + public DateTime? FirstAired { get; set; } + public DateTime? LastInfoSync { get; set; } + public SeriesTypes SeriesType { get; set; } + public string CleanTitle { get; set; } + public string ImdbId { get; set; } + public string TitleSlug { get; set; } + public string RootFolderPath { get; set; } + public string Certification { get; set; } + public List Genres { get; set; } + public HashSet Tags { get; set; } + public DateTime Added { get; set; } + public AddSeriesOptions AddOptions { get; set; } + public Ratings Ratings { get; set; } + + public SeriesStatisticsResource Statistics { get; set; } + } + + public static class SeriesResourceMapper + { + public static SeriesResource ToResource(this NzbDrone.Core.Tv.Series model, bool includeSeasonImages = false) + { + if (model == null) return null; + + return new SeriesResource + { + Id = model.Id, + + Title = model.Title, + //AlternateTitles + SortTitle = model.SortTitle, + + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Status = model.Status, + Overview = model.Overview, + //NextAiring + //PreviousAiring + Network = model.Network, + AirTime = model.AirTime, + Images = model.Images, + + Seasons = model.Seasons.ToResource(includeSeasonImages), + Year = model.Year, + + Path = model.Path, + QualityProfileId = model.ProfileId, + LanguageProfileId = model.LanguageProfileId, + + SeasonFolder = model.SeasonFolder, + Monitored = model.Monitored, + + UseSceneNumbering = model.UseSceneNumbering, + Runtime = model.Runtime, + TvdbId = model.TvdbId, + TvRageId = model.TvRageId, + TvMazeId = model.TvMazeId, + FirstAired = model.FirstAired, + LastInfoSync = model.LastInfoSync, + SeriesType = model.SeriesType, + CleanTitle = model.CleanTitle, + ImdbId = model.ImdbId, + TitleSlug = model.TitleSlug, + + // Root folder path needs to be calculated from the series path + // RootFolderPath = model.RootFolderPath, + + Certification = model.Certification, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + AddOptions = model.AddOptions, + Ratings = model.Ratings + }; + } + + public static NzbDrone.Core.Tv.Series ToModel(this SeriesResource resource) + { + if (resource == null) return null; + + return new NzbDrone.Core.Tv.Series + { + Id = resource.Id, + + Title = resource.Title, + //AlternateTitles + SortTitle = resource.SortTitle, + + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Status = resource.Status, + Overview = resource.Overview, + //NextAiring + //PreviousAiring + Network = resource.Network, + AirTime = resource.AirTime, + Images = resource.Images, + + Seasons = resource.Seasons.ToModel(), + Year = resource.Year, + + Path = resource.Path, + ProfileId = resource.QualityProfileId, + LanguageProfileId = resource.LanguageProfileId, + + SeasonFolder = resource.SeasonFolder, + Monitored = resource.Monitored, + + UseSceneNumbering = resource.UseSceneNumbering, + Runtime = resource.Runtime, + TvdbId = resource.TvdbId, + TvRageId = resource.TvRageId, + TvMazeId = resource.TvMazeId, + FirstAired = resource.FirstAired, + LastInfoSync = resource.LastInfoSync, + SeriesType = resource.SeriesType, + CleanTitle = resource.CleanTitle, + ImdbId = resource.ImdbId, + TitleSlug = resource.TitleSlug, + RootFolderPath = resource.RootFolderPath, + Certification = resource.Certification, + Genres = resource.Genres, + Tags = resource.Tags, + Added = resource.Added, + AddOptions = resource.AddOptions, + Ratings = resource.Ratings + }; + } + + public static NzbDrone.Core.Tv.Series ToModel(this SeriesResource resource, NzbDrone.Core.Tv.Series series) + { + var updatedSeries = resource.ToModel(); + + series.ApplyChanges(updatedSeries); + + return series; + } + + public static List ToResource(this IEnumerable series, bool includeSeasonImages = false) + { + return series.Select(s => ToResource(s, includeSeasonImages)).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesStatisticsResource.cs b/src/Sonarr.Api.V3/Series/SeriesStatisticsResource.cs new file mode 100644 index 000000000..a2c797b95 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesStatisticsResource.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.SeriesStats; + +namespace Sonarr.Api.V3.Series +{ + public class SeriesStatisticsResource + { + public int SeasonCount { get; set; } + public int EpisodeFileCount { get; set; } + public int EpisodeCount { get; set; } + public int TotalEpisodeCount { get; set; } + public long SizeOnDisk { get; set; } + + public decimal PercentOfEpisodes + { + get + { + if (EpisodeCount == 0) return 0; + + return (decimal)EpisodeFileCount / (decimal)EpisodeCount * 100; + } + } + + + } + + public static class SeriesStatisticsResourceMapper + { + public static SeriesStatisticsResource ToResource(this SeriesStatistics model, List seasons) + { + if (model == null) return null; + + return new SeriesStatisticsResource + { + SeasonCount = seasons == null ? 0 : seasons.Where(s => s.SeasonNumber > 0).Count(), + EpisodeFileCount = model.EpisodeFileCount, + EpisodeCount = model.EpisodeCount, + TotalEpisodeCount = model.TotalEpisodeCount, + SizeOnDisk = model.SizeOnDisk + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj new file mode 100644 index 000000000..b8d75dc35 --- /dev/null +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -0,0 +1,253 @@ + + + + + Debug + x86 + {7140FF1F-79BE-492F-9188-B21A050BF708} + Library + Properties + Sonarr.Api.V3 + Sonarr.Api.V3 + v4.0 + 512 + ..\ + true + + + 12.0.0 + 2.0 + + + true + ..\..\_output\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + 4 + false + + + ..\..\_output\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + 4 + + + + ..\packages\Ical.Net.2.2.32\lib\net40\antlr.runtime.dll + + + ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll + True + + + ..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.dll + + + ..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.Collections.dll + + + ..\packages\Nancy.1.4.3\lib\net40\Nancy.dll + True + + + ..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll + True + + + ..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll + True + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll + True + + + ..\packages\NLog.4.4.3\lib\net40\NLog.dll + + + ..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll + + + + + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} + Marr.Data + + + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} + NzbDrone.Common + + + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205} + NzbDrone.Core + + + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} + NzbDrone.SignalR + + + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Sonarr.Http + + + + + + + + \ No newline at end of file diff --git a/src/Sonarr.Api.V3/SonarrV3FeedModule.cs b/src/Sonarr.Api.V3/SonarrV3FeedModule.cs new file mode 100644 index 000000000..794718c9e --- /dev/null +++ b/src/Sonarr.Api.V3/SonarrV3FeedModule.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace Sonarr.Api.V3 +{ + public abstract class SonarrV3FeedModule : NancyModule + { + protected SonarrV3FeedModule(string resource) + : base("/feed/v3/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/SonarrV3Module.cs b/src/Sonarr.Api.V3/SonarrV3Module.cs new file mode 100644 index 000000000..6f07dc06c --- /dev/null +++ b/src/Sonarr.Api.V3/SonarrV3Module.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace Sonarr.Api.V3 +{ + public abstract class SonarrV3Module : NancyModule + { + protected SonarrV3Module(string resource) + : base("/api/v3/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/System/Backup/BackupModule.cs b/src/Sonarr.Api.V3/System/Backup/BackupModule.cs new file mode 100644 index 000000000..9bee19942 --- /dev/null +++ b/src/Sonarr.Api.V3/System/Backup/BackupModule.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Core.Backup; +using Sonarr.Http; + +namespace Sonarr.Api.V3.System.Backup +{ + public class BackupModule : SonarrRestModule + { + private readonly IBackupService _backupService; + + public BackupModule(IBackupService backupService) : base("system/backup") + { + _backupService = backupService; + GetResourceAll = GetBackupFiles; + } + + public List GetBackupFiles() + { + var backups = _backupService.GetBackups(); + + return backups.Select(b => new BackupResource + { + Id = b.Name.GetHashCode(), + Name = b.Name, + Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", + Type = b.Type, + Time = b.Time + }) + .OrderByDescending(b => b.Time) + .ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/System/Backup/BackupResource.cs b/src/Sonarr.Api.V3/System/Backup/BackupResource.cs new file mode 100644 index 000000000..86b23c4da --- /dev/null +++ b/src/Sonarr.Api.V3/System/Backup/BackupResource.cs @@ -0,0 +1,14 @@ +using System; +using NzbDrone.Core.Backup; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.System.Backup +{ + public class BackupResource : RestResource + { + public string Name { get; set; } + public string Path { get; set; } + public BackupType Type { get; set; } + public DateTime Time { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/System/SystemModule.cs b/src/Sonarr.Api.V3/System/SystemModule.cs new file mode 100644 index 000000000..4ff87cce5 --- /dev/null +++ b/src/Sonarr.Api.V3/System/SystemModule.cs @@ -0,0 +1,94 @@ +using Nancy; +using Nancy.Routing; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Lifecycle; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.System +{ + public class SystemModule : SonarrV3Module + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IPlatformInfo _platformInfo; + private readonly IOsInfo _osInfo; + private readonly IRouteCacheProvider _routeCacheProvider; + private readonly IConfigFileProvider _configFileProvider; + private readonly IMainDatabase _database; + private readonly ILifecycleService _lifecycleService; + + public SystemModule(IAppFolderInfo appFolderInfo, + IRuntimeInfo runtimeInfo, + IPlatformInfo platformInfo, + IOsInfo osInfo, + IRouteCacheProvider routeCacheProvider, + IConfigFileProvider configFileProvider, + IMainDatabase database, + ILifecycleService lifecycleService) + : base("system") + { + _appFolderInfo = appFolderInfo; + _runtimeInfo = runtimeInfo; + _platformInfo = platformInfo; + _osInfo = osInfo; + _routeCacheProvider = routeCacheProvider; + _configFileProvider = configFileProvider; + _database = database; + _lifecycleService = lifecycleService; + Get["/status"] = x => GetStatus(); + Get["/routes"] = x => GetRoutes(); + Post["/shutdown"] = x => Shutdown(); + Post["/restart"] = x => Restart(); + } + + private Response GetStatus() + { + return new + { + Version = BuildInfo.Version.ToString(), + BuildTime = BuildInfo.BuildDateTime, + IsDebug = BuildInfo.IsDebug, + IsProduction = RuntimeInfo.IsProduction, + IsAdmin = _runtimeInfo.IsAdmin, + IsUserInteractive = RuntimeInfo.IsUserInteractive, + StartupPath = _appFolderInfo.StartUpFolder, + AppData = _appFolderInfo.GetAppDataPath(), + OsName = _osInfo.Name, + OsVersion = _osInfo.Version, + IsMonoRuntime = PlatformInfo.IsMono, + IsMono = PlatformInfo.IsMono, + IsLinux = OsInfo.IsLinux, + IsOsx = OsInfo.IsOsx, + IsWindows = OsInfo.IsWindows, + Mode = _runtimeInfo.Mode, + Branch = _configFileProvider.Branch, + Authentication = _configFileProvider.AuthenticationMethod, + SqliteVersion = _database.Version, + UrlBase = _configFileProvider.UrlBase, + RuntimeVersion = _platformInfo.Version, + RuntimeName = PlatformInfo.Platform, + StartTime = _runtimeInfo.StartTime + }.AsResponse(); + } + + private Response GetRoutes() + { + return _routeCacheProvider.GetCache().Values.AsResponse(); + } + + private Response Shutdown() + { + _lifecycleService.Shutdown(); + return "".AsResponse(); + } + + private Response Restart() + { + _lifecycleService.Restart(); + return "".AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs b/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs new file mode 100644 index 000000000..c8840adeb --- /dev/null +++ b/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Jobs; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Sonarr.Http; + +namespace Sonarr.Api.V3.System.Tasks +{ + public class TaskModule : SonarrRestModuleWithSignalR, IHandle + { + private readonly ITaskManager _taskManager; + + private static readonly Regex NameRegex = new Regex("(? GetAll() + { + return _taskManager.GetAll() + .Select(ConvertToResource) + .OrderBy(t => t.Name) + .ToList(); + } + + private TaskResource GetTask(int id) + { + var task = _taskManager.GetAll() + .SingleOrDefault(t => t.Id == id); + + if (task == null) + { + return null; + } + + return ConvertToResource(task); + } + + private static TaskResource ConvertToResource(ScheduledTask scheduledTask) + { + var taskName = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""); + + return new TaskResource + { + Id = scheduledTask.Id, + Name = NameRegex.Replace(taskName, match => " " + match.Value), + TaskName = taskName, + Interval = scheduledTask.Interval, + LastExecution = scheduledTask.LastExecution, + NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) + }; + } + + public void Handle(CommandExecutedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/System/Tasks/TaskResource.cs b/src/Sonarr.Api.V3/System/Tasks/TaskResource.cs new file mode 100644 index 000000000..b19f6dbc9 --- /dev/null +++ b/src/Sonarr.Api.V3/System/Tasks/TaskResource.cs @@ -0,0 +1,14 @@ +using System; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.System.Tasks +{ + public class TaskResource : RestResource + { + public string Name { get; set; } + public string TaskName { get; set; } + public int Interval { get; set; } + public DateTime LastExecution { get; set; } + public DateTime NextExecution { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs b/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs new file mode 100644 index 000000000..3e5b06db0 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using NzbDrone.Core.Tags; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Tags +{ + public class TagDetailsModule : SonarrRestModule + { + private readonly ITagService _tagService; + + public TagDetailsModule(ITagService tagService) + : base("/tag/detail") + { + _tagService = tagService; + + GetResourceById = Get; + GetResourceAll = GetAll; + } + + private TagDetailsResource Get(int id) + { + return _tagService.Details(id).ToResource(); + } + + private List GetAll() + { + return _tagService.Details().ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs new file mode 100644 index 000000000..d2d39dc32 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Tags +{ + public class TagDetailsResource : RestResource + { + public string Label { get; set; } + public List DelayProfileIds { get; set; } + public List NotificationIds { get; set; } + public List RestrictionIds { get; set; } + public List SeriesIds { get; set; } + } + + public static class TagDetailsResourceMapper + { + public static TagDetailsResource ToResource(this TagDetails model) + { + if (model == null) return null; + + return new TagDetailsResource + { + Id = model.Id, + Label = model.Label, + DelayProfileIds = model.DelayProfileIds, + NotificationIds = model.NotificationIds, + RestrictionIds = model.RestrictionIds, + SeriesIds = model.SeriesIds + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagModule.cs b/src/Sonarr.Api.V3/Tags/TagModule.cs new file mode 100644 index 000000000..5aba55881 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagModule.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tags; +using NzbDrone.SignalR; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Tags +{ + public class TagModule : SonarrRestModuleWithSignalR, IHandle + { + private readonly ITagService _tagService; + + public TagModule(IBroadcastSignalRMessage signalRBroadcaster, + ITagService tagService) + : base(signalRBroadcaster) + { + _tagService = tagService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + } + + private TagResource Get(int id) + { + return _tagService.GetTag(id).ToResource(); + } + + private List GetAll() + { + return _tagService.All().ToResource(); + } + + private int Create(TagResource resource) + { + return _tagService.Add(resource.ToModel()).Id; + } + + private void Update(TagResource resource) + { + _tagService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _tagService.Delete(id); + } + + public void Handle(TagsUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagResource.cs b/src/Sonarr.Api.V3/Tags/TagResource.cs new file mode 100644 index 000000000..3c8a00244 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagResource.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Tags +{ + public class TagResource : RestResource + { + public string Label { get; set; } + } + + public static class TagResourceMapper + { + public static TagResource ToResource(this Tag model) + { + if (model == null) return null; + + return new TagResource + { + Id = model.Id, + Label = model.Label + }; + } + + public static Tag ToModel(this TagResource resource) + { + if (resource == null) return null; + + return new Tag + { + Id = resource.Id, + Label = resource.Label + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Update/UpdateModule.cs b/src/Sonarr.Api.V3/Update/UpdateModule.cs new file mode 100644 index 000000000..76c357be1 --- /dev/null +++ b/src/Sonarr.Api.V3/Update/UpdateModule.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Update; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Update +{ + public class UpdateModule : SonarrRestModule + { + private readonly IRecentUpdateProvider _recentUpdateProvider; + + public UpdateModule(IRecentUpdateProvider recentUpdateProvider) + { + _recentUpdateProvider = recentUpdateProvider; + GetResourceAll = GetRecentUpdates; + } + + private List GetRecentUpdates() + { + var resources = _recentUpdateProvider.GetRecentUpdatePackages() + .OrderByDescending(u => u.Version) + .ToResource(); + + if (resources.Any()) + { + var first = resources.First(); + first.Latest = true; + + if (first.Version > BuildInfo.Version) + { + first.Installable = true; + } + + var installed = resources.SingleOrDefault(r => r.Version == BuildInfo.Version); + + if (installed != null) + { + installed.Installed = true; + } + } + + return resources; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Update/UpdateResource.cs b/src/Sonarr.Api.V3/Update/UpdateResource.cs new file mode 100644 index 000000000..e01ad966d --- /dev/null +++ b/src/Sonarr.Api.V3/Update/UpdateResource.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Update; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Update +{ + public class UpdateResource : RestResource + { + [JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))] + public Version Version { get; set; } + + public string Branch { get; set; } + public DateTime ReleaseDate { get; set; } + public string FileName { get; set; } + public string Url { get; set; } + public bool Installed { get; set; } + public bool Installable { get; set; } + public bool Latest { get; set; } + public UpdateChanges Changes { get; set; } + public string Hash { get; set; } + } + + public static class UpdateResourceMapper + { + public static UpdateResource ToResource(this UpdatePackage model) + { + if (model == null) return null; + + return new UpdateResource + { + Version = model.Version, + + Branch = model.Branch, + ReleaseDate = model.ReleaseDate, + FileName = model.FileName, + Url = model.Url, + //Installed + //Installable + //Latest + Changes = model.Changes, + Hash = model.Hash, + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Wanted/CutoffModule.cs b/src/Sonarr.Api.V3/Wanted/CutoffModule.cs new file mode 100644 index 000000000..2210fbe5c --- /dev/null +++ b/src/Sonarr.Api.V3/Wanted/CutoffModule.cs @@ -0,0 +1,56 @@ +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Api.V3.Episodes; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Wanted +{ + public class CutoffModule : EpisodeModuleWithSignalR + { + private readonly IEpisodeCutoffService _episodeCutoffService; + + public CutoffModule(IEpisodeCutoffService episodeCutoffService, + IEpisodeService episodeService, + ISeriesService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/cutoff") + { + _episodeCutoffService = episodeCutoffService; + GetResourcePaged = GetCutoffUnmetEpisodes; + } + + private PagingResource GetCutoffUnmetEpisodes(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisodeFile = Request.GetBooleanQueryParameter("includeEpisodeFile"); + var includeImages = Request.GetBooleanQueryParameter("includeImages"); + var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); + + if (filter != null && filter.Value == "false") + { + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); + } + else + { + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); + } + + var resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages)); + + return resource; + } + } +} diff --git a/src/Sonarr.Api.V3/Wanted/MissingModule.cs b/src/Sonarr.Api.V3/Wanted/MissingModule.cs new file mode 100644 index 000000000..d14d247e8 --- /dev/null +++ b/src/Sonarr.Api.V3/Wanted/MissingModule.cs @@ -0,0 +1,51 @@ +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Api.V3.Episodes; +using Sonarr.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Api.V3.Wanted +{ + public class MissingModule : EpisodeModuleWithSignalR + { + public MissingModule(IEpisodeService episodeService, + ISeriesService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing") + { + GetResourcePaged = GetMissingEpisodes; + } + + private PagingResource GetMissingEpisodes(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeImages = Request.GetBooleanQueryParameter("includeImages"); + var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); + + if (monitoredFilter != null && monitoredFilter.Value == "false") + { + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); + } + else + { + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); + } + + var resource = ApplyToPage(_episodeService.EpisodesWithoutFiles, pagingSpec, v => MapToResource(v, includeSeries, false, includeImages)); + + return resource; + } + } +} diff --git a/src/Sonarr.Api.V3/app.config b/src/Sonarr.Api.V3/app.config new file mode 100644 index 000000000..c1684a7be --- /dev/null +++ b/src/Sonarr.Api.V3/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Sonarr.Api.V3/packages.config b/src/Sonarr.Api.V3/packages.config new file mode 100644 index 000000000..f68f69f6b --- /dev/null +++ b/src/Sonarr.Api.V3/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Api/Authentication/1tews5g3.gd1~ b/src/Sonarr.Http/Authentication/1tews5g3.gd1~ similarity index 100% rename from src/NzbDrone.Api/Authentication/1tews5g3.gd1~ rename to src/Sonarr.Http/Authentication/1tews5g3.gd1~ diff --git a/src/NzbDrone.Api/Authentication/AuthenticationModule.cs b/src/Sonarr.Http/Authentication/AuthenticationModule.cs similarity index 89% rename from src/NzbDrone.Api/Authentication/AuthenticationModule.cs rename to src/Sonarr.Http/Authentication/AuthenticationModule.cs index df940a947..ed57e686a 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationModule.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationModule.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Authentication +namespace Sonarr.Http.Authentication { public class AuthenticationModule : NancyModule { @@ -33,7 +33,8 @@ namespace NzbDrone.Api.Authentication if (user == null) { - return Context.GetRedirect("~/login?returnUrl=" + (string)Request.Query.returnUrl); + var returnUrl = (string)Request.Query.returnUrl; + return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); } DateTime? expiry = null; diff --git a/src/NzbDrone.Api/Authentication/AuthenticationService.cs b/src/Sonarr.Http/Authentication/AuthenticationService.cs similarity index 95% rename from src/NzbDrone.Api/Authentication/AuthenticationService.cs rename to src/Sonarr.Http/Authentication/AuthenticationService.cs index beb908b11..3c761c9e4 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationService.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationService.cs @@ -4,12 +4,12 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Authentication.Forms; using Nancy.Security; -using NzbDrone.Api.Extensions; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; +using Sonarr.Http.Extensions; -namespace NzbDrone.Api.Authentication +namespace Sonarr.Http.Authentication { public interface IAuthenticationService : IUserValidator, IUserMapper { @@ -18,7 +18,6 @@ namespace NzbDrone.Api.Authentication public class AuthenticationService : IAuthenticationService { - private readonly IConfigFileProvider _configFileProvider; private readonly IUserService _userService; private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" }; @@ -27,7 +26,6 @@ namespace NzbDrone.Api.Authentication public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) { - _configFileProvider = configFileProvider; _userService = userService; API_KEY = configFileProvider.ApiKey; AUTH_METHOD = configFileProvider.AuthenticationMethod; diff --git a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs similarity index 62% rename from src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs rename to src/Sonarr.Http/Authentication/EnableAuthInNancy.cs index 4feebbdb4..942750742 100644 --- a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs +++ b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs @@ -1,22 +1,25 @@ -using System; +using System; using System.Text; using Nancy; using Nancy.Authentication.Basic; using Nancy.Authentication.Forms; using Nancy.Bootstrapper; +using Nancy.Cookies; using Nancy.Cryptography; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Extensions.Pipelines; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; +using Sonarr.Http.Extensions; +using Sonarr.Http.Extensions.Pipelines; -namespace NzbDrone.Api.Authentication +namespace Sonarr.Http.Authentication { public class EnableAuthInNancy : IRegisterNancyPipeline { private readonly IAuthenticationService _authenticationService; private readonly IConfigService _configService; private readonly IConfigFileProvider _configFileProvider; + private FormsAuthenticationConfiguration FormsAuthConfig; public EnableAuthInNancy(IAuthenticationService authenticationService, IConfigService configService, @@ -34,6 +37,7 @@ namespace NzbDrone.Api.Authentication if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) { RegisterFormsAuth(pipelines); + pipelines.AfterRequest.AddItemToEndOfPipeline((Action)SlidingAuthenticationForFormsAuth); } else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) @@ -41,8 +45,8 @@ namespace NzbDrone.Api.Authentication pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr")); } - pipelines.BeforeRequest.AddItemToEndOfPipeline((Func) RequiresAuthentication); - pipelines.AfterRequest.AddItemToEndOfPipeline((Action) RemoveLoginHooksForApiCalls); + pipelines.BeforeRequest.AddItemToEndOfPipeline((Func)RequiresAuthentication); + pipelines.AfterRequest.AddItemToEndOfPipeline((Action)RemoveLoginHooksForApiCalls); } private Response RequiresAuthentication(NancyContext context) @@ -59,20 +63,22 @@ namespace NzbDrone.Api.Authentication private void RegisterFormsAuth(IPipelines pipelines) { + FormsAuthentication.FormsAuthenticationCookieName = "SonarrAuth"; + var cryptographyConfiguration = new CryptographyConfiguration( new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))) ); - FormsAuthentication.FormsAuthenticationCookieName = "_ncfa_sonarr"; - - FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration + FormsAuthConfig = new FormsAuthenticationConfiguration { RedirectUrl = _configFileProvider.UrlBase + "/login", UserMapper = _authenticationService, - Path = _configFileProvider.UrlBase, + Path = GetCookiePath(), CryptographyConfiguration = cryptographyConfiguration - }); + }; + + FormsAuthentication.Enable(pipelines, FormsAuthConfig); } private void RemoveLoginHooksForApiCalls(NancyContext context) @@ -87,5 +93,43 @@ namespace NzbDrone.Api.Authentication } } } + + private void SlidingAuthenticationForFormsAuth(NancyContext context) + { + if (context.CurrentUser == null) + { + return; + } + + var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName; + + if (!context.Request.Path.Equals("/logout") && + context.Request.Cookies.ContainsKey(formsAuthCookieName)) + { + var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName]; + + if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, FormsAuthConfig).IsNotNullOrWhiteSpace()) + { + var formsAuthCookie = new NancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7)) + { + Path = GetCookiePath() + }; + + context.Response.WithCookie(formsAuthCookie); + } + } + } + + private string GetCookiePath() + { + var urlBase = _configFileProvider.UrlBase; + + if (urlBase.IsNullOrWhiteSpace()) + { + return "/"; + } + + return urlBase; + } } } diff --git a/src/NzbDrone.Api/Authentication/LoginResource.cs b/src/Sonarr.Http/Authentication/LoginResource.cs similarity index 81% rename from src/NzbDrone.Api/Authentication/LoginResource.cs rename to src/Sonarr.Http/Authentication/LoginResource.cs index 5d6a5c9f5..0a725541c 100644 --- a/src/NzbDrone.Api/Authentication/LoginResource.cs +++ b/src/Sonarr.Http/Authentication/LoginResource.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Authentication +namespace Sonarr.Http.Authentication { public class LoginResource { diff --git a/src/NzbDrone.Api/Authentication/NzbDroneUser.cs b/src/Sonarr.Http/Authentication/NzbDroneUser.cs similarity index 85% rename from src/NzbDrone.Api/Authentication/NzbDroneUser.cs rename to src/Sonarr.Http/Authentication/NzbDroneUser.cs index c8fce02fd..3614d1de9 100644 --- a/src/NzbDrone.Api/Authentication/NzbDroneUser.cs +++ b/src/Sonarr.Http/Authentication/NzbDroneUser.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Nancy.Security; -namespace NzbDrone.Api.Authentication +namespace Sonarr.Http.Authentication { public class NzbDroneUser : IUserIdentity { diff --git a/src/NzbDrone.Api/ClientSchema/Field.cs b/src/Sonarr.Http/ClientSchema/Field.cs similarity index 83% rename from src/NzbDrone.Api/ClientSchema/Field.cs rename to src/Sonarr.Http/ClientSchema/Field.cs index ff9f6aebd..2e4e4db1a 100644 --- a/src/NzbDrone.Api/ClientSchema/Field.cs +++ b/src/Sonarr.Http/ClientSchema/Field.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace NzbDrone.Api.ClientSchema +namespace Sonarr.Http.ClientSchema { public class Field { @@ -14,6 +14,7 @@ namespace NzbDrone.Api.ClientSchema public string Type { get; set; } public bool Advanced { get; set; } public List SelectOptions { get; set; } + public string Section { get; set; } public Field Clone() { diff --git a/src/NzbDrone.Api/ClientSchema/FieldMapping.cs b/src/Sonarr.Http/ClientSchema/FieldMapping.cs similarity index 67% rename from src/NzbDrone.Api/ClientSchema/FieldMapping.cs rename to src/Sonarr.Http/ClientSchema/FieldMapping.cs index 93e90b792..649c1ad69 100644 --- a/src/NzbDrone.Api/ClientSchema/FieldMapping.cs +++ b/src/Sonarr.Http/ClientSchema/FieldMapping.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System; -namespace NzbDrone.Api.ClientSchema +namespace Sonarr.Http.ClientSchema { public class FieldMapping { diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs similarity index 98% rename from src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs rename to src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index c2e4a4379..6fc751151 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -8,7 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; -namespace NzbDrone.Api.ClientSchema +namespace Sonarr.Http.ClientSchema { public static class SchemaBuilder { @@ -100,7 +100,8 @@ namespace NzbDrone.Api.ClientSchema HelpLink = fieldAttribute.HelpLink, Order = fieldAttribute.Order, Advanced = fieldAttribute.Advanced, - Type = fieldAttribute.Type.ToString().ToLowerInvariant() + Type = fieldAttribute.Type.ToString().ToLowerInvariant(), + Section = fieldAttribute.Section, }; if (fieldAttribute.Type == FieldType.Select) diff --git a/src/NzbDrone.Api/ClientSchema/SelectOption.cs b/src/Sonarr.Http/ClientSchema/SelectOption.cs similarity index 76% rename from src/NzbDrone.Api/ClientSchema/SelectOption.cs rename to src/Sonarr.Http/ClientSchema/SelectOption.cs index fe42f46dd..e90648640 100644 --- a/src/NzbDrone.Api/ClientSchema/SelectOption.cs +++ b/src/Sonarr.Http/ClientSchema/SelectOption.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.ClientSchema +namespace Sonarr.Http.ClientSchema { public class SelectOption { diff --git a/src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs b/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs similarity index 93% rename from src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs rename to src/Sonarr.Http/ErrorManagement/ErrorHandler.cs index 6ba25d741..edcac2ede 100644 --- a/src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs +++ b/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs @@ -1,8 +1,8 @@ using Nancy; using Nancy.ErrorHandling; -using NzbDrone.Api.Extensions; +using Sonarr.Http.Extensions; -namespace NzbDrone.Api.ErrorManagement +namespace Sonarr.Http.ErrorManagement { public class ErrorHandler : IStatusCodeHandler { diff --git a/src/NzbDrone.Api/ErrorManagement/ErrorModel.cs b/src/Sonarr.Http/ErrorManagement/ErrorModel.cs similarity index 83% rename from src/NzbDrone.Api/ErrorManagement/ErrorModel.cs rename to src/Sonarr.Http/ErrorManagement/ErrorModel.cs index b88600717..3d6c22dff 100644 --- a/src/NzbDrone.Api/ErrorManagement/ErrorModel.cs +++ b/src/Sonarr.Http/ErrorManagement/ErrorModel.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Api.ErrorManagement +using Sonarr.Http.Exceptions; + +namespace Sonarr.Http.ErrorManagement { public class ErrorModel { diff --git a/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs b/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs similarity index 92% rename from src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs rename to src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs index d98925f8e..ab2f8a21d 100644 --- a/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs +++ b/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs @@ -1,19 +1,20 @@ using System; using System.Data.SQLite; using FluentValidation; -using NLog; using Nancy; -using NzbDrone.Api.Extensions; +using NLog; using NzbDrone.Core.Exceptions; +using Sonarr.Http.Exceptions; +using Sonarr.Http.Extensions; using HttpStatusCode = Nancy.HttpStatusCode; -namespace NzbDrone.Api.ErrorManagement +namespace Sonarr.Http.ErrorManagement { - public class NzbDroneErrorPipeline + public class SonarrErrorPipeline { private readonly Logger _logger; - public NzbDroneErrorPipeline(Logger logger) + public SonarrErrorPipeline(Logger logger) { _logger = logger; } diff --git a/src/NzbDrone.Api/ErrorManagement/ApiException.cs b/src/Sonarr.Http/Exceptions/ApiException.cs similarity index 90% rename from src/NzbDrone.Api/ErrorManagement/ApiException.cs rename to src/Sonarr.Http/Exceptions/ApiException.cs index 2a9f2678f..0eefd8474 100644 --- a/src/NzbDrone.Api/ErrorManagement/ApiException.cs +++ b/src/Sonarr.Http/Exceptions/ApiException.cs @@ -1,9 +1,10 @@ using System; using Nancy; using Nancy.Responses; -using NzbDrone.Api.Extensions; +using Sonarr.Http.ErrorManagement; +using Sonarr.Http.Extensions; -namespace NzbDrone.Api.ErrorManagement +namespace Sonarr.Http.Exceptions { public abstract class ApiException : Exception { diff --git a/src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs b/src/Sonarr.Http/Exceptions/InvalidApiKeyException.cs similarity index 87% rename from src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs rename to src/Sonarr.Http/Exceptions/InvalidApiKeyException.cs index 8c16e8133..d319ed731 100644 --- a/src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs +++ b/src/Sonarr.Http/Exceptions/InvalidApiKeyException.cs @@ -1,6 +1,6 @@ using System; -namespace NzbDrone.Api.Exceptions +namespace Sonarr.Http.Exceptions { public class InvalidApiKeyException : Exception { diff --git a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs b/src/Sonarr.Http/Extensions/AccessControlHeaders.cs similarity index 92% rename from src/NzbDrone.Api/Extensions/AccessControlHeaders.cs rename to src/Sonarr.Http/Extensions/AccessControlHeaders.cs index 5a32395cb..84ac606ef 100644 --- a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs +++ b/src/Sonarr.Http/Extensions/AccessControlHeaders.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Extensions +namespace Sonarr.Http.Extensions { public static class AccessControlHeaders { diff --git a/src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs similarity index 93% rename from src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs rename to src/Sonarr.Http/Extensions/NancyJsonSerializer.cs index 00b3c3b2c..079d4cdf9 100644 --- a/src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs +++ b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs @@ -3,7 +3,7 @@ using System.IO; using Nancy; using NzbDrone.Common.Serializer; -namespace NzbDrone.Api.Extensions +namespace Sonarr.Http.Extensions { public class NancyJsonSerializer : ISerializer { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs similarity index 92% rename from src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs index d8e9266ad..b08d5d6e5 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs @@ -1,9 +1,9 @@ using System; using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using Sonarr.Http.Frontend; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { public class CacheHeaderPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs index ad98837e8..5b64f9c2c 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs @@ -4,7 +4,7 @@ using Nancy; using Nancy.Bootstrapper; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { public class CorsPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs index 8aa9f4ad2..d7ff3f3d4 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs @@ -7,7 +7,7 @@ using Nancy.Bootstrapper; using NLog; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { public class GzipCompressionPipeline : IRegisterNancyPipeline { @@ -64,20 +64,24 @@ namespace NzbDrone.Api.Extensions.Pipelines private static bool ContentLengthIsTooSmall(Response response) { var contentLength = response.Headers.GetValueOrDefault("Content-Length"); + if (contentLength != null && long.Parse(contentLength) < 1024) { return true; } + return false; } private static bool AlreadyGzipEncoded(Response response) { var contentEncoding = response.Headers.GetValueOrDefault("Content-Encoding"); + if (contentEncoding == "gzip") { return true; } + return false; } } diff --git a/src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs similarity index 78% rename from src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs index 0376ccc70..81dc848dd 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs @@ -1,6 +1,6 @@ using Nancy.Bootstrapper; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { public interface IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs similarity index 93% rename from src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs index 68abf4ade..788876d76 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs @@ -1,9 +1,9 @@ using System; using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using Sonarr.Http.Frontend; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { public class IfModifiedPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs similarity index 89% rename from src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs index 73668bc81..200c4e299 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs @@ -3,8 +3,10 @@ using System.Threading; using Nancy; using Nancy.Bootstrapper; using NLog; -using NzbDrone.Api.ErrorManagement; using NzbDrone.Common.Extensions; +using Sonarr.Http.ErrorManagement; +using Sonarr.Http.Extensions; +using Sonarr.Http.Extensions.Pipelines; namespace NzbDrone.Api.Extensions.Pipelines { @@ -15,9 +17,9 @@ namespace NzbDrone.Api.Extensions.Pipelines private static int _requestSequenceID; - private readonly NzbDroneErrorPipeline _errorPipeline; + private readonly SonarrErrorPipeline _errorPipeline; - public RequestLoggingPipeline(NzbDroneErrorPipeline errorPipeline) + public RequestLoggingPipeline(SonarrErrorPipeline errorPipeline) { _errorPipeline = errorPipeline; } diff --git a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs similarity index 84% rename from src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs index 00488657b..cb25e6833 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs @@ -3,9 +3,9 @@ using Nancy; using Nancy.Bootstrapper; using NzbDrone.Common.EnvironmentInfo; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { - public class NzbDroneVersionPipeline : IRegisterNancyPipeline + public class SonarrVersionPipeline : IRegisterNancyPipeline { public int Order => 0; diff --git a/src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs similarity index 94% rename from src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs index d8c765e67..a12451f4e 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs @@ -1,11 +1,11 @@ -using System; +using System; using Nancy; using Nancy.Bootstrapper; using Nancy.Responses; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Sonarr.Http.Extensions.Pipelines { public class UrlBasePipeline : IRegisterNancyPipeline { @@ -43,4 +43,4 @@ namespace NzbDrone.Api.Extensions.Pipelines return null; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Extensions/ReqResExtensions.cs b/src/Sonarr.Http/Extensions/ReqResExtensions.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/ReqResExtensions.cs rename to src/Sonarr.Http/Extensions/ReqResExtensions.cs index 1f1d89180..533bc8dd1 100644 --- a/src/NzbDrone.Api/Extensions/ReqResExtensions.cs +++ b/src/Sonarr.Http/Extensions/ReqResExtensions.cs @@ -6,7 +6,7 @@ using Nancy.Responses; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Serializer; -namespace NzbDrone.Api.Extensions +namespace Sonarr.Http.Extensions { public static class ReqResExtensions { diff --git a/src/NzbDrone.Api/Extensions/RequestExtensions.cs b/src/Sonarr.Http/Extensions/RequestExtensions.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/RequestExtensions.cs rename to src/Sonarr.Http/Extensions/RequestExtensions.cs index f08c0c075..84843494e 100644 --- a/src/NzbDrone.Api/Extensions/RequestExtensions.cs +++ b/src/Sonarr.Http/Extensions/RequestExtensions.cs @@ -1,7 +1,7 @@ using System; using Nancy; -namespace NzbDrone.Api.Extensions +namespace Sonarr.Http.Extensions { public static class RequestExtensions { diff --git a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs b/src/Sonarr.Http/Frontend/CacheableSpecification.cs similarity index 88% rename from src/NzbDrone.Api/Frontend/CacheableSpecification.cs rename to src/Sonarr.Http/Frontend/CacheableSpecification.cs index 7995c7da1..457ef8a50 100644 --- a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs +++ b/src/Sonarr.Http/Frontend/CacheableSpecification.cs @@ -3,7 +3,7 @@ using Nancy; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend +namespace Sonarr.Http.Frontend { public interface ICacheableSpecification { @@ -29,7 +29,8 @@ namespace NzbDrone.Api.Frontend } if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) return false; - if (context.Request.Path.EndsWith("main.js")) return false; + if (context.Request.Path.EndsWith("index.js")) return false; + if (context.Request.Path.EndsWith("initialize.js")) return false; if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) return false; if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) && @@ -46,4 +47,4 @@ namespace NzbDrone.Api.Frontend return true; } } -} \ No newline at end of file +} diff --git a/src/Sonarr.Http/Frontend/InitializeJsModule.cs b/src/Sonarr.Http/Frontend/InitializeJsModule.cs new file mode 100644 index 000000000..968d554a0 --- /dev/null +++ b/src/Sonarr.Http/Frontend/InitializeJsModule.cs @@ -0,0 +1,75 @@ +using System.IO; +using System.Text; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Analytics; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend +{ + public class InitializeJsModule : NancyModule + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IAnalyticsService _analyticsService; + + private static string _apiKey; + private static string _urlBase; + private string _generatedContent; + + public InitializeJsModule(IConfigFileProvider configFileProvider, + IAnalyticsService analyticsService) + { + _configFileProvider = configFileProvider; + _analyticsService = analyticsService; + + _apiKey = configFileProvider.ApiKey; + _urlBase = configFileProvider.UrlBase; + + Get["/initialize.js"] = x => Index(); + } + + private Response Index() + { + // TODO: Move away from window.Sonarr and prefetch the information returned here when starting the UI + return new StreamResponse(GetContentStream, "application/javascript"); + } + + private Stream GetContentStream() + { + var text = GetContent(); + + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + + return stream; + } + + private string GetContent() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var builder = new StringBuilder(); + builder.AppendLine("window.Sonarr = {"); + builder.AppendLine($" apiRoot: '{_urlBase}/api/v3',"); + builder.AppendLine($" apiKey: '{_apiKey}',"); + builder.AppendLine($" release: '{BuildInfo.Release}',"); + builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',"); + builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',"); + builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); + builder.AppendLine($" urlBase: '{_urlBase}',"); + builder.AppendLine($" isProduction: {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}"); + builder.AppendLine("};"); + + _generatedContent = builder.ToString(); + + return _generatedContent; + } + } +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs index 9e4912524..2ce35f9f6 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class BackupFileMapper : StaticResourceMapperBase { diff --git a/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs new file mode 100644 index 000000000..50eb6103f --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -0,0 +1,34 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend.Mappers +{ + public class BrowserConfig : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + + public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; + } + + public override string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = path.Trim(Path.DirectorySeparatorChar); + + return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "xml"); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/content/images/icons/browserconfig"); + } + } +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs similarity index 96% rename from src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs rename to src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index 53ebb2986..ab7324124 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -3,7 +3,7 @@ using System.Linq; using NzbDrone.Common.Crypto; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public interface ICacheBreakerProvider { diff --git a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs b/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs similarity index 88% rename from src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs index 002ffa7ce..c007e0a89 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs @@ -1,10 +1,10 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class FaviconMapper : StaticResourceMapperBase { @@ -27,7 +27,7 @@ namespace NzbDrone.Api.Frontend.Mappers fileName = "favicon-debug.ico"; } - var path = Path.Combine("Content", "Images", fileName); + var path = Path.Combine("Content", "Images", "Icons", fileName); return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); } diff --git a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs new file mode 100644 index 000000000..b31ce1b6c --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using Nancy; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace Sonarr.Http.Frontend.Mappers +{ + public abstract class HtmlMapperBase : StaticResourceMapperBase + { + private readonly IDiskProvider _diskProvider; + private readonly Func _cacheBreakProviderFactory; + private static readonly Regex ReplaceRegex = new Regex(@"(?:(?href|src)=\"")(?.*?(?css|js|png|ico|ics|svg))(?:\"")(?:\s(?data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private string _generatedContent; + + protected HtmlMapperBase(IDiskProvider diskProvider, + Func cacheBreakProviderFactory, + Logger logger) : base(diskProvider, logger) + { + _diskProvider = diskProvider; + _cacheBreakProviderFactory = cacheBreakProviderFactory; + } + + protected string HtmlPath; + protected string UrlBase; + + protected override Stream GetContentStream(string filePath) + { + var text = GetHtmlText(); + + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + return stream; + } + + public override Response GetResponse(string resourceUrl) + { + var response = base.GetResponse(resourceUrl); + response.Headers["X-UA-Compatible"] = "IE=edge"; + + return response; + } + + protected string GetHtmlText() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var text = _diskProvider.ReadAllText(HtmlPath); + var cacheBreakProvider = _cacheBreakProviderFactory(); + + text = ReplaceRegex.Replace(text, match => + { + string url; + + if (match.Groups["nohash"].Success) + { + url = match.Groups["path"].Value; + } + + else + { + url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value); + } + + return string.Format("{0}=\"{1}{2}\"", match.Groups["attribute"].Value, UrlBase, url); + }); + + _generatedContent = text; + + return _generatedContent; + } + } +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs similarity index 84% rename from src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs rename to src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 6390a2545..885d08a9e 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,7 +1,7 @@  using Nancy; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public interface IMapHttpRequestsToDisk { diff --git a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs new file mode 100644 index 000000000..fde71dd1b --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend.Mappers +{ + public class IndexHtmlMapper : HtmlMapperBase + { + private readonly IConfigFileProvider _configFileProvider; + + public IndexHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + Func cacheBreakProviderFactory, + Logger logger) + : base(diskProvider, cacheBreakProviderFactory, logger) + { + _configFileProvider = configFileProvider; + + HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "index.html"); + UrlBase = configFileProvider.UrlBase; + } + + public override string Map(string resourceUrl) + { + return HtmlPath; + } + + public override bool CanHandle(string resourceUrl) + { + resourceUrl = resourceUrl.ToLowerInvariant(); + + return !resourceUrl.StartsWith("/content") && + !resourceUrl.StartsWith("/mediacover") && + !resourceUrl.Contains(".") && + !resourceUrl.StartsWith("/login"); + } + } +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs index 5fda7d483..376b82581 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class UpdateLogFileMapper : StaticResourceMapperBase { diff --git a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs new file mode 100644 index 000000000..b2040eb95 --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using Nancy; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend.Mappers +{ + public class LoginHtmlMapper : HtmlMapperBase + { + public LoginHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + Func cacheBreakProviderFactory, + IConfigFileProvider configFileProvider, + Logger logger) + : base(diskProvider, cacheBreakProviderFactory, logger) + { + HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); + UrlBase = configFileProvider.UrlBase; + } + + public override string Map(string resourceUrl) + { + return HtmlPath; + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/login"); + } + } +} diff --git a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs new file mode 100644 index 000000000..266557e20 --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -0,0 +1,34 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend.Mappers +{ + public class ManifestMapper : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + + public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; + } + + public override string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = path.Trim(Path.DirectorySeparatorChar); + + return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "json"); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/Content/Images/Icons/manifest"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs similarity index 97% rename from src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs index 8a5626cf2..78d36f5fc 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -6,7 +6,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class MediaCoverMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs b/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs similarity index 96% rename from src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs index 60b3131c6..e17fb267e 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class RobotsTxtMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs similarity index 80% rename from src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 4b3b939a1..8b3e0d4a6 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -5,7 +5,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class StaticResourceMapper : StaticResourceMapperBase { @@ -30,9 +30,15 @@ namespace NzbDrone.Api.Frontend.Mappers public override bool CanHandle(string resourceUrl) { resourceUrl = resourceUrl.ToLowerInvariant(); + + if (resourceUrl.StartsWith("/content/images/icons/manifest") || + resourceUrl.StartsWith("/content/images/icons/browserconfig")) + { + return false; + } return resourceUrl.StartsWith("/content") || - resourceUrl.EndsWith(".js") || + (resourceUrl.EndsWith(".js") && !resourceUrl.EndsWith("initialize.js")) || resourceUrl.EndsWith(".map") || resourceUrl.EndsWith(".css") || (resourceUrl.EndsWith(".ico") && !resourceUrl.Equals("/favicon.ico")) || diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs similarity index 97% rename from src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs rename to src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 6f7088832..a6cfe03e2 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -1,12 +1,12 @@ using System; using System.IO; -using NLog; using Nancy; using Nancy.Responses; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public abstract class StaticResourceMapperBase : IMapHttpRequestsToDisk { diff --git a/src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index 021bdba58..95a352d47 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Sonarr.Http.Frontend.Mappers { public class LogFileMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs b/src/Sonarr.Http/Frontend/StaticResourceModule.cs similarity index 79% rename from src/NzbDrone.Api/Frontend/StaticResourceModule.cs rename to src/Sonarr.Http/Frontend/StaticResourceModule.cs index 270f48387..60569a8bc 100644 --- a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs +++ b/src/Sonarr.Http/Frontend/StaticResourceModule.cs @@ -1,24 +1,21 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using NLog; using Nancy; -using NzbDrone.Api.Frontend.Mappers; -using NzbDrone.Core.Configuration; +using NLog; +using Sonarr.Http.Frontend.Mappers; -namespace NzbDrone.Api.Frontend +namespace Sonarr.Http.Frontend { public class StaticResourceModule : NancyModule { private readonly IEnumerable _requestMappers; - private readonly IConfigFileProvider _configFileProvider; private readonly Logger _logger; - public StaticResourceModule(IEnumerable requestMappers, IConfigFileProvider configFileProvider, Logger logger) + public StaticResourceModule(IEnumerable requestMappers, Logger logger) { _requestMappers = requestMappers; - _configFileProvider = configFileProvider; _logger = logger; Get["/{resource*}"] = x => Index(); diff --git a/src/Sonarr.Http/Mapping/MappingValidation.cs b/src/Sonarr.Http/Mapping/MappingValidation.cs new file mode 100644 index 000000000..4b055c73f --- /dev/null +++ b/src/Sonarr.Http/Mapping/MappingValidation.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Reflection; +using NzbDrone.Common.Reflection; +using Sonarr.Http.REST; + +namespace Sonarr.Http.Mapping +{ + public static class MappingValidation + { + public static void ValidateMapping(Type modelType, Type resourceType) + { + var errors = modelType.GetSimpleProperties().Where(c=>!c.GetGetMethod().IsStatic).Select(p => GetError(resourceType, p)).Where(c => c != null).ToList(); + + if (errors.Any()) + { + throw new ResourceMappingException(errors); + } + + PrintExtraProperties(modelType, resourceType); + } + + private static void PrintExtraProperties(Type modelType, Type resourceType) + { + var resourceBaseProperties = typeof(RestResource).GetProperties().Select(c => c.Name); + var resourceProperties = resourceType.GetProperties().Select(c => c.Name).Except(resourceBaseProperties); + var modelProperties = modelType.GetProperties().Select(c => c.Name); + + var extra = resourceProperties.Except(modelProperties); + + foreach (var extraProp in extra) + { + Console.WriteLine("Extra: [{0}]", extraProp); + } + } + + private static string GetError(Type resourceType, PropertyInfo modelProperty) + { + var resourceProperty = resourceType.GetProperties().FirstOrDefault(c => c.Name == modelProperty.Name); + + if (resourceProperty == null) + { + return string.Format("public {0} {1} {{ get; set; }}", modelProperty.PropertyType.Name, modelProperty.Name); + } + + if (resourceProperty.PropertyType != modelProperty.PropertyType && !typeof(RestResource).IsAssignableFrom(resourceProperty.PropertyType)) + { + return string.Format("Expected {0}.{1} to have type of {2} but found {3}", resourceType.Name, resourceProperty.Name, modelProperty.PropertyType, resourceProperty.PropertyType); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Http/Mapping/ResourceMappingException.cs b/src/Sonarr.Http/Mapping/ResourceMappingException.cs new file mode 100644 index 000000000..42d564b37 --- /dev/null +++ b/src/Sonarr.Http/Mapping/ResourceMappingException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Sonarr.Http.Mapping +{ + public class ResourceMappingException : ApplicationException + { + public ResourceMappingException(IEnumerable error) + : base(Environment.NewLine + string.Join(Environment.NewLine, error.OrderBy(c => c))) + { + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/PagingResource.cs b/src/Sonarr.Http/PagingResource.cs similarity index 89% rename from src/NzbDrone.Api/PagingResource.cs rename to src/Sonarr.Http/PagingResource.cs index b8025efc4..4525f80cc 100644 --- a/src/NzbDrone.Api/PagingResource.cs +++ b/src/Sonarr.Http/PagingResource.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Datastore; -namespace NzbDrone.Api +namespace Sonarr.Http { public class PagingResource { @@ -9,8 +9,7 @@ namespace NzbDrone.Api public int PageSize { get; set; } public string SortKey { get; set; } public SortDirection SortDirection { get; set; } - public string FilterKey { get; set; } - public string FilterValue { get; set; } + public List Filters { get; set; } public int TotalRecords { get; set; } public List Records { get; set; } } diff --git a/src/Sonarr.Http/PagingResourceFilter.cs b/src/Sonarr.Http/PagingResourceFilter.cs new file mode 100644 index 000000000..303097a9a --- /dev/null +++ b/src/Sonarr.Http/PagingResourceFilter.cs @@ -0,0 +1,8 @@ +namespace Sonarr.Http +{ + public class PagingResourceFilter + { + public string Key { get; set; } + public string Value { get; set; } + } +} diff --git a/src/Sonarr.Http/Properties/AssemblyInfo.cs b/src/Sonarr.Http/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9d340b973 --- /dev/null +++ b/src/Sonarr.Http/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Sonarr.Nancy")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Sonarr.Nancy")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5370bff7-1bd7-46bc-af06-7d9ea5cda1d6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Api/REST/BadRequestException.cs b/src/Sonarr.Http/REST/BadRequestException.cs similarity index 76% rename from src/NzbDrone.Api/REST/BadRequestException.cs rename to src/Sonarr.Http/REST/BadRequestException.cs index 450f484e5..37b94f5c1 100644 --- a/src/NzbDrone.Api/REST/BadRequestException.cs +++ b/src/Sonarr.Http/REST/BadRequestException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Sonarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Sonarr.Http.REST { public class BadRequestException : ApiException { diff --git a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs b/src/Sonarr.Http/REST/MethodNotAllowedException.cs similarity index 78% rename from src/NzbDrone.Api/REST/MethodNotAllowedException.cs rename to src/Sonarr.Http/REST/MethodNotAllowedException.cs index 44d2065c6..6d34170fc 100644 --- a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs +++ b/src/Sonarr.Http/REST/MethodNotAllowedException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Sonarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Sonarr.Http.REST { public class MethodNotAllowedException : ApiException { diff --git a/src/NzbDrone.Api/REST/NotFoundException.cs b/src/Sonarr.Http/REST/NotFoundException.cs similarity index 76% rename from src/NzbDrone.Api/REST/NotFoundException.cs rename to src/Sonarr.Http/REST/NotFoundException.cs index 92b4016a9..8b6109a5f 100644 --- a/src/NzbDrone.Api/REST/NotFoundException.cs +++ b/src/Sonarr.Http/REST/NotFoundException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Sonarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Sonarr.Http.REST { public class NotFoundException : ApiException { diff --git a/src/NzbDrone.Api/REST/ResourceValidator.cs b/src/Sonarr.Http/REST/ResourceValidator.cs similarity index 95% rename from src/NzbDrone.Api/REST/ResourceValidator.cs rename to src/Sonarr.Http/REST/ResourceValidator.cs index 8062e6fd0..6f91bf8f0 100644 --- a/src/NzbDrone.Api/REST/ResourceValidator.cs +++ b/src/Sonarr.Http/REST/ResourceValidator.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using FluentValidation; using FluentValidation.Internal; using FluentValidation.Resources; -using NzbDrone.Api.ClientSchema; -using System.Linq; +using Sonarr.Http.ClientSchema; -namespace NzbDrone.Api.REST +namespace Sonarr.Http.REST { public class ResourceValidator : AbstractValidator { diff --git a/src/NzbDrone.Api/REST/RestModule.cs b/src/Sonarr.Http/REST/RestModule.cs similarity index 78% rename from src/NzbDrone.Api/REST/RestModule.cs rename to src/Sonarr.Http/REST/RestModule.cs index 7c6ba37a4..65596501f 100644 --- a/src/NzbDrone.Api/REST/RestModule.cs +++ b/src/Sonarr.Http/REST/RestModule.cs @@ -1,12 +1,12 @@ -using System; +using System; using System.Collections.Generic; +using System.Linq; using FluentValidation; using Nancy; -using NzbDrone.Api.Extensions; -using System.Linq; using NzbDrone.Core.Datastore; +using Sonarr.Http.Extensions; -namespace NzbDrone.Api.REST +namespace Sonarr.Http.REST { public abstract class RestModule : NancyModule where TResource : RestResource, new() @@ -14,6 +14,16 @@ namespace NzbDrone.Api.REST private const string ROOT_ROUTE = "/"; private const string ID_ROUTE = @"/(?[\d]{1,10})"; + private HashSet EXCLUDED_KEYS = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "page", + "pageSize", + "sortKey", + "sortDirection", + "filterKey", + "filterValue", + }; + private Action _deleteResource; private Func _getResourceById; private Func> _getResourceAll; @@ -226,12 +236,14 @@ namespace NzbDrone.Api.REST { PageSize = pageSize, Page = page, + Filters = new List() }; if (Request.Query.SortKey != null) { pagingResource.SortKey = Request.Query.SortKey.ToString(); + // For backwards compatibility with v2 if (Request.Query.SortDir != null) { pagingResource.SortDirection = Request.Query.SortDir.ToString() @@ -239,19 +251,50 @@ namespace NzbDrone.Api.REST ? SortDirection.Ascending : SortDirection.Descending; } + + // v3 uses SortDirection instead of SortDir to be consistent with every other use of it + if (Request.Query.SortDirection != null) + { + pagingResource.SortDirection = Request.Query.SortDirection.ToString() + .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) + ? SortDirection.Ascending + : SortDirection.Descending; + } } + // For backwards compatibility with v2 if (Request.Query.FilterKey != null) { - pagingResource.FilterKey = Request.Query.FilterKey.ToString(); + var filter = new PagingResourceFilter + { + Key = Request.Query.FilterKey.ToString() + }; if (Request.Query.FilterValue != null) { - pagingResource.FilterValue = Request.Query.FilterValue.ToString(); + filter.Value = Request.Query.FilterValue?.ToString(); } + + pagingResource.Filters.Add(filter); + } + + // v3 uses filters in key=value format + + foreach (var key in Request.Query) + { + if (EXCLUDED_KEYS.Contains(key)) + { + continue; + } + + pagingResource.Filters.Add(new PagingResourceFilter + { + Key = key, + Value = Request.Query[key] + }); } return pagingResource; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/REST/RestResource.cs b/src/Sonarr.Http/REST/RestResource.cs similarity index 91% rename from src/NzbDrone.Api/REST/RestResource.cs rename to src/Sonarr.Http/REST/RestResource.cs index ec9f195c6..d6347882a 100644 --- a/src/NzbDrone.Api/REST/RestResource.cs +++ b/src/Sonarr.Http/REST/RestResource.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace NzbDrone.Api.REST +namespace Sonarr.Http.REST { public abstract class RestResource { diff --git a/src/NzbDrone.Api/ResourceChangeMessage.cs b/src/Sonarr.Http/ResourceChangeMessage.cs similarity index 93% rename from src/NzbDrone.Api/ResourceChangeMessage.cs rename to src/Sonarr.Http/ResourceChangeMessage.cs index 6319dcc39..f0c54f881 100644 --- a/src/NzbDrone.Api/ResourceChangeMessage.cs +++ b/src/Sonarr.Http/ResourceChangeMessage.cs @@ -1,8 +1,8 @@ using System; -using NzbDrone.Api.REST; using NzbDrone.Core.Datastore.Events; +using Sonarr.Http.REST; -namespace NzbDrone.Api +namespace Sonarr.Http { public class ResourceChangeMessage where TResource : RestResource { diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj new file mode 100644 index 000000000..3cd96b074 --- /dev/null +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -0,0 +1,159 @@ + + + + + Debug + AnyCPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Library + Properties + Sonarr.Http + Sonarr.Http + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll + True + + + ..\packages\Nancy.1.4.3\lib\net40\Nancy.dll + True + + + ..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll + True + + + ..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll + True + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll + True + + + ..\packages\NLog.4.4.3\lib\net40\NLog.dll + + + + + ..\Libraries\Sqlite\System.Data.SQLite.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} + Marr.Data + + + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} + NzbDrone.Common + + + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205} + NzbDrone.Core + + + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} + NzbDrone.SignalR + + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Api/NancyBootstrapper.cs b/src/Sonarr.Http/SonarrBootstrapper.cs similarity index 87% rename from src/NzbDrone.Api/NancyBootstrapper.cs rename to src/Sonarr.Http/SonarrBootstrapper.cs index a34641659..a8a8ae1e8 100644 --- a/src/NzbDrone.Api/NancyBootstrapper.cs +++ b/src/Sonarr.Http/SonarrBootstrapper.cs @@ -2,22 +2,22 @@ using Nancy.Bootstrapper; using Nancy.Diagnostics; using NLog; -using NzbDrone.Api.Extensions.Pipelines; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; +using Sonarr.Http.Extensions.Pipelines; using TinyIoC; -namespace NzbDrone.Api +namespace Sonarr.Http { - public class NancyBootstrapper : TinyIoCNancyBootstrapper + public class SonarrBootstrapper : TinyIoCNancyBootstrapper { private readonly TinyIoCContainer _tinyIoCContainer; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(NancyBootstrapper)); + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(SonarrBootstrapper)); - public NancyBootstrapper(TinyIoCContainer tinyIoCContainer) + public SonarrBootstrapper(TinyIoCContainer tinyIoCContainer) { _tinyIoCContainer = tinyIoCContainer; } diff --git a/src/NzbDrone.Api/NzbDroneRestModule.cs b/src/Sonarr.Http/SonarrRestModule.cs similarity index 55% rename from src/NzbDrone.Api/NzbDroneRestModule.cs rename to src/Sonarr.Http/SonarrRestModule.cs index 4cc103d95..3924c6f2a 100644 --- a/src/NzbDrone.Api/NzbDroneRestModule.cs +++ b/src/Sonarr.Http/SonarrRestModule.cs @@ -1,21 +1,37 @@ using System; -using NzbDrone.Api.REST; -using NzbDrone.Api.Validation; using NzbDrone.Core.Datastore; +using Sonarr.Http.REST; +using Sonarr.Http.Validation; -namespace NzbDrone.Api +namespace Sonarr.Http { - public abstract class NzbDroneRestModule : RestModule where TResource : RestResource, new() + public abstract class SonarrRestModule : RestModule where TResource : RestResource, new() { protected string Resource { get; private set; } - protected NzbDroneRestModule() - : this(new TResource().ResourceName) + + private static string BaseUrl() + { + var isV3 = typeof(TResource).Namespace.Contains(".V3."); + if (isV3) + { + return "/api/v3/"; + } + return "/api/"; + } + + private static string ResourceName() + { + return new TResource().ResourceName.Trim('/').ToLower(); + } + + protected SonarrRestModule() + : this(ResourceName()) { } - protected NzbDroneRestModule(string resource) - : base("/api/" + resource.Trim('/')) + protected SonarrRestModule(string resource) + : base(BaseUrl() + resource.Trim('/').ToLower()) { Resource = resource; PostValidator.RuleFor(r => r.Id).IsZero(); diff --git a/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs b/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs new file mode 100644 index 000000000..11fc74f2c --- /dev/null +++ b/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs @@ -0,0 +1,79 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Sonarr.Http.REST; + +namespace Sonarr.Http +{ + public abstract class SonarrRestModuleWithSignalR : SonarrRestModule, IHandle> + where TResource : RestResource, new() + where TModel : ModelBase, new() + { + private readonly IBroadcastSignalRMessage _signalRBroadcaster; + + protected SonarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) + { + _signalRBroadcaster = signalRBroadcaster; + } + + protected SonarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) + : base(resource) + { + _signalRBroadcaster = signalRBroadcaster; + } + + public void Handle(ModelEvent message) + { + if (message.Action == ModelAction.Deleted || message.Action == ModelAction.Sync) + { + BroadcastResourceChange(message.Action); + } + + BroadcastResourceChange(message.Action, message.Model.Id); + } + + protected void BroadcastResourceChange(ModelAction action, int id) + { + if (action == ModelAction.Deleted) + { + BroadcastResourceChange(action, new TResource {Id = id}); + } + else + { + var resource = GetResourceById(id); + BroadcastResourceChange(action, resource); + } + } + + protected void BroadcastResourceChange(ModelAction action, TResource resource) + { + if (GetType().Namespace.Contains("V3")) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage(resource, action), + Action = action + }; + + _signalRBroadcaster.BroadcastMessage(signalRMessage); + } + } + + protected void BroadcastResourceChange(ModelAction action) + { + if (GetType().Namespace.Contains("V3")) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage(action), + Action = action + }; + + _signalRBroadcaster.BroadcastMessage(signalRMessage); + } + } + } +} diff --git a/src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs b/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs similarity index 99% rename from src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs rename to src/Sonarr.Http/TinyIoCNancyBootstrapper.cs index d938b0c6e..b0a727209 100644 --- a/src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs +++ b/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs @@ -7,7 +7,7 @@ using Nancy; using Nancy.Diagnostics; using Nancy.Bootstrapper; -namespace NzbDrone.Api +namespace Sonarr.Http { diff --git a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs b/src/Sonarr.Http/Validation/EmptyCollectionValidator.cs similarity index 94% rename from src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs rename to src/Sonarr.Http/Validation/EmptyCollectionValidator.cs index 432eb1ed9..b91604e79 100644 --- a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs +++ b/src/Sonarr.Http/Validation/EmptyCollectionValidator.cs @@ -2,7 +2,7 @@ using FluentValidation.Validators; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Validation +namespace Sonarr.Http.Validation { public class EmptyCollectionValidator : PropertyValidator { diff --git a/src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs b/src/Sonarr.Http/Validation/RssSyncIntervalValidator.cs similarity index 95% rename from src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs rename to src/Sonarr.Http/Validation/RssSyncIntervalValidator.cs index 8a3f2d54c..4e56e2923 100644 --- a/src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs +++ b/src/Sonarr.Http/Validation/RssSyncIntervalValidator.cs @@ -1,6 +1,6 @@ using FluentValidation.Validators; -namespace NzbDrone.Api.Validation +namespace Sonarr.Http.Validation { public class RssSyncIntervalValidator : PropertyValidator { diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/Sonarr.Http/Validation/RuleBuilderExtensions.cs similarity index 97% rename from src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs rename to src/Sonarr.Http/Validation/RuleBuilderExtensions.cs index 01a3a4f75..2a14198b9 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/Sonarr.Http/Validation/RuleBuilderExtensions.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; -namespace NzbDrone.Api.Validation +namespace Sonarr.Http.Validation { public static class RuleBuilderExtensions { diff --git a/src/Sonarr.Http/app.config b/src/Sonarr.Http/app.config new file mode 100644 index 000000000..8460dd432 --- /dev/null +++ b/src/Sonarr.Http/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Sonarr.Http/packages.config b/src/Sonarr.Http/packages.config new file mode 100644 index 000000000..1d49009e6 --- /dev/null +++ b/src/Sonarr.Http/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Sonarr.sln b/src/Sonarr.sln index 25ca4856a..9aa32b9c4 100644 --- a/src/Sonarr.sln +++ b/src/Sonarr.sln @@ -1,8 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +VisualStudioVersion = 15.0.26730.10 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Console", "NzbDrone.Console\NzbDrone.Console.csproj", "{3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}" + ProjectSection(ProjectDependencies) = postProject + {7140FF1F-79BE-492F-9188-B21A050BF708} = {7140FF1F-79BE-492F-9188-B21A050BF708} + {15AD7579-A314-4626-B556-663F51D97CD1} = {15AD7579-A314-4626-B556-663F51D97CD1} + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} = {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205} = {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205} + {911284D3-F130-459E-836C-2430B6FBF21D} = {911284D3-F130-459E-836C-2430B6FBF21D} + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} = {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2} = {FD286DF8-2D3A-4394-8AD5-443FADE55FB2} + EndProjectSection +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test.Common", "Test.Common", "{47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97}" @@ -51,8 +62,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Api", "NzbDrone.Ap EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Host", "Host", "{486ADF86-DD89-4E19-B805-9D94F19800D9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Console", "NzbDrone.Console\NzbDrone.Console.csproj", "{3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host", "NzbDrone.Host\NzbDrone.Host.csproj", "{95C11A9E-56ED-456A-8447-2C89C1139266}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone", "NzbDrone\NzbDrone.csproj", "{D12F7F2F-8A3C-415F-88FA-6DD061A84869}" @@ -327,6 +336,7 @@ Global {74420A79-CC16-442C-8B1E-7C1B913844F0} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2955716E-0882-41EC-935D-C95694C5C30F} EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35 EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution