diff --git a/Logo/64.png b/Logo/64.png index 8ef72896e..33387d7f9 100644 Binary files a/Logo/64.png and b/Logo/64.png differ diff --git a/Logo/twitter.banner.fw.png b/Logo/twitter.banner.fw.png deleted file mode 100644 index 83c839d77..000000000 Binary files a/Logo/twitter.banner.fw.png and /dev/null differ diff --git a/Logo/twitter.fw.png b/Logo/twitter.fw.png deleted file mode 100644 index c722627ce..000000000 Binary files a/Logo/twitter.fw.png and /dev/null differ diff --git a/Logo/twitter.wall.fw.png b/Logo/twitter.wall.fw.png deleted file mode 100644 index 2c26ea0c3..000000000 Binary files a/Logo/twitter.wall.fw.png and /dev/null differ diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs b/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs new file mode 100644 index 000000000..31e56685e --- /dev/null +++ b/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs @@ -0,0 +1,21 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Api.Config +{ + public class DownloadClientConfigModule : NzbDroneConfigModule + { + public DownloadClientConfigModule(IConfigService configService, RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator) + : base(configService) + { + SharedValidator.RuleFor(c => c.DownloadedEpisodesFolder) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(pathExistsValidator) + .When(c => !String.IsNullOrWhiteSpace(c.DownloadedEpisodesFolder)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs new file mode 100644 index 000000000..f3fa14e8b --- /dev/null +++ b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs @@ -0,0 +1,15 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Config +{ + public class DownloadClientConfigResource : RestResource + { + public String DownloadedEpisodesFolder { get; set; } + public String DownloadClientWorkingFolders { get; set; } + + public Boolean AutoRedownloadFailed { get; set; } + public Boolean RemoveFailedDownloads { get; set; } + public Boolean EnableFailedDownloadHandling { get; set; } + } +} diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs new file mode 100644 index 000000000..6d818db6a --- /dev/null +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Reflection; +using FluentValidation; +using NzbDrone.Core.Configuration; +using Omu.ValueInjecter; + +namespace NzbDrone.Api.Config +{ + public class HostConfigModule : NzbDroneRestModule + { + private readonly IConfigFileProvider _configFileProvider; + + public HostConfigModule(ConfigFileProvider configFileProvider) + : base("/config/host") + { + _configFileProvider = configFileProvider; + + GetResourceSingle = GetHostConfig; + GetResourceById = GetHostConfig; + UpdateResource = SaveHostConfig; + + SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); + SharedValidator.RuleFor(c => c.Port).InclusiveBetween(1, 65535); + + SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled); + SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled); + + SharedValidator.RuleFor(c => c.SslPort).InclusiveBetween(1, 65535).When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl); + } + + private HostConfigResource GetHostConfig() + { + var resource = new HostConfigResource(); + resource.InjectFrom(_configFileProvider); + resource.Id = 1; + + 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); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/HostConfigResource.cs b/src/NzbDrone.Api/Config/HostConfigResource.cs new file mode 100644 index 000000000..8fc4151d9 --- /dev/null +++ b/src/NzbDrone.Api/Config/HostConfigResource.cs @@ -0,0 +1,22 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Config +{ + public class HostConfigResource : RestResource + { + public Int32 Port { get; set; } + public Int32 SslPort { get; set; } + public Boolean EnableSsl { get; set; } + public Boolean LaunchBrowser { get; set; } + public Boolean AuthenticationEnabled { 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 Boolean Torrent { get; set; } + public String SslCertHash { get; set; } + public String UrlBase { get; set; } + } +} diff --git a/src/NzbDrone.Api/Config/IndexerConfigModule.cs b/src/NzbDrone.Api/Config/IndexerConfigModule.cs new file mode 100644 index 000000000..10df7f2ae --- /dev/null +++ b/src/NzbDrone.Api/Config/IndexerConfigModule.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Config +{ + public class IndexerConfigModule : NzbDroneConfigModule + { + + public IndexerConfigModule(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(c => c.RssSyncInterval).InclusiveBetween(10, 120); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/IndexerConfigResource.cs b/src/NzbDrone.Api/Config/IndexerConfigResource.cs new file mode 100644 index 000000000..aeb9706e3 --- /dev/null +++ b/src/NzbDrone.Api/Config/IndexerConfigResource.cs @@ -0,0 +1,12 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Config +{ + public class IndexerConfigResource : RestResource + { + public Int32 Retention { get; set; } + public Int32 RssSyncInterval { get; set; } + public String ReleaseRestrictions { get; set; } + } +} diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs b/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs new file mode 100644 index 000000000..2ed63fc2d --- /dev/null +++ b/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs @@ -0,0 +1,18 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Api.Config +{ + public class MediaManagementConfigModule : NzbDroneConfigModule + { + 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)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs new file mode 100644 index 000000000..9ff4efc66 --- /dev/null +++ b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs @@ -0,0 +1,19 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Config +{ + public class MediaManagementConfigResource : RestResource + { + public Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } + public String RecycleBin { get; set; } + public Boolean AutoDownloadPropers { get; set; } + public Boolean CreateEmptySeriesFolders { get; set; } + + public Boolean SetPermissionsLinux { get; set; } + public String FileChmod { get; set; } + public String FolderChmod { get; set; } + public String ChownUser { get; set; } + public String ChownGroup { get; set; } + } +} diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs similarity index 97% rename from src/NzbDrone.Api/Config/NamingModule.cs rename to src/NzbDrone.Api/Config/NamingConfigModule.cs index f340f5ab9..eb41ef9e9 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -12,14 +12,14 @@ using Omu.ValueInjecter; namespace NzbDrone.Api.Config { - public class NamingModule : NzbDroneRestModule + public class NamingConfigModule : NzbDroneRestModule { private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; private readonly IFilenameValidationService _filenameValidationService; private readonly IBuildFileNames _filenameBuilder; - public NamingModule(INamingConfigService namingConfigService, + public NamingConfigModule(INamingConfigService namingConfigService, IFilenameSampleService filenameSampleService, IFilenameValidationService filenameValidationService, IBuildFileNames filenameBuilder) diff --git a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs new file mode 100644 index 000000000..64b31014d --- /dev/null +++ b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs @@ -0,0 +1,51 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Api.REST; +using NzbDrone.Core.Configuration; +using Omu.ValueInjecter; + +namespace NzbDrone.Api.Config +{ + public abstract class NzbDroneConfigModule : NzbDroneRestModule where TResource : RestResource, new() + { + private readonly IConfigService _configService; + + protected NzbDroneConfigModule(IConfigService configService) + : this(new TResource().ResourceName.Replace("config", ""), configService) + { + } + + protected NzbDroneConfigModule(string resource, IConfigService configService) : + base("config/" + resource.Trim('/')) + { + _configService = configService; + + GetResourceSingle = GetConfig; + GetResourceById = GetConfig; + UpdateResource = SaveConfig; + } + + private TResource GetConfig() + { + var resource = new TResource(); + resource.InjectFrom(_configService); + resource.Id = 1; + + return resource; + } + + 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/NzbDrone.Api/Config/SettingsModule.cs b/src/NzbDrone.Api/Config/SettingsModule.cs deleted file mode 100644 index d4135393f..000000000 --- a/src/NzbDrone.Api/Config/SettingsModule.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Config -{ - public class SettingsModule : NzbDroneApiModule - { - private readonly IConfigService _configService; - private readonly IConfigFileProvider _configFileProvider; - - public SettingsModule(IConfigService configService, IConfigFileProvider configFileProvider) - : base("/settings") - { - _configService = configService; - _configFileProvider = configFileProvider; - Get["/"] = x => GetGeneralSettings(); - Post["/"] = x => SaveGeneralSettings(); - - Get["/host"] = x => GetHostSettings(); - Post["/host"] = x => SaveHostSettings(); - - Get["/log"] = x => GetLogSettings(); - Post["/log"] = x => SaveLogSettings(); - } - - private Response SaveLogSettings() - { - throw new NotImplementedException(); - } - - private Response GetLogSettings() - { - throw new NotImplementedException(); - } - - private Response SaveHostSettings() - { - var request = Request.Body.FromJson>(); - _configFileProvider.SaveConfigDictionary(request); - - return GetHostSettings(); - } - - private Response GetHostSettings() - { - return _configFileProvider.GetConfigDictionary().AsResponse(); - } - - private Response GetGeneralSettings() - { - var collection = Request.Query.Collection; - - if (collection.HasValue && Boolean.Parse(collection.Value)) - return _configService.All().AsResponse(); - - return _configService.AllWithDefaults().AsResponse(); - } - - private Response SaveGeneralSettings() - { - var request = Request.Body.FromJson>(); - _configService.SaveValues(request); - - - return request.AsResponse(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientModule.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientModule.cs new file mode 100644 index 000000000..cdef47a78 --- /dev/null +++ b/src/NzbDrone.Api/DownloadClient/DownloadClientModule.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Download; + +namespace NzbDrone.Api.DownloadClient +{ + public class DownloadClientModule : ProviderModuleBase + { + public DownloadClientModule(IDownloadClientFactory downloadClientFactory) + : base(downloadClientFactory, "downloadclient") + { + } + + protected override void Validate(DownloadClientDefinition definition) + { + if (!definition.Enable) return; + base.Validate(definition); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs new file mode 100644 index 000000000..cb1054168 --- /dev/null +++ b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs @@ -0,0 +1,10 @@ +using System; + +namespace NzbDrone.Api.DownloadClient +{ + public class DownloadClientResource : ProviderResource + { + public Boolean Enable { get; set; } + public Int32 Protocol { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs new file mode 100644 index 000000000..58c1a2149 --- /dev/null +++ b/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using NzbDrone.Api.ClientSchema; +using NzbDrone.Core.Download; +using Omu.ValueInjecter; + +namespace NzbDrone.Api.DownloadClient +{ + public class DownloadClientSchemaModule : NzbDroneRestModule + { + private readonly IDownloadClientFactory _notificationFactory; + + public DownloadClientSchemaModule(IDownloadClientFactory notificationFactory) + : base("downloadclient/schema") + { + _notificationFactory = notificationFactory; + GetResourceAll = GetSchema; + } + + private List GetSchema() + { + var notifications = _notificationFactory.Templates(); + + var result = new List(notifications.Count); + + foreach (var notification in notifications) + { + var notificationResource = new DownloadClientResource(); + notificationResource.InjectFrom(notification); + notificationResource.Fields = SchemaBuilder.ToSchema(notification.Settings); + + result.Add(notificationResource); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index bda4694cc..9ca8abe50 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -92,8 +92,19 @@ + + + + + + + + - + + + + @@ -122,6 +133,7 @@ + @@ -145,7 +157,7 @@ - + @@ -174,11 +186,9 @@ - - diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index 5d0298698..be1c43674 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -5,7 +5,7 @@ using FluentValidation.Results; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.RootFolders; using NzbDrone.Api.Mapping; -using NzbDrone.Api.Validation; +using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Api.RootFolders { @@ -13,7 +13,11 @@ namespace NzbDrone.Api.RootFolders { private readonly IRootFolderService _rootFolderService; - public RootFolderModule(IRootFolderService rootFolderService, ICommandExecutor commandExecutor) + public RootFolderModule(IRootFolderService rootFolderService, + ICommandExecutor commandExecutor, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + DroneFactoryValidator droneFactoryValidator) : base(commandExecutor) { _rootFolderService = rootFolderService; @@ -23,7 +27,12 @@ namespace NzbDrone.Api.RootFolders CreateResource = CreateRootFolder; DeleteResource = DeleteFolder; - SharedValidator.RuleFor(c => c.Path).IsValidPath(); + SharedValidator.RuleFor(c => c.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(pathExistsValidator) + .SetValidator(droneFactoryValidator); } private RootFolderResource GetRootFolder(int id) @@ -33,15 +42,7 @@ namespace NzbDrone.Api.RootFolders private int CreateRootFolder(RootFolderResource rootFolderResource) { - try - { - return GetNewId(_rootFolderService.Add, rootFolderResource); - } - catch (Exception ex) - { - throw new ValidationException(new [] { new ValidationFailure("Path", ex.Message) }); - } - + return GetNewId(_rootFolderService.Add, rootFolderResource); } private List GetRootFolders() diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index d87a0f4ec..97255f50c 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Tv; using NzbDrone.Api.Validation; using NzbDrone.Api.Mapping; using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Api.Series { diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs index a13cbf52a..42f0d8db0 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; +using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Api.Validation { @@ -21,11 +22,6 @@ namespace NzbDrone.Api.Validation return ruleBuilder.SetValidator(new RegularExpressionValidator("^http(s)?://", RegexOptions.IgnoreCase)).WithMessage("must start with http:// or https://"); } - public static IRuleBuilderOptions IsValidPath(this IRuleBuilder ruleBuilder) - { - return ruleBuilder.SetValidator(new PathValidator()); - } - public static IRuleBuilderOptions NotBlank(this IRuleBuilder ruleBuilder) { return ruleBuilder.SetValidator(new NotNullValidator()).SetValidator(new NotEmptyValidator("")); diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index 309fee9cb..c08770fc8 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -71,7 +71,6 @@ namespace NzbDrone.Common return false; } - public static bool ContainsInvalidPathChars(this string text) { return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0; diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index e36c86691..d1c4774e5 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -179,9 +179,7 @@ namespace NzbDrone.Common.Processes public ProcessOutput StartAndCapture(string path, string args = null) { var output = new ProcessOutput(); - var process = Start(path, args, s => output.Standard.Add(s), error => output.Error.Add(error)); - - WaitForExit(process); + Start(path, args, s => output.Standard.Add(s), error => output.Error.Add(error)).WaitForExit(); return output; } @@ -190,10 +188,7 @@ namespace NzbDrone.Common.Processes { Logger.Trace("Waiting for process {0} to exit.", process.ProcessName); - if (!process.HasExited) - { - process.WaitForExit(); - } + process.WaitForExit(); } public void SetPriority(int processId, ProcessPriorityClass priority) diff --git a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs index aaf562c7f..35caa1216 100644 --- a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs @@ -105,16 +105,6 @@ namespace NzbDrone.Core.Test.Configuration Subject.GetValue(key, value2).Should().Be(value2); } - [Test] - public void updating_a_vakye_should_update_its_value() - { - Subject.SabHost = "Test"; - Subject.SabHost.Should().Be("Test"); - - Subject.SabHost = "Test2"; - Subject.SabHost.Should().Be("Test2"); - } - [Test] [Description("This test will use reflection to ensure each config property read/writes to a unique key")] public void config_properties_should_write_and_read_using_same_key() diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs index b586622ea..117c81ec0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs @@ -15,340 +15,166 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public class AcceptableSizeSpecificationFixture : CoreTest { + private RemoteEpisode parseResultMultiSet; private RemoteEpisode parseResultMulti; private RemoteEpisode parseResultSingle; - private Series series30minutes; - private Series series60minutes; + private Series series; private QualityDefinition qualityType; [SetUp] public void Setup() { + series = Builder.CreateNew() + .Build(); + + parseResultMultiSet = new RemoteEpisode + { + Series = series, + Release = new ReleaseInfo(), + ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, true) }, + Episodes = new List { new Episode(), new Episode(), new Episode(), new Episode(), new Episode(), new Episode() } + }; + parseResultMulti = new RemoteEpisode - { - Release = new ReleaseInfo(), - ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, true) }, - Episodes = new List { new Episode(), new Episode() } - }; + { + Series = series, + Release = new ReleaseInfo(), + ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, true) }, + Episodes = new List { new Episode(), new Episode() } + }; parseResultSingle = new RemoteEpisode { + Series = series, Release = new ReleaseInfo(), ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, true) }, Episodes = new List { new Episode() } }; - series30minutes = Builder.CreateNew() - .With(c => c.Runtime = 30) - .Build(); - - series60minutes = Builder.CreateNew() - .With(c => c.Runtime = 60) - .Build(); - qualityType = Builder.CreateNew() - .With(q => q.MinSize = 0) + .With(q => q.MinSize = 2) .With(q => q.MaxSize = 10) .With(q => q.Quality = Quality.SDTV) .Build(); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); } - [Test] - public void IsAcceptableSize_true_single_episode_not_first_or_last_30_minute() + private void GivenLastEpisode() { - parseResultSingle.Series = series30minutes; - parseResultSingle.Release.Size = 184572800; - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); - } - - [Test] - public void IsAcceptableSize_true_single_episode_not_first_or_last_60_minute() - { - parseResultSingle.Series = series60minutes; - parseResultSingle.Release.Size = 368572800; - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); - } - - [Test] - public void IsAcceptableSize_false_single_episode_not_first_or_last_30_minute() - { - parseResultSingle.Series = series30minutes; - parseResultSingle.Release.Size = 1.Gigabytes(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeFalse(); - } - - [Test] - public void IsAcceptableSize_false_single_episode_not_first_or_last_60_minute() - { - parseResultSingle.Series = series60minutes; - parseResultSingle.Release.Size = 1.Gigabytes(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - result.Should().BeFalse(); - } - - [Test] - public void IsAcceptableSize_true_multi_episode_not_first_or_last_30_minute() - { - parseResultMulti.Series = series30minutes; - parseResultMulti.Release.Size = 184572800; - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultMulti, null); - - - result.Should().BeTrue(); - } - - [Test] - public void IsAcceptableSize_true_multi_episode_not_first_or_last_60_minute() - { - parseResultMulti.Series = series60minutes; - parseResultMulti.Release.Size = 368572800; - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultMulti, null); - - - result.Should().BeTrue(); - } - - [Test] - public void IsAcceptableSize_false_multi_episode_not_first_or_last_30_minute() - { - parseResultMulti.Series = series30minutes; - parseResultMulti.Release.Size = 1.Gigabytes(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultMulti, null); - - - result.Should().BeFalse(); - } - - [Test] - public void IsAcceptableSize_false_multi_episode_not_first_or_last_60_minute() - { - parseResultMulti.Series = series60minutes; - parseResultMulti.Release.Size = 10.Gigabytes(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - - - bool result = Subject.IsSatisfiedBy(parseResultMulti, null); - - - result.Should().BeFalse(); - } - - [Test] - public void IsAcceptableSize_true_single_episode_first_30_minute() - { - parseResultSingle.Series = series30minutes; - parseResultSingle.Release.Size = 184572800; - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) .Returns(true); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); } - [Test] - public void IsAcceptableSize_true_single_episode_first_60_minute() - { - parseResultSingle.Series = series60minutes; - parseResultSingle.Release.Size = 368572800; + [TestCase(30, 50, false)] + [TestCase(30, 250, true)] + [TestCase(30, 500, false)] + [TestCase(60, 100, false)] + [TestCase(60, 500, true)] + [TestCase(60, 1000, false)] + public void single_episode(int runtime, int sizeInMegaBytes, bool expectedResult) + { + series.Runtime = runtime; + parseResultSingle.Series = series; + parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes(); - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); + Subject.IsSatisfiedBy(parseResultSingle, null).Should().Be(expectedResult); + } + + [TestCase(30, 500, true)] + [TestCase(30, 1000, false)] + [TestCase(60, 1000, true)] + [TestCase(60, 2000, false)] + public void single_episode_first_or_last(int runtime, int sizeInMegaBytes, bool expectedResult) + { + GivenLastEpisode(); + + series.Runtime = runtime; + parseResultSingle.Series = series; + parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes(); + + Subject.IsSatisfiedBy(parseResultSingle, null).Should().Be(expectedResult); + } + + [TestCase(30, 50 * 2, false)] + [TestCase(30, 250 * 2, true)] + [TestCase(30, 500 * 2, false)] + [TestCase(60, 100 * 2, false)] + [TestCase(60, 500 * 2, true)] + [TestCase(60, 1000 * 2, false)] + public void multi_episode(int runtime, int sizeInMegaBytes, bool expectedResult) + { + series.Runtime = runtime; + parseResultMulti.Series = series; + parseResultMulti.Release.Size = sizeInMegaBytes.Megabytes(); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); + .Returns(false); - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); + Subject.IsSatisfiedBy(parseResultMulti, null).Should().Be(expectedResult); } - [Test] - public void IsAcceptableSize_false_single_episode_first_30_minute() + [TestCase(30, 50 * 6, false)] + [TestCase(30, 250 * 6, true)] + [TestCase(30, 500 * 6, false)] + [TestCase(60, 100 * 6, false)] + [TestCase(60, 500 * 6, true)] + [TestCase(60, 1000 * 6, false)] + public void multiset_episode(int runtime, int sizeInMegaBytes, bool expectedResult) { - parseResultSingle.Series = series30minutes; - parseResultSingle.Release.Size = 1.Gigabytes(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); + series.Runtime = runtime; + parseResultMultiSet.Series = series; + parseResultMultiSet.Release.Size = sizeInMegaBytes.Megabytes(); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); + .Returns(false); - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeFalse(); + Subject.IsSatisfiedBy(parseResultMultiSet, null).Should().Be(expectedResult); } [Test] - public void IsAcceptableSize_false_single_episode_first_60_minute() + public void should_return_true_if_unlimited_30_minute() { + GivenLastEpisode(); - - parseResultSingle.Series = series60minutes; - parseResultSingle.Release.Size = 10.Gigabytes(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeFalse(); - } - - [Test] - public void IsAcceptableSize_true_unlimited_30_minute() - { - - - parseResultSingle.Series = series30minutes; + series.Runtime = 30; + parseResultSingle.Series = series; parseResultSingle.Release.Size = 18457280000; qualityType.MaxSize = 0; - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); + Subject.IsSatisfiedBy(parseResultSingle, null).Should().BeTrue(); } - + [Test] - public void IsAcceptableSize_true_unlimited_60_minute() + public void should_return_true_if_unlimited_60_minute() { + GivenLastEpisode(); - - parseResultSingle.Series = series60minutes; + series.Runtime = 60; + parseResultSingle.Series = series; parseResultSingle.Release.Size = 36857280000; qualityType.MaxSize = 0; - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); + Subject.IsSatisfiedBy(parseResultSingle, null).Should().BeTrue();; } [Test] - public void IsAcceptableSize_should_treat_daily_series_as_single_episode() + public void should_treat_daily_series_as_single_episode() { + GivenLastEpisode(); - parseResultSingle.Series = series60minutes; + series.Runtime = 60; + parseResultSingle.Series = series; parseResultSingle.Series.SeriesType = SeriesTypes.Daily; - parseResultSingle.Release.Size = 300.Megabytes(); - qualityType.MaxSize = (int)600.Megabytes(); + qualityType.MaxSize = 10; - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); - - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); - - - bool result = Subject.IsSatisfiedBy(parseResultSingle, null); - - - result.Should().BeTrue(); + Subject.IsSatisfiedBy(parseResultSingle, null).Should().BeTrue(); } [Test] @@ -362,9 +188,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(parseResult, null).Should().BeTrue(); } - [Test] - public void should_always_return_false_if_unknow() + public void should_always_return_false_if_unknown() { var parseResult = new RemoteEpisode { @@ -373,7 +198,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(parseResult, null).Should().BeFalse(); - Mocker.GetMock().Verify(c => c.Get(It.IsAny()), Times.Never()); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index d250c4d98..23b662982 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenSpecifications(params Mock[] mocks) { - Mocker.SetConstant>(mocks.Select(c => c.Object)); + Mocker.SetConstant>(mocks.Select(c => c.Object)); } [Test] diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 54c8ff6e0..48710e6f9 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenSabnzbdDownloadClient() { Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); + .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); } private void GivenMostRecentForEpisode(HistoryEventType eventType) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index baa24d5b4..4a3be3627 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -56,9 +56,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.GetMock() .Setup(s => s.GetDownloadClient()) .Returns(_downloadClient.Object); - - _downloadClient.SetupGet(s => s.IsConfigured) - .Returns(true); } private void GivenEmptyQueue() diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs index 45db8a529..2305954bc 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs @@ -20,65 +20,62 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { parseResult = new RemoteEpisode { - Release = new ReleaseInfo - { - PublishDate = DateTime.Now.AddDays(-100) - } + Release = new ReleaseInfo() }; } - private void WithUnlimitedRetention() + private void WithRetention(int days) { - Mocker.GetMock().SetupGet(c => c.Retention).Returns(0); + Mocker.GetMock().SetupGet(c => c.Retention).Returns(days); } - private void WithLongRetention() + private void WithAge(int days) { - Mocker.GetMock().SetupGet(c => c.Retention).Returns(1000); - } - - private void WithShortRetention() - { - Mocker.GetMock().SetupGet(c => c.Retention).Returns(10); - } - - private void WithEqualRetention() - { - Mocker.GetMock().SetupGet(c => c.Retention).Returns(100); + parseResult.Release.PublishDate = DateTime.Now.AddDays(-days); } [Test] - public void unlimited_retention_should_return_true() + public void should_return_true_when_retention_is_set_to_zero() { - WithUnlimitedRetention(); + WithRetention(0); + WithAge(100); + Subject.IsSatisfiedBy(parseResult, null).Should().BeTrue(); } [Test] - public void longer_retention_should_return_true() + public void should_return_true_when_release_if_younger_than_retention() { - WithLongRetention(); + WithRetention(1000); + WithAge(100); + Subject.IsSatisfiedBy(parseResult, null).Should().BeTrue(); } [Test] - public void equal_retention_should_return_true() + public void should_return_true_when_release_and_retention_are_the_same() { - WithEqualRetention(); + WithRetention(100); + WithAge(100); + Subject.IsSatisfiedBy(parseResult, null).Should().BeTrue(); } [Test] - public void shorter_retention_should_return_false() + public void should_return_false_when_old_than_retention() { - WithShortRetention(); + WithRetention(10); + WithAge(100); + Subject.IsSatisfiedBy(parseResult, null).Should().BeFalse(); } [Test] - public void zeroDay_report_should_return_true() + public void should_return_true_if_release_came_out_today_and_retention_is_zero() { - WithUnlimitedRetention(); + WithRetention(0); + WithAge(100); + Subject.IsSatisfiedBy(parseResult, null).Should().BeTrue(); } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs index 2b0639019..9f1c9d317 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs @@ -4,8 +4,9 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Download.Clients.Blackhole; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -13,7 +14,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests { [TestFixture] - public class BlackholeProviderFixture : CoreTest + public class BlackholeProviderFixture : CoreTest { private const string _nzbUrl = "http://www.nzbs.com/url"; private const string _title = "some_nzb_title"; @@ -27,13 +28,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _blackHoleFolder = @"c:\nzb\blackhole\".AsOsAgnostic(); _nzbPath = @"c:\nzb\blackhole\some_nzb_title.nzb".AsOsAgnostic(); - - Mocker.GetMock().SetupGet(c => c.BlackholeFolder).Returns(_blackHoleFolder); - _remoteEpisode = new RemoteEpisode(); _remoteEpisode.Release = new ReleaseInfo(); _remoteEpisode.Release.Title = _title; _remoteEpisode.Release.DownloadUrl = _nzbUrl; + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new FolderSettings + { + Folder = _blackHoleFolder + }; } private void WithExistingFile() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetProviderTests/DownloadNzbFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs similarity index 63% rename from src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetProviderTests/DownloadNzbFixture.cs rename to src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs index 397eb531a..b0c8d2efb 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetProviderTests/DownloadNzbFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs @@ -3,17 +3,15 @@ using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetProviderTests +namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests { - public class DownloadNzbFixture : CoreTest + public class DownloadNzbFixture : CoreTest { private const string _url = "http://www.nzbdrone.com"; private const string _title = "30.Rock.S01E01.Pilot.720p.hdtv"; @@ -32,6 +30,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetProviderTests .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) .Build() .ToList(); + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new NzbgetSettings + { + Host = "localhost", + Port = 6789, + Username = "nzbget", + Password = "pass", + TvCategory = "tv", + RecentTvPriority = (int)NzbgetPriority.High + }; } [Test] @@ -39,14 +48,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetProviderTests { var p = new object[] {"30.Rock.S01E01.Pilot.720p.hdtv.nzb", "TV", 50, false, "http://www.nzbdrone.com"}; - Mocker.GetMock() - .Setup(s => s.AddNzb(p)) + Mocker.GetMock() + .Setup(s => s.AddNzb(It.IsAny(), p)) .Returns(true); - Mocker.Resolve().DownloadNzb(_remoteEpisode); + Subject.DownloadNzb(_remoteEpisode); - Mocker.GetMock() - .Verify(v => v.AddNzb(It.IsAny()), Times.Once()); + Mocker.GetMock() + .Verify(v => v.AddNzb(It.IsAny(), It.IsAny()), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetProviderTests/QueueFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs similarity index 63% rename from src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetProviderTests/QueueFixture.cs rename to src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs index a152e9cdf..4fbbbad74 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetProviderTests/QueueFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs @@ -5,40 +5,52 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetProviderTests +namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests { - public class QueueFixture : CoreTest + public class QueueFixture : CoreTest { - private List _queue; + private List _queue; [SetUp] public void Setup() { - _queue = Builder.CreateListOfSize(5) + _queue = Builder.CreateListOfSize(5) .All() .With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb") .Build() .ToList(); + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new NzbgetSettings + { + Host = "localhost", + Port = 6789, + Username = "nzbget", + Password = "pass", + TvCategory = "tv", + RecentTvPriority = (int)NzbgetPriority.High + }; } private void WithFullQueue() { - Mocker.GetMock() - .Setup(s => s.GetQueue()) + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny())) .Returns(_queue); } private void WithEmptyQueue() { - Mocker.GetMock() - .Setup(s => s.GetQueue()) - .Returns(new List()); + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny())) + .Returns(new List()); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index dfb27a5f7..3d564a69f 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -6,7 +6,9 @@ using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Download.Clients.Pneumatic; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -14,7 +16,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests { [TestFixture] - public class PneumaticProviderFixture : CoreTest + public class PneumaticProviderFixture : CoreTest { private const string _nzbUrl = "http://www.nzbs.com/url"; private const string _title = "30.Rock.S01E05.hdtv.xvid-LoL"; @@ -31,7 +33,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _nzbPath = Path.Combine(_pneumaticFolder, _title + ".nzb").AsOsAgnostic(); _sabDrop = @"d:\unsorted tv\".AsOsAgnostic(); - Mocker.GetMock().SetupGet(c => c.PneumaticFolder).Returns(_pneumaticFolder); Mocker.GetMock().SetupGet(c => c.DownloadedEpisodesFolder).Returns(_sabDrop); _remoteEpisode = new RemoteEpisode(); @@ -41,6 +42,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo(); _remoteEpisode.ParsedEpisodeInfo.FullSeason = false; + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new FolderSettings + { + Folder = _pneumaticFolder + }; } private void WithExistingFile() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs deleted file mode 100644 index 9c4943ace..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Download.Clients.Sabnzbd; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabProviderTests -{ - [TestFixture] - - public class SabProviderFixture : CoreTest - { - private const string URL = "http://www.nzbclub.com/nzb_download.aspx?mid=1950232"; - private const string TITLE = "My Series Name - 5x2-5x3 - My title [Bluray720p] [Proper]"; - private RemoteEpisode _remoteEpisode; - - [SetUp] - public void Setup() - { - var fakeConfig = Mocker.GetMock(); - - fakeConfig.SetupGet(c => c.SabHost).Returns("192.168.5.55"); - fakeConfig.SetupGet(c => c.SabPort).Returns(2222); - fakeConfig.SetupGet(c => c.SabApiKey).Returns("5c770e3197e4fe763423ee7c392c25d1"); - fakeConfig.SetupGet(c => c.SabUsername).Returns("admin"); - fakeConfig.SetupGet(c => c.SabPassword).Returns("pass"); - fakeConfig.SetupGet(c => c.SabTvCategory).Returns("tv"); - - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = TITLE; - _remoteEpisode.Release.DownloadUrl = URL; - - _remoteEpisode.Episodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) - .Build() - .ToList(); - } - - [Test] - public void should_be_able_to_get_categories_when_config_is_passed_in() - { - - const string host = "192.168.5.22"; - const int port = 1111; - const string apikey = "5c770e3197e4fe763423ee7c392c25d2"; - const string username = "admin2"; - const string password = "pass2"; - - Mocker.GetMock(MockBehavior.Strict) - .Setup(s => s.DownloadString("http://192.168.5.22:1111/api?mode=get_cats&output=json&apikey=5c770e3197e4fe763423ee7c392c25d2&ma_username=admin2&ma_password=pass2")) - .Returns(ReadAllText("Files", "Categories_json.txt")); - - var result = Subject.GetCategories(host, port, apikey, username, password); - - - result.Should().NotBeNull(); - result.categories.Should().NotBeEmpty(); - } - - [Test] - public void should_be_able_to_get_categories_using_config() - { - Mocker.GetMock(MockBehavior.Strict) - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=get_cats&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files", "Categories_json.txt")); - - - var result = Subject.GetCategories(); - - - result.Should().NotBeNull(); - result.categories.Should().NotBeEmpty(); - } - - [Test] - public void GetHistory_should_return_a_list_with_items_when_the_history_has_items() - { - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=history&output=json&start=0&limit=0&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files", "History.txt")); - - - var result = Subject.GetHistory(); - - - result.Should().HaveCount(1); - } - - [Test] - public void GetHistory_should_return_an_empty_list_when_the_queue_is_empty() - { - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=history&output=json&start=0&limit=0&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files", "HistoryEmpty.txt")); - - - var result = Subject.GetHistory(); - - - result.Should().BeEmpty(); - } - - [Test] - public void GetHistory_should_return_an_empty_list_when_there_is_an_error_getting_the_queue() - { - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=history&output=json&start=0&limit=0&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files", "JsonError.txt")); - - - Assert.Throws(() => Subject.GetHistory(), "API Key Incorrect"); - } - - [Test] - public void GetVersion_should_return_the_version_using_passed_in_values() - { - var response = "{ \"version\": \"0.6.9\" }"; - - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=version&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(response); - - - var result = Subject.GetVersion("192.168.5.55", 2222, "5c770e3197e4fe763423ee7c392c25d1", "admin", "pass"); - - - result.Should().NotBeNull(); - result.Version.Should().Be("0.6.9"); - } - - [Test] - public void GetVersion_should_return_the_version_using_saved_values() - { - var response = "{ \"version\": \"0.6.9\" }"; - - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=version&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(response); - - - var result = Subject.GetVersion(); - - - result.Should().NotBeNull(); - result.Version.Should().Be("0.6.9"); - } - - [Test] - public void Test_should_return_version_as_a_string() - { - const string response = "{ \"version\": \"0.6.9\" }"; - - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=version&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(response); - - - var result = Subject.Test("192.168.5.55", 2222, "5c770e3197e4fe763423ee7c392c25d1", "admin", "pass"); - - - result.Should().Be("0.6.9"); - } - - [Test] - public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_true() - { - Mocker.GetMock() - .SetupGet(s => s.SabRecentTvPriority) - .Returns(SabPriorityType.High); - - Mocker.GetMock() - .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabPriorityType.High)) - .Returns(new SabAddResponse()); - - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock() - .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabPriorityType.High), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs new file mode 100644 index 000000000..fe9529ef1 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Sabnzbd; +using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests +{ + [TestFixture] + public class SabnzbdFixture : CoreTest + { + private const string URL = "http://www.nzbclub.com/nzb_download.aspx?mid=1950232"; + private const string TITLE = "My Series Name - 5x2-5x3 - My title [Bluray720p] [Proper]"; + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _remoteEpisode = new RemoteEpisode(); + _remoteEpisode.Release = new ReleaseInfo(); + _remoteEpisode.Release.Title = TITLE; + _remoteEpisode.Release.DownloadUrl = URL; + + _remoteEpisode.Episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) + .Build() + .ToList(); + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new SabnzbdSettings + { + Host = "192.168.5.55", + Port = 2222, + ApiKey = "5c770e3197e4fe763423ee7c392c25d1", + Username = "admin", + Password = "pass", + TvCategory = "tv", + RecentTvPriority = (int)SabnzbdPriority.High + }; + } + + [Test] + public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_true() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabnzbdPriority.High, It.IsAny())) + .Returns(new SabnzbdAddResponse()); + + Subject.DownloadNzb(_remoteEpisode); + + Mocker.GetMock() + .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabnzbdPriority.High, It.IsAny()), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index f1a64dd7b..0d3755468 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Net; using FizzWare.NBuilder; @@ -33,9 +34,6 @@ namespace NzbDrone.Core.Test.Download .With(c => c.Release = Builder.CreateNew().Build()) .With(c => c.Episodes = episodes) .Build(); - - - Mocker.GetMock().Setup(c => c.IsConfigured).Returns(true); } private void WithSuccessfulAdd() @@ -85,7 +83,8 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_attempt_download_if_client_isnt_configure() { - Mocker.GetMock().Setup(c => c.IsConfigured).Returns(false); + Mocker.GetMock() + .Setup(c => c.GetDownloadClient()).Returns((IDownloadClient)null); Subject.DownloadReport(_parseResult); diff --git a/src/NzbDrone.Core.Test/Files/Categories_json.txt b/src/NzbDrone.Core.Test/Files/Categories_json.txt deleted file mode 100644 index 5759a90e5..000000000 --- a/src/NzbDrone.Core.Test/Files/Categories_json.txt +++ /dev/null @@ -1,25 +0,0 @@ -{ - "categories":[ - "*", - "anime", - "apps", - "books", - "consoles", - "ds-games", - "emulation", - "games", - "misc", - "movies", - "music", - "pda", - "resources", - "test", - "tv", - "tv-dvd", - "unknown", - "wii-games", - "xbox-dlc", - "xbox-xbla", - "xxx" - ] -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Files/History.txt b/src/NzbDrone.Core.Test/Files/History.txt deleted file mode 100644 index 6d13ffe42..000000000 --- a/src/NzbDrone.Core.Test/Files/History.txt +++ /dev/null @@ -1,99 +0,0 @@ -{ - "history":{ - "active_lang":"en", - "paused":false, - "session":"5c770e3197e4fe763423ee7c392c25d1", - "restart_req":false, - "power_options":true, - "slots":[ - { - "action_line":"", - "show_details":"True", - "script_log":"", - "meta":null, - "fail_message":"", - "loaded":false, - "id":9858, - "size":"970 MB", - "category":"tv", - "pp":"D", - "retry":0, - "completeness":0, - "script":"None", - "nzb_name":"The.Mentalist.S04E12.720p.HDTV.x264-IMMERSE.nzb", - "download_time":524, - "storage":"C:\\ServerPool\\ServerFolders\\Unsorted TV\\The Mentalist - 4x12 - My Bloody Valentine [HDTV-720p]", - "status":"Completed", - "script_line":"", - "completed":1327033479, - "nzo_id":"SABnzbd_nzo_0crgis", - "downloaded":1016942445, - "report":"", - "path":"D:\\SABnzbd\\downloading\\The Mentalist - 4x12 - My Bloody Valentine [HDTV-720p]", - "postproc_time":24, - "name":"The Mentalist - 4x12 - My Bloody Valentine [HDTV-720p]", - "url":"", - "bytes":1016942445, - "url_info":"", - "stage_log":[ - { - "name":"Download", - "actions":[ - "Downloaded in 8 minutes 44 seconds at an average of 1.8 MB/s" - ] - }, - { - "name":"Repair", - "actions":[ - "[the.mentalist.s04e12.720p.hdtv.x264-immerse] Quick Check OK" - ] - }, - { - "name":"Unpack", - "actions":[ - "[the.mentalist.s04e12.720p.hdtv.x264-immerse] Unpacked 1 files/folders in 23 seconds" - ] - } - ] - } - ], - "speed":"0 ", - "helpuri":"http://wiki.sabnzbd.org/", - "size":"0 B", - "uptime":"1d", - "total_size":"10.2 T", - "month_size":"445.7 G", - "week_size":"46.6 G", - "version":"0.6.9", - "new_rel_url":"http://sourceforge.net/projects/sabnzbdplus/files/sabnzbdplus/sabnzbd-0.6.14", - "diskspacetotal2":"9314.57", - "color_scheme":"gold", - "diskspacetotal1":"871.41", - "nt":true, - "status":"Idle", - "last_warning":"2012-01-19 23:58:01,736\nWARNING:\nAPI Key incorrect, Use the api key from Config->General in your 3rd party program:", - "have_warnings":"3", - "cache_art":"0", - "sizeleft":"0 B", - "finishaction":null, - "paused_all":false, - "cache_size":"0 B", - "new_release":"0.6.14", - "pause_int":"0", - "mbleft":"0.00", - "diskspace1":"869.82", - "darwin":false, - "timeleft":"0:00:00", - "mb":"0.00", - "noofslots":9724, - "day_size":"0 ", - "eta":"unknown", - "nzb_quota":"", - "loadavg":"", - "cache_max":"-1", - "kbpersec":"0.00", - "speedlimit":"", - "webdir":"D:\\SABnzbd\\SABnzbd\\interfaces\\Plush\\templates", - "diskspace2":"1084.96" - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Files/HistoryEmpty.txt b/src/NzbDrone.Core.Test/Files/HistoryEmpty.txt deleted file mode 100644 index 1c5cb9d95..000000000 --- a/src/NzbDrone.Core.Test/Files/HistoryEmpty.txt +++ /dev/null @@ -1,50 +0,0 @@ -{ - "history":{ - "active_lang":"en", - "paused":false, - "session":"5c770e3197e4fe763423ee7c392c25d1", - "restart_req":false, - "power_options":true, - "slots":[ - - ], - "speed":"0 ", - "helpuri":"http://wiki.sabnzbd.org/", - "size":"0 B", - "uptime":"1d", - "total_size":"10.2 T", - "month_size":"445.7 G", - "week_size":"46.6 G", - "version":"0.6.9", - "new_rel_url":"http://sourceforge.net/projects/sabnzbdplus/files/sabnzbdplus/sabnzbd-0.6.14", - "diskspacetotal2":"9314.57", - "color_scheme":"gold", - "diskspacetotal1":"871.41", - "nt":true, - "status":"Idle", - "last_warning":"2012-01-19 23:58:01,736\nWARNING:\nAPI Key incorrect, Use the api key from Config->General in your 3rd party program:", - "have_warnings":"3", - "cache_art":"0", - "sizeleft":"0 B", - "finishaction":null, - "paused_all":false, - "cache_size":"0 B", - "new_release":"0.6.14", - "pause_int":"0", - "mbleft":"0.00", - "diskspace1":"869.82", - "darwin":false, - "timeleft":"0:00:00", - "mb":"0.00", - "noofslots":9724, - "day_size":"0 ", - "eta":"unknown", - "nzb_quota":"", - "loadavg":"", - "cache_max":"-1", - "kbpersec":"0.00", - "speedlimit":"", - "webdir":"D:\\SABnzbd\\SABnzbd\\interfaces\\Plush\\templates", - "diskspace2":"1084.96" - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Files/JsonError.txt b/src/NzbDrone.Core.Test/Files/JsonError.txt deleted file mode 100644 index aa32a0bdd..000000000 --- a/src/NzbDrone.Core.Test/Files/JsonError.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": false, - "error": "API Key Incorrect" -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index dcd8a12dc..0dea398ea 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -77,7 +77,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport }; Mocker.GetMock() - .Setup(c => c.GetEpisodes(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_localEpisode); @@ -150,7 +150,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_pass1); - Mocker.GetMock().Setup(c => c.GetEpisodes(It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock().Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(); _videoFiles = new List @@ -168,7 +168,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Subject.GetImportDecisions(_videoFiles, _series, false); Mocker.GetMock() - .Verify(c => c.GetEpisodes(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); ExceptionVerification.ExpectedErrors(3); } diff --git a/src/NzbDrone.Core.Test/MetadataSourceTests/TraktProxyFixture.cs b/src/NzbDrone.Core.Test/MetadataSourceTests/TraktProxyFixture.cs index 4577fe5c9..f2fe140d4 100644 --- a/src/NzbDrone.Core.Test/MetadataSourceTests/TraktProxyFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSourceTests/TraktProxyFixture.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.Test.MetadataSourceTests [TestCase("Franklin & Bash", "Franklin & Bash")] [TestCase("Mr. D", "Mr. D")] [TestCase("Rob & Big", "Rob and Big")] + [TestCase("M*A*S*H", "M*A*S*H")] public void successful_search(string title, string expected) { var result = Subject.SearchForNewSeries(title); @@ -38,7 +39,7 @@ namespace NzbDrone.Core.Test.MetadataSourceTests } [TestCase(75978)] - [TestCase(79349)] + [TestCase(83462)] public void should_be_able_to_get_series_detail(int tvdbId) { var details = Subject.GetSeriesInfo(tvdbId); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index bb3fa1367..fe5a842f8 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -122,10 +122,10 @@ - - + + - + @@ -180,6 +180,17 @@ + + + + + + + + + + + @@ -287,18 +298,9 @@ Always - - Always - Always - - Always - - - Always - App.config @@ -330,9 +332,6 @@ Always - - Always - Always diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs new file mode 100644 index 000000000..d86a4b563 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class AbsoluteEpisodeNumberParserFixture : CoreTest + { + [TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "High School DxD", 7, 0, 0)] + [TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Working!!", 6, 0, 0)] + [TestCase("[Commie]_Senki_Zesshou_Symphogear_-_11_[65F220B4]", "Senki_Zesshou_Symphogear", 11, 0, 0)] + [TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Rinne_no_Lagrange", 12, 0, 0)] + [TestCase("[Commie]_Rinne_no_Lagrange_-_15_[E76552EA]", "Rinne_no_Lagrange", 15, 0, 0)] + [TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "Hunter_X_Hunter", 33, 0, 0)] + [TestCase("[HorribleSubs]_Fairy_Tail_-_145_[720p]", "Fairy_Tail", 145, 0, 0)] + [TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "Tonari no Kaibutsu-kun", 13, 0, 0)] + [TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Yes.Pretty.Cure.5.Go.Go!", 31, 0, 0)] + [TestCase("[K-F] One Piece 214", "One Piece", 214, 0, 0)] + [TestCase("[K-F] One Piece S10E14 214", "One Piece", 214, 10, 14)] + [TestCase("[K-F] One Piece 10x14 214", "One Piece", 214, 10, 14)] + [TestCase("[K-F] One Piece 214 10x14", "One Piece", 214, 10, 14)] +// [TestCase("One Piece S10E14 214", "One Piece", 214, 10, 14)] +// [TestCase("One Piece 10x14 214", "One Piece", 214, 10, 14)] +// [TestCase("One Piece 214 10x14", "One Piece", 214, 10, 14)] +// [TestCase("214 One Piece 10x14", "One Piece", 214, 10, 14)] + [TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Bleach", 31, 0, 0)] + [TestCase("Bleach - 031 - The Resolution to Kill [Lunar]", "Bleach", 31, 0, 0)] + [TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "Hack Sign", 1, 0, 0)] + [TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)] + [TestCase("ducktales_e66_time_is_money_part_one_marking_time", "DuckTales", 66, 0, 0)] + public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.AbsoluteEpisodeNumbers.First().Should().Be(absoluteEpisodeNumber); + result.SeasonNumber.Should().Be(seasonNumber); + result.EpisodeNumbers.FirstOrDefault().Should().Be(episodeNumber); + result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); + result.FullSeason.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs new file mode 100644 index 000000000..83fd99d00 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Expansive; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class CrapParserFixture : CoreTest + { + [TestCase("76El6LcgLzqb426WoVFg1vVVVGx4uCYopQkfjmLe")] + [TestCase("Vrq6e1Aba3U amCjuEgV5R2QvdsLEGYF3YQAQkw8")] + [TestCase("TDAsqTea7k4o6iofVx3MQGuDK116FSjPobMuh8oB")] + [TestCase("yp4nFodAAzoeoRc467HRh1mzuT17qeekmuJ3zFnL")] + [TestCase("oxXo8S2272KE1 lfppvxo3iwEJBrBmhlQVK1gqGc")] + [TestCase("dPBAtu681Ycy3A4NpJDH6kNVQooLxqtnsW1Umfiv")] + [TestCase("password - \"bdc435cb-93c4-4902-97ea-ca00568c3887.337\" yEnc")] + [TestCase("185d86a343e39f3341e35c4dad3f9959")] + [TestCase("ba27283b17c00d01193eacc02a8ba98eeb523a76")] + [TestCase("45a55debe3856da318cc35882ad07e43cd32fd15")] + [TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")] + [TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")] + [TestCase("THIS SHOULD NEVER PARSE")] + public void should_not_parse_crap(string title) + { + Parser.Parser.ParseTitle(title).Should().BeNull(); + ExceptionVerification.IgnoreWarns(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs new file mode 100644 index 000000000..e889dc607 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Expansive; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class DailyEpisodeParserFixture : CoreTest + { + [TestCase("Conan 2011 04 18 Emma Roberts HDTV XviD BFF", "Conan", 2011, 04, 18)] + [TestCase("The Tonight Show With Jay Leno 2011 04 15 1080i HDTV DD5 1 MPEG2 TrollHD", "The Tonight Show With Jay Leno", 2011, 04, 15)] + [TestCase("The.Daily.Show.2010.10.11.Johnny.Knoxville.iTouch-MW", "The.Daily.Show", 2010, 10, 11)] + [TestCase("The Daily Show - 2011-04-12 - Gov. Deval Patrick", "The.Daily.Show", 2011, 04, 12)] + [TestCase("2011.01.10 - Denis Leary - HD TV.mkv", "", 2011, 1, 10)] + [TestCase("2011.03.13 - Denis Leary - HD TV.mkv", "", 2011, 3, 13)] + [TestCase("The Tonight Show with Jay Leno - 2011-06-16 - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo", "The Tonight Show with Jay Leno", 2011, 6, 16)] + [TestCase("2020.NZ.2012.16.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 16)] + [TestCase("2020.NZ.2012.13.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 13)] + [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020nz", 2011, 12, 2)] + public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) + { + var result = Parser.Parser.ParseTitle(postTitle); + var airDate = new DateTime(year, month, day); + result.Should().NotBeNull(); + result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); + result.AirDate.Should().Be(airDate.ToString(Episode.AIR_DATE_FORMAT)); + result.EpisodeNumbers.Should().BeEmpty(); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + } + + [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] + [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] + [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] + [TestCase("The Daily Show - {year}-{month}-{day} - Gov. Deval Patrick")] + [TestCase("{year}.{month}.{day} - Denis Leary - HD TV.mkv")] + [TestCase("The Tonight Show with Jay Leno - {year}-{month}-{day} - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo")] + [TestCase("2020.NZ.{year}.{month}.{day}.PDTV.XviD-C4TV")] + public void should_not_accept_ancient_daily_series(string title) + { + var yearTooLow = title.Expand(new { year = 1950, month = 10, day = 14 }); + Parser.Parser.ParseTitle(yearTooLow).Should().BeNull(); + } + + [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] + [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] + [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] + [TestCase("The Daily Show - {year}-{month}-{day} - Gov. Deval Patrick")] + [TestCase("{year}.{month}.{day} - Denis Leary - HD TV.mkv")] + [TestCase("The Tonight Show with Jay Leno - {year}-{month}-{day} - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo")] + [TestCase("2020.NZ.{year}.{month}.{day}.PDTV.XviD-C4TV")] + public void should_not_accept_future_dates(string title) + { + var twoDaysFromNow = DateTime.Now.AddDays(2); + + var validDate = title.Expand(new { year = twoDaysFromNow.Year, month = twoDaysFromNow.Month.ToString("00"), day = twoDaysFromNow.Day.ToString("00") }); + + Parser.Parser.ParseTitle(validDate).Should().BeNull(); + } + + [Test] + public void should_fail_if_episode_is_far_in_future() + { + var title = string.Format("{0:yyyy.MM.dd} - Denis Leary - HD TV.mkv", DateTime.Now.AddDays(2)); + + Parser.Parser.ParseTitle(title).Should().BeNull(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs b/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs new file mode 100644 index 000000000..5d0cc3829 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class IsPossibleSpecialEpisodeFixture + { + [Test] + public void should_not_treat_files_without_a_series_title_as_a_special() + { + var parsedEpisodeInfo = new ParsedEpisodeInfo + { + EpisodeNumbers = new[]{ 7 }, + SeasonNumber = 1, + SeriesTitle = "" + }; + + parsedEpisodeInfo.IsPossibleSpecialEpisode().Should().BeFalse(); + } + + [Test] + public void should_return_true_when_episode_numbers_is_empty() + { + var parsedEpisodeInfo = new ParsedEpisodeInfo + { + SeasonNumber = 1, + SeriesTitle = "" + }; + + parsedEpisodeInfo.IsPossibleSpecialEpisode().Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs new file mode 100644 index 000000000..1b200d5c1 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class LanguageParserFixture : CoreTest + { + [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", Language.English)] + [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL", Language.French)] + [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL", Language.Spanish)] + [TestCase("Castle.2009.S01E14.German.HDTV.XviD-LOL", Language.German)] + [TestCase("Castle.2009.S01E14.Germany.HDTV.XviD-LOL", Language.English)] + [TestCase("Castle.2009.S01E14.Italian.HDTV.XviD-LOL", Language.Italian)] + [TestCase("Castle.2009.S01E14.Danish.HDTV.XviD-LOL", Language.Danish)] + [TestCase("Castle.2009.S01E14.Dutch.HDTV.XviD-LOL", Language.Dutch)] + [TestCase("Castle.2009.S01E14.Japanese.HDTV.XviD-LOL", Language.Japanese)] + [TestCase("Castle.2009.S01E14.Cantonese.HDTV.XviD-LOL", Language.Cantonese)] + [TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL", Language.Mandarin)] + [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)] + [TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL", Language.Russian)] + [TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL", Language.Polish)] + [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)] + [TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL", Language.Swedish)] + [TestCase("Castle.2009.S01E14.Norwegian.HDTV.XviD-LOL", Language.Norwegian)] + [TestCase("Castle.2009.S01E14.Finnish.HDTV.XviD-LOL", Language.Finnish)] + [TestCase("Castle.2009.S01E14.Turkish.HDTV.XviD-LOL", Language.Turkish)] + [TestCase("Castle.2009.S01E14.Portuguese.HDTV.XviD-LOL", Language.Portuguese)] + [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL", Language.English)] + [TestCase("person.of.interest.1x19.ita.720p.bdmux.x264-novarip", Language.Italian)] + [TestCase("Salamander.S01E01.FLEMISH.HDTV.x264-BRiGAND", Language.Flemish)] + [TestCase("H.Polukatoikia.S03E13.Greek.PDTV.XviD-Ouzo", Language.Greek)] + [TestCase("Burn.Notice.S04E15.Brotherly.Love.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP", Language.German)] + [TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Norwegian)] + [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)] + [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)] + public void should_parse_language(string postTitle, Language language) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Should().Be(language); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs new file mode 100644 index 000000000..40644a880 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Expansive; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class MultiEpisodeParserFixture : CoreTest + { + [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD", "WEEDS", 3, new[] { 1, 2, 3, 4, 5, 6 })] + [TestCase("Two.and.a.Half.Men.103.104.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Men", 1, new[] { 3, 4 })] + [TestCase("Weeds.S03E01.S03E02.720p.HDTV.X264-DIMENSION", "Weeds", 3, new[] { 1, 2 })] + [TestCase("The Borgias S01e01 e02 ShoHD On Demand 1080i DD5 1 ALANiS", "The Borgias", 1, new[] { 1, 2 })] + [TestCase("White.Collar.2x04.2x05.720p.BluRay-FUTV", "White.Collar", 2, new[] { 4, 5 })] + [TestCase("Desperate.Housewives.S07E22E23.720p.HDTV.X264-DIMENSION", "Desperate.Housewives", 7, new[] { 22, 23 })] + [TestCase("Desparate Housewives - S07E22 - S07E23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 })] + [TestCase("S03E01.S03E02.720p.HDTV.X264-DIMENSION", "", 3, new[] { 1, 2 })] + [TestCase("Desparate Housewives - S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 })] + [TestCase("S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "", 7, new[] { 22, 23 })] + [TestCase("2x04x05.720p.BluRay-FUTV", "", 2, new[] { 4, 5 })] + [TestCase("S02E04E05.720p.BluRay-FUTV", "", 2, new[] { 4, 5 })] + [TestCase("S02E03-04-05.720p.BluRay-FUTV", "", 2, new[] { 3, 4, 5 })] + [TestCase("Breakout.Kings.S02E09-E10.HDTV.x264-ASAP", "Breakout Kings", 2, new[] { 9, 10 })] + [TestCase("Breakout Kings - 2x9-2x10 - Served Cold [SDTV] ", "Breakout Kings", 2, new[] { 9, 10 })] + [TestCase("Breakout Kings - 2x09-2x10 - Served Cold [SDTV] ", "Breakout Kings", 2, new[] { 9, 10 })] + [TestCase("Hell on Wheels S02E09 E10 HDTV x264 EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] + [TestCase("Hell.on.Wheels.S02E09-E10.720p.HDTV.x264-EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] + [TestCase("Grey's Anatomy - 8x01_02 - Free Falling", "Grey's Anatomy", 8, new [] { 1,2 })] + [TestCase("8x01_02 - Free Falling", "", 8, new[] { 1, 2 })] + [TestCase("Kaamelott.S01E91-E100", "Kaamelott", 1, new[] { 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 })] + [TestCase("Neighbours.S29E161-E165.PDTV.x264-FQM", "Neighbours", 29, new[] { 161, 162, 163, 164, 165 })] + [TestCase("Shortland.Street.S22E5363-E5366.HDTV.x264-FiHTV", "Shortland Street", 22, new[] { 5363, 5364, 5365, 5366 })] + public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.SeasonNumber.Should().Be(season); + result.EpisodeNumbers.Should().BeEquivalentTo(episodes); + result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/NormalizeTitleFixture.cs b/src/NzbDrone.Core.Test/ParserTests/NormalizeTitleFixture.cs new file mode 100644 index 000000000..19c42dff4 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/NormalizeTitleFixture.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Expansive; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class NormalizeTitleFixture : CoreTest + { + [TestCase("Conan", "conan")] + [TestCase("The Tonight Show With Jay Leno", "tonightshowwithjayleno")] + [TestCase("The.Daily.Show", "dailyshow")] + [TestCase("Castle (2009)", "castle2009")] + [TestCase("Parenthood.2010", "parenthood2010")] + [TestCase("Law_and_Order_SVU", "lawordersvu")] + public void should_normalize_series_title(string parsedSeriesName, string seriesName) + { + var result = parsedSeriesName.CleanSeriesTitle(); + result.Should().Be(seriesName); + } + + [TestCase("CaPitAl", "capital")] + [TestCase("peri.od", "period")] + [TestCase("this.^&%^**$%@#$!That", "thisthat")] + [TestCase("test/test", "testtest")] + [TestCase("90210", "90210")] + [TestCase("24", "24")] + public void should_remove_special_characters_and_casing(string dirty, string clean) + { + var result = dirty.CleanSeriesTitle(); + result.Should().Be(clean); + } + + [TestCase("the")] + [TestCase("and")] + [TestCase("or")] + [TestCase("a")] + [TestCase("an")] + [TestCase("of")] + public void should_remove_common_words(string word) + { + var dirtyFormat = new[] + { + "word.{0}.word", + "word {0} word", + "word-{0}-word", + "{0}.word.word", + "{0}-word-word", + "{0} word word", + "word.word.{0}", + "word-word-{0}", + "word-word {0}", + }; + + foreach (var s in dirtyFormat) + { + var dirty = String.Format(s, word); + dirty.CleanSeriesTitle().Should().Be("wordword"); + } + + } + + [TestCase("the")] + [TestCase("and")] + [TestCase("or")] + [TestCase("a")] + [TestCase("an")] + [TestCase("of")] + public void should_not_remove_common_words_in_the_middle_of_word(string word) + { + var dirtyFormat = new[] + { + "word.{0}word", + "word {0}word", + "word-{0}word", + "word{0}.word", + "word{0}-word", + "word{0}-word", + }; + + foreach (var s in dirtyFormat) + { + var dirty = String.Format(s, word); + dirty.CleanSeriesTitle().Should().Be(("word" + word.ToLower() + "word")); + } + + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index b0078900d..359699d13 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -2,11 +2,8 @@ using System; using System.Linq; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Common.Expansive; using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.ParserTests { @@ -26,179 +23,6 @@ namespace NzbDrone.Core.Test.ParserTests * Superman.-.The.Man.of.Steel.1994-05.33.hybrid.DreamGirl-Novus-HD */ - [TestCase("Sonny.With.a.Chance.S02E15", "Sonny.With.a.Chance", 2, 15)] - [TestCase("Two.and.a.Half.Me.103.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Me", 1, 3)] - [TestCase("Two.and.a.Half.Me.113.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Me", 1, 13)] - [TestCase("Two.and.a.Half.Me.1013.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Me", 10, 13)] - [TestCase("Chuck.4x05.HDTV.XviD-LOL", "Chuck", 4, 5)] - [TestCase("The.Girls.Next.Door.S03E06.DVDRip.XviD-WiDE", "The.Girls.Next.Door", 3, 6)] - [TestCase("Degrassi.S10E27.WS.DSR.XviD-2HD", "Degrassi", 10, 27)] - [TestCase("Parenthood.2010.S02E14.HDTV.XviD-LOL", "Parenthood 2010", 2, 14)] - [TestCase("Hawaii Five 0 S01E19 720p WEB DL DD5 1 H 264 NT", "Hawaii Five 0", 1, 19)] - [TestCase("The Event S01E14 A Message Back 720p WEB DL DD5 1 H264 SURFER", "The Event", 1, 14)] - [TestCase("Adam Hills In Gordon St Tonight S01E07 WS PDTV XviD FUtV", "Adam Hills In Gordon St Tonight", 1, 7)] - [TestCase("Adam Hills In Gordon St Tonight S01E07 WS PDTV XviD FUtV", "Adam Hills In Gordon St Tonight", 1, 7)] - [TestCase("Adventure.Inc.S03E19.DVDRip.XviD-OSiTV", "Adventure.Inc", 3, 19)] - [TestCase("S03E09 WS PDTV XviD FUtV", "", 3, 9)] - [TestCase("5x10 WS PDTV XviD FUtV", "", 5, 10)] - [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL", "Castle 2009", 1, 14)] - [TestCase("Pride.and.Prejudice.1995.S03E20.HDTV.XviD-LOL", "Pride and Prejudice 1995", 3, 20)] - [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "The.Office", 3, 115)] - [TestCase(@"Parks and Recreation - S02E21 - 94 Meetings - 720p TV.mkv", "Parks and Recreation", 2, 21)] - [TestCase(@"24-7 Penguins-Capitals- Road to the NHL Winter Classic - S01E03 - Episode 3.mkv", "24-7 Penguins-Capitals- Road to the NHL Winter Classic", 1, 3)] - [TestCase("Adventure.Inc.S03E19.DVDRip.\"XviD\"-OSiTV", "Adventure.Inc", 3, 19)] - [TestCase("Hawaii Five-0 (2010) - 1x05 - Nalowale (Forgotten/Missing)", "Hawaii Five-0 (2010)", 1, 5)] - [TestCase("Hawaii Five-0 (2010) - 1x05 - Title", "Hawaii Five-0 (2010)", 1, 5)] - [TestCase("House - S06E13 - 5 to 9 [DVD]", "House", 6, 13)] - [TestCase("The Mentalist - S02E21 - 18-5-4", "The Mentalist", 2, 21)] - [TestCase("Breaking.In.S01E07.21.0.Jump.Street.720p.WEB-DL.DD5.1.h.264-KiNGS", "Breaking In", 1, 7)] - [TestCase("CSI.525", "CSI", 5, 25)] - [TestCase("King of the Hill - 10x12 - 24 Hour Propane People [SDTV]", "King of the Hill", 10, 12)] - [TestCase("Brew Masters S01E06 3 Beers For Batali DVDRip XviD SPRiNTER", "Brew Masters", 1, 6)] - [TestCase("24 7 Flyers Rangers Road to the NHL Winter Classic Part01 720p HDTV x264 ORENJI", "24 7 Flyers Rangers Road to the NHL Winter Classic", 1, 1)] - [TestCase("24 7 Flyers Rangers Road to the NHL Winter Classic Part 02 720p HDTV x264 ORENJI", "24 7 Flyers Rangers Road to the NHL Winter Classic", 1, 2)] - [TestCase("24-7 Flyers-Rangers- Road to the NHL Winter Classic - S01E01 - Part 1", "24 7 Flyers Rangers Road to the NHL Winter Classic", 1, 1)] - [TestCase("The.Kennedys.Part.2.DSR.XviD-SYS", "The Kennedys", 1, 2)] - [TestCase("the-pacific-e07-720p", "The Pacific", 1, 7)] - [TestCase("S6E02-Unwrapped-(Playing With Food) - [DarkData]", "", 6, 2)] - [TestCase("S06E03-Unwrapped-(Number Ones Unwrapped) - [DarkData]", "", 6, 3)] - [TestCase("The Mentalist S02E21 18 5 4 720p WEB DL DD5 1 h 264 EbP", "The Mentalist", 2, 21)] - [TestCase("01x04 - Halloween, Part 1 - 720p WEB-DL", "", 1, 4)] - [TestCase("extras.s03.e05.ws.dvdrip.xvid-m00tv", "Extras", 3, 5)] - [TestCase("castle.2009.416.hdtv-lol", "Castle 2009", 4, 16)] - [TestCase("hawaii.five-0.2010.217.hdtv-lol", "Hawaii Five-0 (2010)", 2, 17)] - [TestCase("Looney Tunes - S1936E18 - I Love to Singa", "Looney Tunes", 1936, 18)] - [TestCase("American_Dad!_-_7x6_-_The_Scarlett_Getter_[SDTV]", "American Dad!", 7, 6)] - [TestCase("Falling_Skies_-_1x1_-_Live_and_Learn_[HDTV-720p]", "Falling Skies", 1, 1)] - [TestCase("Top Gear - 07x03 - 2005.11.70", "Top Gear", 7, 3)] - [TestCase("Hatfields and McCoys 2012 Part 1 REPACK 720p HDTV x264 2HD", "Hatfields and McCoys 2012", 1, 1)] - [TestCase("Glee.S04E09.Swan.Song.1080p.WEB-DL.DD5.1.H.264-ECI", "Glee", 4, 9)] - [TestCase("S08E20 50-50 Carla [DVD]", "", 8, 20)] - [TestCase("Cheers S08E20 50-50 Carla [DVD]", "Cheers", 8, 20)] - [TestCase("S02E10 6-50 to SLC [SDTV]", "", 2, 10)] - [TestCase("Franklin & Bash S02E10 6-50 to SLC [SDTV]", "Franklin & Bash", 2, 10)] - [TestCase("The_Big_Bang_Theory_-_6x12_-_The_Egg_Salad_Equivalency_[HDTV-720p]", "The Big Bang Theory", 6, 12)] - [TestCase("Top_Gear.19x06.720p_HDTV_x264-FoV", "Top Gear", 19, 6)] - [TestCase("Portlandia.S03E10.Alexandra.720p.WEB-DL.AAC2.0.H.264-CROM.mkv", "Portlandia", 3, 10)] - [TestCase("(Game of Thrones s03 e - \"Game of Thrones Season 3 Episode 10\"", "Game of Thrones", 3, 10)] - [TestCase("House.Hunters.International.S05E607.720p.hdtv.x264", "House.Hunters.International", 5, 607)] - [TestCase("Adventure.Time.With.Finn.And.Jake.S01E20.720p.BluRay.x264-DEiMOS", "Adventure.Time.With.Finn.And.Jake", 1, 20)] - [TestCase("Hostages.S01E04.2-45.PM.[HDTV-720p].mkv", "Hostages", 1, 4)] - [TestCase("S01E04", "", 1, 4)] - [TestCase("1x04", "", 1, 4)] - [TestCase("10.Things.You.Dont.Know.About.S02E04.Prohibition.HDTV.XviD-AFG", "10 Things You Dont Know About", 2, 4)] - [TestCase("30 Rock - S01E01 - Pilot.avi", "30 Rock", 1, 1)] - [TestCase("666 Park Avenue - S01E01", "666 Park Avenue", 1, 1)] - [TestCase("Warehouse 13 - S01E01", "Warehouse 13", 1, 1)] - [TestCase("Don't Trust The B---- in Apartment 23.S01E01", "Don't Trust The B---- in Apartment 23", 1, 1)] - [TestCase("Warehouse.13.S01E01", "Warehouse.13", 1, 1)] - [TestCase("Dont.Trust.The.B----.in.Apartment.23.S01E01", "Dont.Trust.The.B----.in.Apartment.23", 1, 1)] - [TestCase("24 S01E01", "24", 1, 1)] - [TestCase("24.S01E01", "24", 1, 1)] - [TestCase("Homeland - 2x12 - The Choice [HDTV-1080p].mkv", "Homeland", 2, 12)] - [TestCase("Homeland - 2x4 - New Car Smell [HDTV-1080p].mkv", "Homeland", 2, 4)] - public void ParseTitle_single(string postTitle, string title, int seasonNumber, int episodeNumber) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Should().NotBeNull(); - result.EpisodeNumbers.Should().HaveCount(1); - result.SeasonNumber.Should().Be(seasonNumber); - result.EpisodeNumbers.First().Should().Be(episodeNumber); - result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - } - - [TestCase(@"z:\tv shows\battlestar galactica (2003)\Season 3\S03E05 - Collaborators.mkv", 3, 5)] - [TestCase(@"z:\tv shows\modern marvels\Season 16\S16E03 - The Potato.mkv", 16, 3)] - [TestCase(@"z:\tv shows\robot chicken\Specials\S00E16 - Dear Consumer - SD TV.avi", 0, 16)] - [TestCase(@"D:\shares\TV Shows\Parks And Recreation\Season 2\S02E21 - 94 Meetings - 720p TV.mkv", 2, 21)] - [TestCase(@"D:\shares\TV Shows\Battlestar Galactica (2003)\Season 2\S02E21.avi", 2, 21)] - [TestCase("C:/Test/TV/Chuck.4x05.HDTV.XviD-LOL", 4, 5)] - [TestCase(@"P:\TV Shows\House\Season 6\S06E13 - 5 to 9 - 720p BluRay.mkv", 6, 13)] - [TestCase(@"S:\TV Drop\House - 10x11 - Title [SDTV]\1011 - Title.avi", 10, 11)] - [TestCase(@"/TV Drop/House - 10x11 - Title [SDTV]/1011 - Title.avi", 10, 11)] - [TestCase(@"S:\TV Drop\King of the Hill - 10x12 - 24 Hour Propane People [SDTV]\1012 - 24 Hour Propane People.avi", 10, 12)] - [TestCase(@"/TV Drop/King of the Hill - 10x12 - 24 Hour Propane People [SDTV]/1012 - 24 Hour Propane People.avi", 10, 12)] - [TestCase(@"S:\TV Drop\King of the Hill - 10x12 - 24 Hour Propane People [SDTV]\Hour Propane People.avi", 10, 12)] - [TestCase(@"/TV Drop/King of the Hill - 10x12 - 24 Hour Propane People [SDTV]/Hour Propane People.avi", 10, 12)] - [TestCase(@"E:\Downloads\tv\The.Big.Bang.Theory.S01E01.720p.HDTV\ajifajjjeaeaeqwer_eppj.avi", 1, 1)] - [TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)] - public void PathParse_tests(string path, int season, int episode) - { - var result = Parser.Parser.ParsePath(path); - result.EpisodeNumbers.Should().HaveCount(1); - result.SeasonNumber.Should().Be(season); - result.EpisodeNumbers[0].Should().Be(episode); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - - ExceptionVerification.IgnoreWarns(); - } - - [TestCase("THIS SHOULD NEVER PARSE")] - public void unparsable_title_should_log_warn_and_return_null(string title) - { - Parser.Parser.ParseTitle(title).Should().BeNull(); - } - - //[Timeout(1000)] - [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD", "WEEDS", 3, new[] { 1, 2, 3, 4, 5, 6 })] - [TestCase("Two.and.a.Half.Men.103.104.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Men", 1, new[] { 3, 4 })] - [TestCase("Weeds.S03E01.S03E02.720p.HDTV.X264-DIMENSION", "Weeds", 3, new[] { 1, 2 })] - [TestCase("The Borgias S01e01 e02 ShoHD On Demand 1080i DD5 1 ALANiS", "The Borgias", 1, new[] { 1, 2 })] - [TestCase("White.Collar.2x04.2x05.720p.BluRay-FUTV", "White.Collar", 2, new[] { 4, 5 })] - [TestCase("Desperate.Housewives.S07E22E23.720p.HDTV.X264-DIMENSION", "Desperate.Housewives", 7, new[] { 22, 23 })] - [TestCase("Desparate Housewives - S07E22 - S07E23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 })] - [TestCase("S03E01.S03E02.720p.HDTV.X264-DIMENSION", "", 3, new[] { 1, 2 })] - [TestCase("Desparate Housewives - S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 })] - [TestCase("S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "", 7, new[] { 22, 23 })] - [TestCase("2x04x05.720p.BluRay-FUTV", "", 2, new[] { 4, 5 })] - [TestCase("S02E04E05.720p.BluRay-FUTV", "", 2, new[] { 4, 5 })] - [TestCase("S02E03-04-05.720p.BluRay-FUTV", "", 2, new[] { 3, 4, 5 })] - [TestCase("Breakout.Kings.S02E09-E10.HDTV.x264-ASAP", "Breakout Kings", 2, new[] { 9, 10 })] - [TestCase("Breakout Kings - 2x9-2x10 - Served Cold [SDTV] ", "Breakout Kings", 2, new[] { 9, 10 })] - [TestCase("Breakout Kings - 2x09-2x10 - Served Cold [SDTV] ", "Breakout Kings", 2, new[] { 9, 10 })] - [TestCase("Hell on Wheels S02E09 E10 HDTV x264 EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] - [TestCase("Hell.on.Wheels.S02E09-E10.720p.HDTV.x264-EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] - [TestCase("Grey's Anatomy - 8x01_02 - Free Falling", "Grey's Anatomy", 8, new [] { 1,2 })] - [TestCase("8x01_02 - Free Falling", "", 8, new[] { 1, 2 })] - [TestCase("Kaamelott.S01E91-E100", "Kaamelott", 1, new[] { 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 })] - [TestCase("Neighbours.S29E161-E165.PDTV.x264-FQM", "Neighbours", 29, new[] { 161, 162, 163, 164, 165 })] - [TestCase("Shortland.Street.S22E5363-E5366.HDTV.x264-FiHTV", "Shortland Street", 22, new[] { 5363, 5364, 5365, 5366 })] - public void TitleParse_multi(string postTitle, string title, int season, int[] episodes) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.SeasonNumber.Should().Be(season); - result.EpisodeNumbers.Should().BeEquivalentTo(episodes); - result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - } - - - [TestCase("Conan 2011 04 18 Emma Roberts HDTV XviD BFF", "Conan", 2011, 04, 18)] - [TestCase("The Tonight Show With Jay Leno 2011 04 15 1080i HDTV DD5 1 MPEG2 TrollHD", "The Tonight Show With Jay Leno", 2011, 04, 15)] - [TestCase("The.Daily.Show.2010.10.11.Johnny.Knoxville.iTouch-MW", "The.Daily.Show", 2010, 10, 11)] - [TestCase("The Daily Show - 2011-04-12 - Gov. Deval Patrick", "The.Daily.Show", 2011, 04, 12)] - [TestCase("2011.01.10 - Denis Leary - HD TV.mkv", "", 2011, 1, 10)] - [TestCase("2011.03.13 - Denis Leary - HD TV.mkv", "", 2011, 3, 13)] - [TestCase("The Tonight Show with Jay Leno - 2011-06-16 - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo", "The Tonight Show with Jay Leno", 2011, 6, 16)] - [TestCase("2020.NZ.2012.16.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 16)] - [TestCase("2020.NZ.2012.13.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 13)] - [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020nz", 2011, 12, 2)] - public void parse_daily_episodes(string postTitle, string title, int year, int month, int day) - { - var result = Parser.Parser.ParseTitle(postTitle); - var airDate = new DateTime(year, month, day); - result.Should().NotBeNull(); - result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); - result.AirDate.Should().Be(airDate.ToString(Episode.AIR_DATE_FORMAT)); - result.EpisodeNumbers.Should().BeEmpty(); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - } - [TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "High School DxD", 7, 0, 0)] [TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Working!!", 6, 0, 0)] [TestCase("[Commie]_Senki_Zesshou_Symphogear_-_11_[65F220B4]", "Senki_Zesshou_Symphogear", 11, 0, 0)] @@ -232,138 +56,6 @@ namespace NzbDrone.Core.Test.ParserTests result.FullSeason.Should().BeFalse(); } - [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] - [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] - [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] - [TestCase("The Daily Show - {year}-{month}-{day} - Gov. Deval Patrick")] - [TestCase("{year}.{month}.{day} - Denis Leary - HD TV.mkv")] - [TestCase("The Tonight Show with Jay Leno - {year}-{month}-{day} - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo")] - [TestCase("2020.NZ.{year}.{month}.{day}.PDTV.XviD-C4TV")] - public void should_not_accept_ancient_daily_series(string title) - { - var yearTooLow = title.Expand(new { year = 1950, month = 10, day = 14 }); - Parser.Parser.ParseTitle(yearTooLow).Should().BeNull(); - } - - [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] - [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] - [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] - [TestCase("The Daily Show - {year}-{month}-{day} - Gov. Deval Patrick")] - [TestCase("{year}.{month}.{day} - Denis Leary - HD TV.mkv")] - [TestCase("The Tonight Show with Jay Leno - {year}-{month}-{day} - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo")] - [TestCase("2020.NZ.{year}.{month}.{day}.PDTV.XviD-C4TV")] - public void should_not_accept_future_dates(string title) - { - var twoDaysFromNow = DateTime.Now.AddDays(2); - - var validDate = title.Expand(new { year = twoDaysFromNow.Year, month = twoDaysFromNow.Month.ToString("00"), day = twoDaysFromNow.Day.ToString("00") }); - - Parser.Parser.ParseTitle(validDate).Should().BeNull(); - } - - [Test] - public void parse_daily_should_fail_if_episode_is_far_in_future() - { - var title = string.Format("{0:yyyy.MM.dd} - Denis Leary - HD TV.mkv", DateTime.Now.AddDays(2)); - - Parser.Parser.ParseTitle(title).Should().BeNull(); - } - - [TestCase("30.Rock.Season.04.HDTV.XviD-DIMENSION", "30.Rock", 4)] - [TestCase("Parks.and.Recreation.S02.720p.x264-DIMENSION", "Parks.and.Recreation", 2)] - [TestCase("The.Office.US.S03.720p.x264-DIMENSION", "The.Office.US", 3)] - [TestCase(@"Sons.of.Anarchy.S03.720p.BluRay-CLUE\REWARD", "Sons.of.Anarchy", 3)] - [TestCase("Adventure Time S02 720p HDTV x264 CRON", "Adventure Time", 2)] - [TestCase("Sealab.2021.S04.iNTERNAL.DVDRip.XviD-VCDVaULT", "Sealab 2021", 4)] - public void full_season_release_parse(string postTitle, string title, int season) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.SeasonNumber.Should().Be(season); - result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); - result.EpisodeNumbers.Should().BeEmpty(); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeTrue(); - } - - [TestCase("Conan", "conan")] - [TestCase("The Tonight Show With Jay Leno", "tonightshowwithjayleno")] - [TestCase("The.Daily.Show", "dailyshow")] - [TestCase("Castle (2009)", "castle2009")] - [TestCase("Parenthood.2010", "parenthood2010")] - [TestCase("Law_and_Order_SVU", "lawordersvu")] - public void series_name_normalize(string parsedSeriesName, string seriesName) - { - var result = parsedSeriesName.CleanSeriesTitle(); - result.Should().Be(seriesName); - } - - [TestCase("CaPitAl", "capital")] - [TestCase("peri.od", "period")] - [TestCase("this.^&%^**$%@#$!That", "thisthat")] - [TestCase("test/test", "testtest")] - [TestCase("90210", "90210")] - [TestCase("24", "24")] - public void Normalize_Title(string dirty, string clean) - { - var result = dirty.CleanSeriesTitle(); - result.Should().Be(clean); - } - - [TestCase("the")] - [TestCase("and")] - [TestCase("or")] - [TestCase("a")] - [TestCase("an")] - [TestCase("of")] - public void Normalize_removed_common_words(string word) - { - var dirtyFormat = new[] - { - "word.{0}.word", - "word {0} word", - "word-{0}-word", - "{0}.word.word", - "{0}-word-word", - "{0} word word", - "word.word.{0}", - "word-word-{0}", - "word-word {0}", - }; - - foreach (var s in dirtyFormat) - { - var dirty = String.Format(s, word); - dirty.CleanSeriesTitle().Should().Be("wordword"); - } - - } - - [TestCase("the")] - [TestCase("and")] - [TestCase("or")] - [TestCase("a")] - [TestCase("an")] - [TestCase("of")] - public void Normalize_not_removed_common_words_in_the_middle(string word) - { - var dirtyFormat = new[] - { - "word.{0}word", - "word {0}word", - "word-{0}word", - "word{0}.word", - "word{0}-word", - "word{0}-word", - }; - - foreach (var s in dirtyFormat) - { - var dirty = String.Format(s, word); - dirty.CleanSeriesTitle().Should().Be(("word" + word.ToLower() + "word")); - } - - } - [TestCase("Chuck - 4x05 - Title", "Chuck")] [TestCase("Law & Order - 4x05 - Title", "laworder")] [TestCase("Bad Format", "badformat")] @@ -376,115 +68,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Hawaii Five 0", "hawaiifive0")] [TestCase("Match of the Day", "matchday")] [TestCase("Match of the Day 2", "matchday2")] - public void parse_series_name(string postTitle, string title) + public void should_parse_series_name(string postTitle, string title) { var result = Parser.Parser.ParseSeriesName(postTitle); result.Should().Be(title.CleanSeriesTitle()); } - - [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", Language.English)] - [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL", Language.French)] - [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL", Language.Spanish)] - [TestCase("Castle.2009.S01E14.German.HDTV.XviD-LOL", Language.German)] - [TestCase("Castle.2009.S01E14.Germany.HDTV.XviD-LOL", Language.English)] - [TestCase("Castle.2009.S01E14.Italian.HDTV.XviD-LOL", Language.Italian)] - [TestCase("Castle.2009.S01E14.Danish.HDTV.XviD-LOL", Language.Danish)] - [TestCase("Castle.2009.S01E14.Dutch.HDTV.XviD-LOL", Language.Dutch)] - [TestCase("Castle.2009.S01E14.Japanese.HDTV.XviD-LOL", Language.Japanese)] - [TestCase("Castle.2009.S01E14.Cantonese.HDTV.XviD-LOL", Language.Cantonese)] - [TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL", Language.Mandarin)] - [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)] - [TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL", Language.Russian)] - [TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL", Language.Polish)] - [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)] - [TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL", Language.Swedish)] - [TestCase("Castle.2009.S01E14.Norwegian.HDTV.XviD-LOL", Language.Norwegian)] - [TestCase("Castle.2009.S01E14.Finnish.HDTV.XviD-LOL", Language.Finnish)] - [TestCase("Castle.2009.S01E14.Turkish.HDTV.XviD-LOL", Language.Turkish)] - [TestCase("Castle.2009.S01E14.Portuguese.HDTV.XviD-LOL", Language.Portuguese)] - [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL", Language.English)] - [TestCase("person.of.interest.1x19.ita.720p.bdmux.x264-novarip", Language.Italian)] - [TestCase("Salamander.S01E01.FLEMISH.HDTV.x264-BRiGAND", Language.Flemish)] - [TestCase("H.Polukatoikia.S03E13.Greek.PDTV.XviD-Ouzo", Language.Greek)] - [TestCase("Burn.Notice.S04E15.Brotherly.Love.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP", Language.German)] - [TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Norwegian)] - [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)] - [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)] - public void parse_language(string postTitle, Language language) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Language.Should().Be(language); - } - - [TestCase("Hawaii Five 0 S01 720p WEB DL DD5 1 H 264 NT", "Hawaii Five 0", 1)] - [TestCase("30 Rock S03 WS PDTV XviD FUtV", "30 Rock", 3)] - [TestCase("The Office Season 4 WS PDTV XviD FUtV", "The Office", 4)] - [TestCase("Eureka Season 1 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)] - [TestCase("The Office Season4 WS PDTV XviD FUtV", "The Office", 4)] - [TestCase("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)] - [TestCase("Doctor Who Confidential Season 3", "Doctor Who Confidential", 3)] - public void parse_season_info(string postTitle, string seriesName, int seasonNumber) - { - var result = Parser.Parser.ParseTitle(postTitle); - - result.SeriesTitle.Should().Be(seriesName.CleanSeriesTitle()); - result.SeasonNumber.Should().Be(seasonNumber); - result.FullSeason.Should().BeTrue(); - } - - [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER")] - [TestCase("Punky Brewster S01 EXTRAS DVDRip XviD RUNNER")] - [TestCase("Instant Star S03 EXTRAS DVDRip XviD OSiTV")] - public void parse_season_extras(string postTitle) - { - var result = Parser.Parser.ParseTitle(postTitle); - - result.Should().BeNull(); - } - - [TestCase("Lie.to.Me.S03.SUBPACK.DVDRip.XviD-REWARD")] - [TestCase("The.Middle.S02.SUBPACK.DVDRip.XviD-REWARD")] - [TestCase("CSI.S11.SUBPACK.DVDRip.XviD-REWARD")] - public void parse_season_subpack(string postTitle) - { - var result = Parser.Parser.ParseTitle(postTitle); - - result.Should().BeNull(); - } - - [TestCase("76El6LcgLzqb426WoVFg1vVVVGx4uCYopQkfjmLe")] - [TestCase("Vrq6e1Aba3U amCjuEgV5R2QvdsLEGYF3YQAQkw8")] - [TestCase("TDAsqTea7k4o6iofVx3MQGuDK116FSjPobMuh8oB")] - [TestCase("yp4nFodAAzoeoRc467HRh1mzuT17qeekmuJ3zFnL")] - [TestCase("oxXo8S2272KE1 lfppvxo3iwEJBrBmhlQVK1gqGc")] - [TestCase("dPBAtu681Ycy3A4NpJDH6kNVQooLxqtnsW1Umfiv")] - [TestCase("password - \"bdc435cb-93c4-4902-97ea-ca00568c3887.337\" yEnc")] - public void should_not_parse_crap(string title) - { - Parser.Parser.ParseTitle(title).Should().BeNull(); - ExceptionVerification.IgnoreWarns(); - } - - [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", "LOL")] - [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "LOL")] - [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "RUNNER")] - [TestCase("Punky.Brewster.S01.EXTRAS.DVDRip.XviD-RUNNER", "RUNNER")] - [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "C4TV")] - [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "OSiTV")] - [TestCase("The Office - S01E01 - Pilot [HTDV-480p]", "DRONE")] - [TestCase("The Office - S01E01 - Pilot [HTDV-720p]", "DRONE")] - [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] - public void parse_releaseGroup(string title, string expected) - { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); - } - - [Test] - public void should_not_include_extension_in_releaseGroup() - { - const string path = @"C:\Test\Doctor.Who.2005.s01e01.internal.bdrip.x264-archivist.mkv"; - - Parser.Parser.ParsePath(path).ReleaseGroup.Should().Be("archivist"); - } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs new file mode 100644 index 000000000..d9f46806a --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Expansive; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class PathParserFixture : CoreTest + { + [TestCase(@"z:\tv shows\battlestar galactica (2003)\Season 3\S03E05 - Collaborators.mkv", 3, 5)] + [TestCase(@"z:\tv shows\modern marvels\Season 16\S16E03 - The Potato.mkv", 16, 3)] + [TestCase(@"z:\tv shows\robot chicken\Specials\S00E16 - Dear Consumer - SD TV.avi", 0, 16)] + [TestCase(@"D:\shares\TV Shows\Parks And Recreation\Season 2\S02E21 - 94 Meetings - 720p TV.mkv", 2, 21)] + [TestCase(@"D:\shares\TV Shows\Battlestar Galactica (2003)\Season 2\S02E21.avi", 2, 21)] + [TestCase("C:/Test/TV/Chuck.4x05.HDTV.XviD-LOL", 4, 5)] + [TestCase(@"P:\TV Shows\House\Season 6\S06E13 - 5 to 9 - 720p BluRay.mkv", 6, 13)] + [TestCase(@"S:\TV Drop\House - 10x11 - Title [SDTV]\1011 - Title.avi", 10, 11)] + [TestCase(@"/TV Drop/House - 10x11 - Title [SDTV]/1011 - Title.avi", 10, 11)] + [TestCase(@"S:\TV Drop\King of the Hill - 10x12 - 24 Hour Propane People [SDTV]\1012 - 24 Hour Propane People.avi", 10, 12)] + [TestCase(@"/TV Drop/King of the Hill - 10x12 - 24 Hour Propane People [SDTV]/1012 - 24 Hour Propane People.avi", 10, 12)] + [TestCase(@"S:\TV Drop\King of the Hill - 10x12 - 24 Hour Propane People [SDTV]\Hour Propane People.avi", 10, 12)] + [TestCase(@"/TV Drop/King of the Hill - 10x12 - 24 Hour Propane People [SDTV]/Hour Propane People.avi", 10, 12)] + [TestCase(@"E:\Downloads\tv\The.Big.Bang.Theory.S01E01.720p.HDTV\ajifajjjeaeaeqwer_eppj.avi", 1, 1)] + [TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)] + [TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E19.720p.BluRay.x264-SiNNERS-RP\ba27283b17c00d01193eacc02a8ba98eeb523a76.mkv", 2, 19)] + [TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E18.720p.BluRay.x264-SiNNERS-RP\45a55debe3856da318cc35882ad07e43cd32fd15.mkv", 2, 18)] + public void should_parse_from_path(string path, int season, int episode) + { + var result = Parser.Parser.ParsePath(path); + result.EpisodeNumbers.Should().HaveCount(1); + result.SeasonNumber.Should().Be(season); + result.EpisodeNumbers[0].Should().Be(episode); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + + ExceptionVerification.IgnoreWarns(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs new file mode 100644 index 000000000..e5b3ba71f --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class ReleaseGroupParserFixture : CoreTest + { + [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", "LOL")] + [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "LOL")] + [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "RUNNER")] + [TestCase("Punky.Brewster.S01.EXTRAS.DVDRip.XviD-RUNNER", "RUNNER")] + [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "C4TV")] + [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "OSiTV")] + [TestCase("The Office - S01E01 - Pilot [HTDV-480p]", "DRONE")] + [TestCase("The Office - S01E01 - Pilot [HTDV-720p]", "DRONE")] + [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] + public void should_parse_release_group(string title, string expected) + { + Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + } + + [Test] + public void should_not_include_extension_in_release_roup() + { + const string path = @"C:\Test\Doctor.Who.2005.s01e01.internal.bdrip.x264-archivist.mkv"; + + Parser.Parser.ParsePath(path).ReleaseGroup.Should().Be("archivist"); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs new file mode 100644 index 000000000..2655f9d6e --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class SeasonParserFixture : CoreTest + { + [TestCase("30.Rock.Season.04.HDTV.XviD-DIMENSION", "30.Rock", 4)] + [TestCase("Parks.and.Recreation.S02.720p.x264-DIMENSION", "Parks.and.Recreation", 2)] + [TestCase("The.Office.US.S03.720p.x264-DIMENSION", "The.Office.US", 3)] + [TestCase(@"Sons.of.Anarchy.S03.720p.BluRay-CLUE\REWARD", "Sons.of.Anarchy", 3)] + [TestCase("Adventure Time S02 720p HDTV x264 CRON", "Adventure Time", 2)] + [TestCase("Sealab.2021.S04.iNTERNAL.DVDRip.XviD-VCDVaULT", "Sealab 2021", 4)] + [TestCase("Hawaii Five 0 S01 720p WEB DL DD5 1 H 264 NT", "Hawaii Five 0", 1)] + [TestCase("30 Rock S03 WS PDTV XviD FUtV", "30 Rock", 3)] + [TestCase("The Office Season 4 WS PDTV XviD FUtV", "The Office", 4)] + [TestCase("Eureka Season 1 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)] + [TestCase("The Office Season4 WS PDTV XviD FUtV", "The Office", 4)] + [TestCase("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)] + [TestCase("Doctor Who Confidential Season 3", "Doctor Who Confidential", 3)] + public void should_parsefull_season_release(string postTitle, string title, int season) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.SeasonNumber.Should().Be(season); + result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); + result.EpisodeNumbers.Should().BeEmpty(); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeTrue(); + } + + [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER")] + [TestCase("Punky Brewster S01 EXTRAS DVDRip XviD RUNNER")] + [TestCase("Instant Star S03 EXTRAS DVDRip XviD OSiTV")] + public void should_parse_season_extras(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + + result.Should().BeNull(); + } + + [TestCase("Lie.to.Me.S03.SUBPACK.DVDRip.XviD-REWARD")] + [TestCase("The.Middle.S02.SUBPACK.DVDRip.XviD-REWARD")] + [TestCase("CSI.S11.SUBPACK.DVDRip.XviD-REWARD")] + public void should_parse_season_subpack(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + + result.Should().BeNull(); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs new file mode 100644 index 000000000..baca66d12 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + + [TestFixture] + public class SingleEpisodeParserFixture : CoreTest + { + [TestCase("Sonny.With.a.Chance.S02E15", "Sonny.With.a.Chance", 2, 15)] + [TestCase("Two.and.a.Half.Me.103.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Me", 1, 3)] + [TestCase("Two.and.a.Half.Me.113.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Me", 1, 13)] + [TestCase("Two.and.a.Half.Me.1013.720p.HDTV.X264-DIMENSION", "Two.and.a.Half.Me", 10, 13)] + [TestCase("Chuck.4x05.HDTV.XviD-LOL", "Chuck", 4, 5)] + [TestCase("The.Girls.Next.Door.S03E06.DVDRip.XviD-WiDE", "The.Girls.Next.Door", 3, 6)] + [TestCase("Degrassi.S10E27.WS.DSR.XviD-2HD", "Degrassi", 10, 27)] + [TestCase("Parenthood.2010.S02E14.HDTV.XviD-LOL", "Parenthood 2010", 2, 14)] + [TestCase("Hawaii Five 0 S01E19 720p WEB DL DD5 1 H 264 NT", "Hawaii Five 0", 1, 19)] + [TestCase("The Event S01E14 A Message Back 720p WEB DL DD5 1 H264 SURFER", "The Event", 1, 14)] + [TestCase("Adam Hills In Gordon St Tonight S01E07 WS PDTV XviD FUtV", "Adam Hills In Gordon St Tonight", 1, 7)] + [TestCase("Adam Hills In Gordon St Tonight S01E07 WS PDTV XviD FUtV", "Adam Hills In Gordon St Tonight", 1, 7)] + [TestCase("Adventure.Inc.S03E19.DVDRip.XviD-OSiTV", "Adventure.Inc", 3, 19)] + [TestCase("S03E09 WS PDTV XviD FUtV", "", 3, 9)] + [TestCase("5x10 WS PDTV XviD FUtV", "", 5, 10)] + [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL", "Castle 2009", 1, 14)] + [TestCase("Pride.and.Prejudice.1995.S03E20.HDTV.XviD-LOL", "Pride and Prejudice 1995", 3, 20)] + [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "The.Office", 3, 115)] + [TestCase(@"Parks and Recreation - S02E21 - 94 Meetings - 720p TV.mkv", "Parks and Recreation", 2, 21)] + [TestCase(@"24-7 Penguins-Capitals- Road to the NHL Winter Classic - S01E03 - Episode 3.mkv", "24-7 Penguins-Capitals- Road to the NHL Winter Classic", 1, 3)] + [TestCase("Adventure.Inc.S03E19.DVDRip.\"XviD\"-OSiTV", "Adventure.Inc", 3, 19)] + [TestCase("Hawaii Five-0 (2010) - 1x05 - Nalowale (Forgotten/Missing)", "Hawaii Five-0 (2010)", 1, 5)] + [TestCase("Hawaii Five-0 (2010) - 1x05 - Title", "Hawaii Five-0 (2010)", 1, 5)] + [TestCase("House - S06E13 - 5 to 9 [DVD]", "House", 6, 13)] + [TestCase("The Mentalist - S02E21 - 18-5-4", "The Mentalist", 2, 21)] + [TestCase("Breaking.In.S01E07.21.0.Jump.Street.720p.WEB-DL.DD5.1.h.264-KiNGS", "Breaking In", 1, 7)] + [TestCase("CSI.525", "CSI", 5, 25)] + [TestCase("King of the Hill - 10x12 - 24 Hour Propane People [SDTV]", "King of the Hill", 10, 12)] + [TestCase("Brew Masters S01E06 3 Beers For Batali DVDRip XviD SPRiNTER", "Brew Masters", 1, 6)] + [TestCase("24 7 Flyers Rangers Road to the NHL Winter Classic Part01 720p HDTV x264 ORENJI", "24 7 Flyers Rangers Road to the NHL Winter Classic", 1, 1)] + [TestCase("24 7 Flyers Rangers Road to the NHL Winter Classic Part 02 720p HDTV x264 ORENJI", "24 7 Flyers Rangers Road to the NHL Winter Classic", 1, 2)] + [TestCase("24-7 Flyers-Rangers- Road to the NHL Winter Classic - S01E01 - Part 1", "24 7 Flyers Rangers Road to the NHL Winter Classic", 1, 1)] + [TestCase("The.Kennedys.Part.2.DSR.XviD-SYS", "The Kennedys", 1, 2)] + [TestCase("the-pacific-e07-720p", "The Pacific", 1, 7)] + [TestCase("S6E02-Unwrapped-(Playing With Food) - [DarkData]", "", 6, 2)] + [TestCase("S06E03-Unwrapped-(Number Ones Unwrapped) - [DarkData]", "", 6, 3)] + [TestCase("The Mentalist S02E21 18 5 4 720p WEB DL DD5 1 h 264 EbP", "The Mentalist", 2, 21)] + [TestCase("01x04 - Halloween, Part 1 - 720p WEB-DL", "", 1, 4)] + [TestCase("extras.s03.e05.ws.dvdrip.xvid-m00tv", "Extras", 3, 5)] + [TestCase("castle.2009.416.hdtv-lol", "Castle 2009", 4, 16)] + [TestCase("hawaii.five-0.2010.217.hdtv-lol", "Hawaii Five-0 (2010)", 2, 17)] + [TestCase("Looney Tunes - S1936E18 - I Love to Singa", "Looney Tunes", 1936, 18)] + [TestCase("American_Dad!_-_7x6_-_The_Scarlett_Getter_[SDTV]", "American Dad!", 7, 6)] + [TestCase("Falling_Skies_-_1x1_-_Live_and_Learn_[HDTV-720p]", "Falling Skies", 1, 1)] + [TestCase("Top Gear - 07x03 - 2005.11.70", "Top Gear", 7, 3)] + [TestCase("Hatfields and McCoys 2012 Part 1 REPACK 720p HDTV x264 2HD", "Hatfields and McCoys 2012", 1, 1)] + [TestCase("Glee.S04E09.Swan.Song.1080p.WEB-DL.DD5.1.H.264-ECI", "Glee", 4, 9)] + [TestCase("S08E20 50-50 Carla [DVD]", "", 8, 20)] + [TestCase("Cheers S08E20 50-50 Carla [DVD]", "Cheers", 8, 20)] + [TestCase("S02E10 6-50 to SLC [SDTV]", "", 2, 10)] + [TestCase("Franklin & Bash S02E10 6-50 to SLC [SDTV]", "Franklin & Bash", 2, 10)] + [TestCase("The_Big_Bang_Theory_-_6x12_-_The_Egg_Salad_Equivalency_[HDTV-720p]", "The Big Bang Theory", 6, 12)] + [TestCase("Top_Gear.19x06.720p_HDTV_x264-FoV", "Top Gear", 19, 6)] + [TestCase("Portlandia.S03E10.Alexandra.720p.WEB-DL.AAC2.0.H.264-CROM.mkv", "Portlandia", 3, 10)] + [TestCase("(Game of Thrones s03 e - \"Game of Thrones Season 3 Episode 10\"", "Game of Thrones", 3, 10)] + [TestCase("House.Hunters.International.S05E607.720p.hdtv.x264", "House.Hunters.International", 5, 607)] + [TestCase("Adventure.Time.With.Finn.And.Jake.S01E20.720p.BluRay.x264-DEiMOS", "Adventure.Time.With.Finn.And.Jake", 1, 20)] + [TestCase("Hostages.S01E04.2-45.PM.[HDTV-720p].mkv", "Hostages", 1, 4)] + [TestCase("S01E04", "", 1, 4)] + [TestCase("1x04", "", 1, 4)] + [TestCase("10.Things.You.Dont.Know.About.S02E04.Prohibition.HDTV.XviD-AFG", "10 Things You Dont Know About", 2, 4)] + [TestCase("30 Rock - S01E01 - Pilot.avi", "30 Rock", 1, 1)] + [TestCase("666 Park Avenue - S01E01", "666 Park Avenue", 1, 1)] + [TestCase("Warehouse 13 - S01E01", "Warehouse 13", 1, 1)] + [TestCase("Don't Trust The B---- in Apartment 23.S01E01", "Don't Trust The B---- in Apartment 23", 1, 1)] + [TestCase("Warehouse.13.S01E01", "Warehouse.13", 1, 1)] + [TestCase("Dont.Trust.The.B----.in.Apartment.23.S01E01", "Dont.Trust.The.B----.in.Apartment.23", 1, 1)] + [TestCase("24 S01E01", "24", 1, 1)] + [TestCase("24.S01E01", "24", 1, 1)] + [TestCase("Homeland - 2x12 - The Choice [HDTV-1080p].mkv", "Homeland", 2, 12)] + [TestCase("Homeland - 2x4 - New Car Smell [HDTV-1080p].mkv", "Homeland", 2, 4)] + public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.EpisodeNumbers.Should().HaveCount(1); + result.SeasonNumber.Should().Be(seasonNumber); + result.EpisodeNumbers.First().Should().Be(episodeNumber); + result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 794a00c43..de22c722f 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Annotations Textbox, Password, Checkbox, - Select + Select, + Path } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index 1a641bfca..fc6498412 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -1,4 +1,7 @@ -using NzbDrone.Core.Datastore; +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Blacklisting @@ -6,6 +9,7 @@ namespace NzbDrone.Core.Blacklisting public interface IBlacklistRepository : IBasicRepository { bool Blacklisted(string sourceTitle); + List BlacklistedBySeries(int seriesId); } public class BlacklistRepository : BasicRepository, IBlacklistRepository @@ -17,7 +21,12 @@ namespace NzbDrone.Core.Blacklisting public bool Blacklisted(string sourceTitle) { - return Query.Any(e => e.SourceTitle.Contains(sourceTitle)); + return Query.Where(e => e.SourceTitle.Contains(sourceTitle)).Any(); + } + + public List BlacklistedBySeries(int seriesId) + { + return Query.Where(b => b.SeriesId == seriesId); } } } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 8b3ab0a2d..fafbec44e 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -3,6 +3,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Blacklisting { @@ -13,7 +14,7 @@ namespace NzbDrone.Core.Blacklisting void Delete(int id); } - public class BlacklistService : IBlacklistService, IHandle, IExecute + public class BlacklistService : IBlacklistService, IExecute, IHandle, IHandle { private readonly IBlacklistRepository _blacklistRepository; private readonly IRedownloadFailedDownloads _redownloadFailedDownloadService; @@ -39,6 +40,11 @@ namespace NzbDrone.Core.Blacklisting _blacklistRepository.Delete(id); } + public void Execute(ClearBlacklistCommand message) + { + _blacklistRepository.Purge(); + } + public void Handle(DownloadFailedEvent message) { var blacklist = new Blacklist @@ -55,9 +61,11 @@ namespace NzbDrone.Core.Blacklisting _redownloadFailedDownloadService.Redownload(message.SeriesId, message.EpisodeIds); } - public void Execute(ClearBlacklistCommand message) + public void Handle(SeriesDeletedEvent message) { - _blacklistRepository.Purge(); + var blacklisted = _blacklistRepository.BlacklistedBySeries(message.Series.Id); + + _blacklistRepository.DeleteMany(blacklisted); } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigRepository.cs b/src/NzbDrone.Core/Configuration/ConfigRepository.cs index 0c21b2793..7aef7d26f 100644 --- a/src/NzbDrone.Core/Configuration/ConfigRepository.cs +++ b/src/NzbDrone.Core/Configuration/ConfigRepository.cs @@ -21,9 +21,7 @@ namespace NzbDrone.Core.Configuration public Config Get(string key) { - return Query.SingleOrDefault(c => c.Key == key); + return Query.Where(c => c.Key == key).SingleOrDefault(); } - - } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 6ebe823ff..a0aa00bc8 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Configuration return dict; } - public void SaveValues(Dictionary configValues) + public void SaveConfigDictionary(Dictionary configValues) { var allWithDefaults = AllWithDefaults(); @@ -73,69 +73,6 @@ namespace NzbDrone.Core.Configuration _eventAggregator.PublishEvent(new ConfigSavedEvent()); } - public String SabHost - { - get { return GetValue("SabHost", "localhost"); } - - set { SetValue("SabHost", value); } - } - - public int SabPort - { - get { return GetValueInt("SabPort", 8080); } - - set { SetValue("SabPort", value); } - } - - public String SabApiKey - { - get { return GetValue("SabApiKey"); } - - set { SetValue("SabApiKey", value); } - } - - public String SabUsername - { - get { return GetValue("SabUsername"); } - - set { SetValue("SabUsername", value); } - } - - public String SabPassword - { - get { return GetValue("SabPassword"); } - - set { SetValue("SabPassword", value); } - } - - public String SabTvCategory - { - get { return GetValue("SabTvCategory", "tv"); } - - set { SetValue("SabTvCategory", value); } - } - - public SabPriorityType SabRecentTvPriority - { - get { return GetValueEnum("SabRecentTvPriority", SabPriorityType.Default); } - - set { SetValue("SabRecentTvPriority", value); } - } - - public SabPriorityType SabOlderTvPriority - { - get { return GetValueEnum("SabOlderTvPriority", SabPriorityType.Default); } - - set { SetValue("SabOlderTvPriority", value); } - } - - public bool SabUseSsl - { - get { return GetValueBoolean("SabUseSsl", false); } - - set { SetValue("SabUseSsl", value); } - } - public String DownloadedEpisodesFolder { get { return GetValue(ConfigKey.DownloadedEpisodesFolder.ToString()); } @@ -155,80 +92,12 @@ namespace NzbDrone.Core.Configuration set { SetValue("Retention", value); } } - public DownloadClientType DownloadClient - { - get { return GetValueEnum("DownloadClient", DownloadClientType.Blackhole); } - - set { SetValue("DownloadClient", value); } - } - - public string BlackholeFolder - { - get { return GetValue("BlackholeFolder", String.Empty); } - set { SetValue("BlackholeFolder", value); } - } - - public string PneumaticFolder - { - get { return GetValue("PneumaticFolder", String.Empty); } - set { SetValue("PneumaticFolder", value); } - } - public string RecycleBin { get { return GetValue("RecycleBin", String.Empty); } set { SetValue("RecycleBin", value); } } - public String NzbgetUsername - { - get { return GetValue("NzbgetUsername", "nzbget"); } - - set { SetValue("NzbgetUsername", value); } - } - - public String NzbgetPassword - { - get { return GetValue("NzbgetPassword", ""); } - - set { SetValue("NzbgetPassword", value); } - } - - public String NzbgetHost - { - get { return GetValue("NzbgetHost", "localhost"); } - - set { SetValue("NzbgetHost", value); } - } - - public Int32 NzbgetPort - { - get { return GetValueInt("NzbgetPort", 6789); } - - set { SetValue("NzbgetPort", value); } - } - - public String NzbgetTvCategory - { - get { return GetValue("NzbgetTvCategory", ""); } - - set { SetValue("NzbgetTvCategory", value); } - } - - public PriorityType NzbgetRecentTvPriority - { - get { return GetValueEnum("NzbgetRecentTvPriority", PriorityType.Normal); } - - set { SetValue("NzbgetRecentTvPriority", value); } - } - - public PriorityType NzbgetOlderTvPriority - { - get { return GetValueEnum("NzbgetOlderTvPriority", PriorityType.Normal); } - - set { SetValue("NzbgetOlderTvPriority", value); } - } - public string ReleaseRestrictions { get { return GetValue("ReleaseRestrictions", String.Empty).Trim('\r', '\n'); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 6c19d6c36..ca44cc046 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients.Nzbget; -using NzbDrone.Core.Download.Clients.Sabnzbd; namespace NzbDrone.Core.Configuration { @@ -10,42 +7,33 @@ namespace NzbDrone.Core.Configuration { IEnumerable All(); Dictionary AllWithDefaults(); - String SabHost { get; set; } - int SabPort { get; set; } - String SabApiKey { get; set; } - String SabUsername { get; set; } - String SabPassword { get; set; } - String SabTvCategory { get; set; } - SabPriorityType SabRecentTvPriority { get; set; } - SabPriorityType SabOlderTvPriority { get; set; } - Boolean SabUseSsl { get; set; } + void SaveConfigDictionary(Dictionary configValues); + + //Download Client String DownloadedEpisodesFolder { get; set; } - bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } - int Retention { get; set; } - DownloadClientType DownloadClient { get; set; } - string BlackholeFolder { get; set; } - string PneumaticFolder { get; set; } - string RecycleBin { get; set; } - String NzbgetUsername { get; set; } - String NzbgetPassword { get; set; } - String NzbgetHost { get; set; } - Int32 NzbgetPort { get; set; } - String NzbgetTvCategory { get; set; } - PriorityType NzbgetRecentTvPriority { get; set; } - PriorityType NzbgetOlderTvPriority { get; set; } - string ReleaseRestrictions { get; set; } - Int32 RssSyncInterval { get; set; } - Boolean AutoDownloadPropers { get; set; } String DownloadClientWorkingFolders { get; set; } + + //Failed Download Handling (Download client) Boolean AutoRedownloadFailed { get; set; } Boolean RemoveFailedDownloads { get; set; } Boolean EnableFailedDownloadHandling { get; set; } + + //Media Management + Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } + String RecycleBin { get; set; } + Boolean AutoDownloadPropers { get; set; } Boolean CreateEmptySeriesFolders { get; set; } - void SaveValues(Dictionary configValues); + + //Permissions (Media Management) Boolean SetPermissionsLinux { get; set; } String FileChmod { get; set; } String FolderChmod { get; set; } String ChownUser { get; set; } String ChownGroup { get; set; } + + //Indexers + Int32 Retention { get; set; } + Int32 RssSyncInterval { get; set; } + String ReleaseRestrictions { get; set; } } } diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs index b7165a7f0..fc0146c7b 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Web.UI.WebControls; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Core.Messaging.Events; diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index f4125c0f2..74354e28f 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Datastore public TModel Get(int id) { - var model = DataMapper.Query().SingleOrDefault(c => c.Id == id); + var model = Query.Where(c => c.Id == id).SingleOrDefault(); if (model == null) { @@ -142,23 +142,44 @@ namespace NzbDrone.Core.Datastore public void InsertMany(IList models) { - foreach (var model in models) + using (var unitOfWork = new UnitOfWork(() => DataMapper)) { - Insert(model); + unitOfWork.BeginTransaction(); + + foreach (var model in models) + { + unitOfWork.DB.Insert(model); + } + + unitOfWork.Commit(); } } public void UpdateMany(IList models) { - foreach (var model in models) + using (var unitOfWork = new UnitOfWork(() => DataMapper)) { - Update(model); + unitOfWork.BeginTransaction(); + + foreach (var model in models) + { + var localModel = model; + + if (model.Id == 0) + { + throw new InvalidOperationException("Can't update model with ID 0"); + } + + unitOfWork.DB.Update(model, c => c.Id == localModel.Id); + } + + unitOfWork.Commit(); } } public void DeleteMany(List models) { - models.ForEach(Delete); + DeleteMany(models.Select(m => m.Id)); } public TModel Upsert(TModel model) @@ -179,7 +200,19 @@ namespace NzbDrone.Core.Datastore public void DeleteMany(IEnumerable ids) { - ids.ToList().ForEach(Delete); + using (var unitOfWork = new UnitOfWork(() => DataMapper)) + { + unitOfWork.BeginTransaction(); + + foreach (var id in ids) + { + var localId = id; + + unitOfWork.DB.Delete(c => c.Id == localId); + } + + unitOfWork.Commit(); + } } public void Purge() diff --git a/src/NzbDrone.Core/Datastore/Migration/042_add_download_clients_table.cs b/src/NzbDrone.Core/Datastore/Migration/042_add_download_clients_table.cs new file mode 100644 index 000000000..08cf7622b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/042_add_download_clients_table.cs @@ -0,0 +1,20 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(42)] + public class add_download_clients_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("DownloadClients") + .WithColumn("Enable").AsBoolean().NotNullable() + .WithColumn("Name").AsString().NotNullable() + .WithColumn("Implementation").AsString().NotNullable() + .WithColumn("Settings").AsString().NotNullable() + .WithColumn("ConfigContract").AsString().NotNullable() + .WithColumn("Protocol").AsInt32().NotNullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/043_convert_config_to_download_clients.cs b/src/NzbDrone.Core/Datastore/Migration/043_convert_config_to_download_clients.cs new file mode 100644 index 000000000..c83e3e20b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/043_convert_config_to_download_clients.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Data; +using FluentMigrator; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(43)] + public class convert_config_to_download_clients : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertToThingyProvder); + } + + private void ConvertToThingyProvder(IDbConnection conn, IDbTransaction tran) + { + var config = new Dictionary(); + + using (IDbCommand configCmd = conn.CreateCommand()) + { + configCmd.Transaction = tran; + configCmd.CommandText = @"SELECT * FROM Config"; + using (IDataReader configReader = configCmd.ExecuteReader()) + { + var keyIndex = configReader.GetOrdinal("Key"); + var valueIndex = configReader.GetOrdinal("Value"); + + while (configReader.Read()) + { + var key = configReader.GetString(keyIndex); + var value = configReader.GetString(valueIndex); + + config.Add(key.ToLowerInvariant(), value); + } + } + } + + var client = GetConfigValue(config, "DownloadClient", ""); + + if (String.IsNullOrWhiteSpace(client)) + { + return; + } + + if (client.Equals("sabnzbd", StringComparison.InvariantCultureIgnoreCase)) + { + var settings = new ClientSettingsForMigration + { + Host = GetConfigValue(config, "SabHost", "localhost"), + Port = GetConfigValue(config, "SabPort", 8080), + ApiKey = GetConfigValue(config, "SabApiKey", ""), + Username = GetConfigValue(config, "SabUsername", ""), + Password = GetConfigValue(config, "SabPassword", ""), + TvCategory = GetConfigValue(config, "SabTvCategory", "tv"), + RecentTvPriority = GetSabnzbdPriority(GetConfigValue(config, "NzbgetRecentTvPriority", "Default")), + OlderTvPriority = GetSabnzbdPriority(GetConfigValue(config, "NzbgetOlderTvPriority", "Default")), + UseSsl = GetConfigValue(config, "SabUseSsl", false) + }; + + AddDownloadClient(conn, tran, "Sabnzbd", "Sabnzbd", settings.ToJson(), "SabnzbdSettings", 1); + } + + else if (client.Equals("nzbget", StringComparison.InvariantCultureIgnoreCase)) + { + var settings = new ClientSettingsForMigration + { + Host = GetConfigValue(config, "NzbGetHost", "localhost"), + Port = GetConfigValue(config, "NzbgetPort", 6789), + Username = GetConfigValue(config, "NzbgetUsername", "nzbget"), + Password = GetConfigValue(config, "NzbgetPassword", ""), + TvCategory = GetConfigValue(config, "NzbgetTvCategory", "tv"), + RecentTvPriority = GetNzbgetPriority(GetConfigValue(config, "NzbgetRecentTvPriority", "Normal")), + OlderTvPriority = GetNzbgetPriority(GetConfigValue(config, "NzbgetOlderTvPriority", "Normal")), + }; + + AddDownloadClient(conn, tran, "Nzbget", "Nzbget", settings.ToJson(), "NzbgetSettings", 1); + } + + else if (client.Equals("pneumatic", StringComparison.InvariantCultureIgnoreCase)) + { + var settings = new FolderSettingsForMigration + { + Folder = GetConfigValue(config, "PneumaticFolder", "") + }; + + AddDownloadClient(conn, tran, "Pneumatic", "Pneumatic", settings.ToJson(), "FolderSettings", 1); + } + + else if (client.Equals("blackhole", StringComparison.InvariantCultureIgnoreCase)) + { + var settings = new FolderSettingsForMigration + { + Folder = GetConfigValue(config, "BlackholeFolder", "") + }; + + AddDownloadClient(conn, tran, "Blackhole", "Blackhole", settings.ToJson(), "FolderSettings", 1); + } + + DeleteOldConfigValues(conn, tran); + } + + private T GetConfigValue(Dictionary config, string key, T defaultValue) + { + key = key.ToLowerInvariant(); + + if (config.ContainsKey(key)) + { + return (T) Convert.ChangeType(config[key], typeof (T)); + } + + return defaultValue; + } + + private void AddDownloadClient(IDbConnection conn, IDbTransaction tran, string name, string implementation, string settings, + string configContract, int protocol) + { + using (IDbCommand updateCmd = conn.CreateCommand()) + { + var text = String.Format("INSERT INTO DownloadClients (Enable, Name, Implementation, Settings, ConfigContract, Protocol) VALUES (1, ?, ?, ?, ?, ?)"); + updateCmd.AddParameter(name); + updateCmd.AddParameter(implementation); + updateCmd.AddParameter(settings); + updateCmd.AddParameter(configContract); + updateCmd.AddParameter(protocol); + + updateCmd.Transaction = tran; + updateCmd.CommandText = text; + updateCmd.ExecuteNonQuery(); + } + } + + private void DeleteOldConfigValues(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand updateCmd = conn.CreateCommand()) + { + var text = "DELETE FROM Config WHERE [KEY] IN ('nzbgetusername', 'nzbgetpassword', 'nzbgethost', 'nzbgetport', " + + "'nzbgettvcategory', 'nzbgetrecenttvpriority', 'nzbgetoldertvpriority', 'sabhost', 'sabport', " + + "'sabapikey', 'sabusername', 'sabpassword', 'sabtvcategory', 'sabrecenttvpriority', " + + "'saboldertvpriority', 'sabusessl', 'downloadclient', 'blackholefolder', 'pneumaticfolder')"; + + updateCmd.Transaction = tran; + updateCmd.CommandText = text; + updateCmd.ExecuteNonQuery(); + } + } + + private int GetSabnzbdPriority(string priority) + { + return (int)Enum.Parse(typeof(SabnzbdPriorityForMigration), priority, true); + } + + private int GetNzbgetPriority(string priority) + { + return (int)Enum.Parse(typeof(NzbGetPriorityForMigration), priority, true); + } + + private class ClientSettingsForMigration + { + public String Host { get; set; } + public Int32 Port { get; set; } + public String ApiKey { get; set; } + public String Username { get; set; } + public String Password { get; set; } + public String TvCategory { get; set; } + public Int32 RecentTvPriority { get; set; } + public Int32 OlderTvPriority { get; set; } + public Boolean UseSsl { get; set; } + } + + private class FolderSettingsForMigration + { + public String Folder { get; set; } + } + + private enum SabnzbdPriorityForMigration + { + Default = -100, + Paused = -2, + Low = -1, + Normal = 0, + High = 1, + Force = 2 + } + + private enum NzbGetPriorityForMigration + { + VeryLow = -100, + Low = -50, + Normal = 0, + High = 50, + VeryHigh = 100 + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/044_fix_xbmc_episode_metadata.cs b/src/NzbDrone.Core/Datastore/Migration/044_fix_xbmc_episode_metadata.cs new file mode 100644 index 000000000..0c645259b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/044_fix_xbmc_episode_metadata.cs @@ -0,0 +1,27 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(44)] + public class fix_xbmc_episode_metadata : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + //Convert Episode Metadata to proper type + Execute.Sql("UPDATE MetadataFiles " + + "SET Type = 2 " + + "WHERE Consumer = 'XbmcMetadata' " + + "AND EpisodeFileId IS NOT NULL " + + "AND Type = 4 " + + "AND RelativePath LIKE '%.nfo'"); + + //Convert Episode Images to proper type + Execute.Sql("UPDATE MetadataFiles " + + "SET Type = 5 " + + "WHERE Consumer = 'XbmcMetadata' " + + "AND EpisodeFileId IS NOT NULL " + + "AND Type = 4"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index b5711cd18..78c6fa0fb 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; @@ -39,6 +40,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("ScheduledTasks"); Mapper.Entity().RegisterModel("Notifications"); Mapper.Entity().RegisterModel("Metadata"); + Mapper.Entity().RegisterModel("DownloadClients"); Mapper.Entity().RegisterModel("SceneMappings"); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 32e7087c6..bae6f3d60 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.DecisionEngine private readonly IParsingService _parsingService; private readonly Logger _logger; - public DownloadDecisionMaker(IEnumerable specifications, IParsingService parsingService, Logger logger) + public DownloadDecisionMaker(IEnumerable specifications, IParsingService parsingService, Logger logger) { _specifications = specifications; _parsingService = parsingService; @@ -63,6 +63,15 @@ namespace NzbDrone.Core.DecisionEngine { var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title); + if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode()) + { + var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(report.Title, report.TvRageId, searchCriteria); + if (specialEpisodeInfo != null) + { + parsedEpisodeInfo = specialEpisodeInfo; + } + } + if (parsedEpisodeInfo != null && !string.IsNullOrWhiteSpace(parsedEpisodeInfo.SeriesTitle)) { var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvRageId, searchCriteria); @@ -91,13 +100,12 @@ namespace NzbDrone.Core.DecisionEngine yield return decision; } } - } private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) { var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) - .Where(c => !string.IsNullOrWhiteSpace(c)); + .Where(c => !string.IsNullOrWhiteSpace(c)); return new DownloadDecision(remoteEpisode, reasons.ToArray()); } diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs index 3330be7ab..aae2a6d6b 100644 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs @@ -1,6 +1,5 @@ using NLog; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index e63edaa8c..8cd175703 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -27,7 +27,6 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { - _logger.Trace("Beginning size check for: {0}", subject); var quality = subject.ParsedEpisodeInfo.Quality.Quality; @@ -45,34 +44,43 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } var qualityDefinition = _qualityDefinitionService.Get(quality); + var minSize = qualityDefinition.MinSize.Megabytes(); + //Multiply maxSize by Series.Runtime + minSize = minSize * subject.Series.Runtime * subject.Episodes.Count; + + //If the parsed size is smaller than minSize we don't want it + if (subject.Release.Size < minSize) + { + _logger.Trace("Item: {0}, Size: {1} is smaller than minimum allowed size ({2}), rejecting.", subject, subject.Release.Size, minSize); + return false; + } if (qualityDefinition.MaxSize == 0) { _logger.Trace("Max size is 0 (unlimited) - skipping check."); - return true; } - - var maxSize = qualityDefinition.MaxSize.Megabytes(); - - //Multiply maxSize by Series.Runtime - maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count; - - //Check if there was only one episode parsed and it is the first - if (subject.Episodes.Count == 1 && subject.Episodes.First().EpisodeNumber == 1) + else { - maxSize = maxSize * 2; - } + var maxSize = qualityDefinition.MaxSize.Megabytes(); - //If the parsed size is greater than maxSize we don't want it - if (subject.Release.Size > maxSize) - { - _logger.Trace("Item: {0}, Size: {1} is greater than maximum allowed size ({2}), rejecting.", subject, subject.Release.Size, maxSize); - return false; - } + //Multiply maxSize by Series.Runtime + maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count; + //Check if there was only one episode parsed and it is the first + if (subject.Episodes.Count == 1 && _episodeService.IsFirstOrLastEpisodeOfSeason(subject.Episodes.First().Id)) + { + maxSize = maxSize * 2; + } + + //If the parsed size is greater than maxSize we don't want it + if (subject.Release.Size > maxSize) + { + _logger.Trace("Item: {0}, Size: {1} is greater than maximum allowed size ({2}), rejecting.", subject, subject.Release.Size, maxSize); + return false; + } + } _logger.Trace("Item: {0}, meets size constraints.", subject); return true; } - } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index fa6312258..7e4f34551 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -5,7 +5,6 @@ using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -32,9 +31,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var downloadClient = _downloadClientProvider.GetDownloadClient(); - if (!downloadClient.IsConfigured) + if (downloadClient == null) { - _logger.Warn("Download client {0} isn't configured yet.", downloadClient.GetType().Name); + _logger.Warn("Download client isn't configured yet."); return true; } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index a2f4dee15..c74289e55 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -28,9 +28,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var age = subject.Release.Age; + var retention = _configService.Retention; _logger.Trace("Checking if report meets retention requirements. {0}", age); - if (_configService.Retention > 0 && age > _configService.Retention) + if (retention > 0 && age > retention) { _logger.Trace("Report age: {0} rejected by user's retention limit", age); return false; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index b2e034cac..1624c6296 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return true; } - if (_downloadClientProvider.GetDownloadClient().GetType() == typeof (SabnzbdClient)) + if (_downloadClientProvider.GetDownloadClient().GetType() == typeof (Sabnzbd)) { _logger.Trace("Performing history status check on report"); foreach (var episode in subject.Episodes) diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs new file mode 100644 index 000000000..8b5ff0242 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download.Clients.Blackhole +{ + public class Blackhole : DownloadClientBase, IExecute + { + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly Logger _logger; + + public Blackhole(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) + { + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _logger = logger; + } + + public override string DownloadNzb(RemoteEpisode remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + + title = FileNameBuilder.CleanFilename(title); + + var filename = Path.Combine(Settings.Folder, title + ".nzb"); + + + _logger.Trace("Downloading NZB from: {0} to: {1}", url, filename); + _httpProvider.DownloadFile(url, filename); + _logger.Trace("NZB Download succeeded, saved to: {0}", filename); + + return null; + } + + public override IEnumerable GetQueue() + { + return new QueueItem[0]; + } + + public override IEnumerable GetHistory(int start = 0, int limit = 10) + { + return new HistoryItem[0]; + } + + public override void RemoveFromQueue(string id) + { + } + + public override void RemoveFromHistory(string id) + { + } + + public void Execute(TestBlackholeCommand message) + { + var testPath = Path.Combine(message.Folder, "drone_test.txt"); + _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); + _diskProvider.DeleteFile(testPath); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs new file mode 100644 index 000000000..10898f80a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download.Clients.Blackhole +{ + public class TestBlackholeCommand : Command + { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public String Folder { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs deleted file mode 100644 index 1f5a5c93d..000000000 --- a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using NLog; -using NzbDrone.Common; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Clients -{ - public class BlackholeProvider : IDownloadClient - { - private readonly IConfigService _configService; - private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; - - - public BlackholeProvider(IConfigService configService, IHttpProvider httpProvider, Logger logger) - { - _configService = configService; - _httpProvider = httpProvider; - _logger = logger; - } - - public string DownloadNzb(RemoteEpisode remoteEpisode) - { - var url = remoteEpisode.Release.DownloadUrl; - var title = remoteEpisode.Release.Title; - - title = FileNameBuilder.CleanFilename(title); - - var filename = Path.Combine(_configService.BlackholeFolder, title + ".nzb"); - - - _logger.Trace("Downloading NZB from: {0} to: {1}", url, filename); - _httpProvider.DownloadFile(url, filename); - _logger.Trace("NZB Download succeeded, saved to: {0}", filename); - - return null; - } - - public bool IsConfigured - { - get - { - return !string.IsNullOrWhiteSpace(_configService.BlackholeFolder); - } - } - - public IEnumerable GetQueue() - { - return new QueueItem[0]; - } - - public IEnumerable GetHistory(int start = 0, int limit = 0) - { - return new HistoryItem[0]; - } - - public void RemoveFromQueue(string id) - { - } - - public void RemoveFromHistory(string id) - { - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/FolderSettings.cs b/src/NzbDrone.Core/Download/Clients/FolderSettings.cs new file mode 100644 index 000000000..cacb847ea --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/FolderSettings.cs @@ -0,0 +1,32 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Core.Download.Clients +{ + public class FolderSettingsValidator : AbstractValidator + { + public FolderSettingsValidator() + { + //Todo: Validate that the path actually exists + RuleFor(c => c.Folder).IsValidPath(); + } + } + + public class FolderSettings : IProviderConfig + { + private static readonly FolderSettingsValidator Validator = new FolderSettingsValidator(); + + [FieldDefinition(0, Label = "Folder", Type = FieldType.Path)] + public String Folder { get; set; } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs index 86cd09843..f7ec8a1be 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs @@ -4,11 +4,11 @@ using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Nzbget { - public class NzbGetQueue + public class NzbgetQueue { public String Version { get; set; } [JsonProperty(PropertyName = "result")] - public List QueueItems { get; set; } + public List QueueItems { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs index 8e2de535d..39bc8eb51 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs @@ -2,14 +2,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { - public class NzbGetQueueItem + public class NzbgetQueueItem { private string _nzbName; - public Int32 NzbId { get; set; } - public string NzbName { get; set; } - public String Category { get; set; } public Int32 FileSizeMb { get; set; } public Int32 RemainingSizeMb { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs new file mode 100644 index 000000000..aa172e449 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using Omu.ValueInjecter; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class Nzbget : DownloadClientBase, IExecute + { + private readonly INzbgetProxy _proxy; + private readonly IParsingService _parsingService; + private readonly Logger _logger; + + public Nzbget(INzbgetProxy proxy, + IParsingService parsingService, + Logger logger) + { + _proxy = proxy; + _parsingService = parsingService; + _logger = logger; + } + + public override string DownloadNzb(RemoteEpisode remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title + ".nzb"; + + string cat = Settings.TvCategory; + int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; + + _logger.Info("Adding report [{0}] to the queue.", title); + + var success = _proxy.AddNzb(Settings, title, cat, priority, false, url); + + _logger.Debug("Queue Response: [{0}]", success); + + return null; + } + + public override IEnumerable GetQueue() + { + var items = _proxy.GetQueue(Settings); + + foreach (var nzbGetQueueItem in items) + { + var queueItem = new QueueItem(); + queueItem.Id = nzbGetQueueItem.NzbId.ToString(); + queueItem.Title = nzbGetQueueItem.NzbName; + queueItem.Size = nzbGetQueueItem.FileSizeMb; + queueItem.Sizeleft = nzbGetQueueItem.RemainingSizeMb; + queueItem.Status = nzbGetQueueItem.FileSizeMb == nzbGetQueueItem.PausedSizeMb ? "paused" : "queued"; + + var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); + if (parsedEpisodeInfo == null) continue; + + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); + if (remoteEpisode.Series == null) continue; + + queueItem.RemoteEpisode = remoteEpisode; + + yield return queueItem; + } + } + + public override IEnumerable GetHistory(int start = 0, int limit = 10) + { + return new HistoryItem[0]; + } + + public override void RemoveFromQueue(string id) + { + throw new NotImplementedException(); + } + + public override void RemoveFromHistory(string id) + { + throw new NotImplementedException(); + } + + public VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null) + { + return _proxy.GetVersion(Settings); + } + + public void Execute(TestNzbgetCommand message) + { + var settings = new NzbgetSettings(); + settings.InjectFrom(message); + + _proxy.GetVersion(settings); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs deleted file mode 100644 index 5431d0355..000000000 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using NLog; -using NzbDrone.Common; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Clients.Nzbget -{ - public class NzbgetClient : IDownloadClient - { - private readonly IConfigService _configService; - private readonly IHttpProvider _httpProvider; - private readonly INzbGetCommunicationProxy _proxy; - private readonly IParsingService _parsingService; - private readonly Logger _logger; - - public NzbgetClient(IConfigService configService, - IHttpProvider httpProvider, - INzbGetCommunicationProxy proxy, - IParsingService parsingService, - Logger logger) - { - _configService = configService; - _httpProvider = httpProvider; - _proxy = proxy; - _parsingService = parsingService; - _logger = logger; - } - - public string DownloadNzb(RemoteEpisode remoteEpisode) - { - var url = remoteEpisode.Release.DownloadUrl; - var title = remoteEpisode.Release.Title + ".nzb"; - - string cat = _configService.NzbgetTvCategory; - int priority = remoteEpisode.IsRecentEpisode() ? (int)_configService.NzbgetRecentTvPriority : (int)_configService.NzbgetOlderTvPriority; - - _logger.Info("Adding report [{0}] to the queue.", title); - - var success = _proxy.AddNzb(title, cat, priority, false, url); - - _logger.Debug("Queue Response: [{0}]", success); - - return null; - } - - public bool IsConfigured - { - get - { - return !string.IsNullOrWhiteSpace(_configService.NzbgetHost) && _configService.NzbgetPort != 0; - } - } - - public virtual IEnumerable GetQueue() - { - var items = _proxy.GetQueue(); - - foreach (var nzbGetQueueItem in items) - { - var queueItem = new QueueItem(); - queueItem.Id = nzbGetQueueItem.NzbId.ToString(); - queueItem.Title = nzbGetQueueItem.NzbName; - queueItem.Size = nzbGetQueueItem.FileSizeMb; - queueItem.Sizeleft = nzbGetQueueItem.RemainingSizeMb; - queueItem.Status = nzbGetQueueItem.FileSizeMb == nzbGetQueueItem.PausedSizeMb ? "paused" : "queued"; - - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); - if (parsedEpisodeInfo == null) continue; - - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) continue; - - queueItem.RemoteEpisode = remoteEpisode; - - yield return queueItem; - } - } - - public IEnumerable GetHistory(int start = 0, int limit = 0) - { - return new HistoryItem[0]; - } - - public void RemoveFromQueue(string id) - { - throw new NotImplementedException(); - } - - public void RemoveFromHistory(string id) - { - throw new NotImplementedException(); - } - - public virtual VersionModel GetVersion(string host = null, int port = 0, string username = null, string password = null) - { - throw new NotImplementedException(); - - //Get saved values if any of these are defaults - if (host == null) - host = _configService.NzbgetHost; - - if (port == 0) - port = _configService.NzbgetPort; - - if (username == null) - username = _configService.NzbgetUsername; - - if (password == null) - password = _configService.NzbgetPassword; - - - var response = _proxy.GetVersion(); - - return Json.Deserialize(response); - } - - public virtual string Test(string host, int port, string username, string password) - { - try - { - var version = GetVersion(host, port, username, password); - return version.Result; - } - catch (Exception ex) - { - _logger.DebugException("Failed to Test Nzbget", ex); - } - - return String.Empty; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/PriorityType.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPriority.cs similarity index 84% rename from src/NzbDrone.Core/Download/Clients/Nzbget/PriorityType.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPriority.cs index 7235f375a..c7e121805 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/PriorityType.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPriority.cs @@ -1,6 +1,6 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { - public enum PriorityType + public enum NzbgetPriority { VeryLow = -100, Low = -50, diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetCommunicationProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs similarity index 60% rename from src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetCommunicationProxy.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 1b733ad69..0376dfad9 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetCommunicationProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -1,57 +1,52 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using NLog; using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Rest; using RestSharp; namespace NzbDrone.Core.Download.Clients.Nzbget { - public interface INzbGetCommunicationProxy + public interface INzbgetProxy { - bool AddNzb(params object[] parameters); - List GetQueue(); - string GetVersion(); + bool AddNzb(NzbgetSettings settings, params object[] parameters); + List GetQueue(NzbgetSettings settings); + VersionResponse GetVersion(NzbgetSettings settings); } - public class NzbGetCommunicationProxy : INzbGetCommunicationProxy + public class NzbgetProxy : INzbgetProxy { - private readonly IConfigService _configService; private readonly Logger _logger; - public NzbGetCommunicationProxy(IConfigService configService, Logger logger) + public NzbgetProxy(Logger logger) { - _configService = configService; _logger = logger; } - public bool AddNzb(params object[] parameters) + public bool AddNzb(NzbgetSettings settings, params object[] parameters) { var request = BuildRequest(new JsonRequest("appendurl", parameters)); - return Json.Deserialize(ProcessRequest(request)).Result; + return Json.Deserialize(ProcessRequest(request, settings)).Result; } - public List GetQueue() + public List GetQueue(NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("listgroups")); + var request = BuildRequest(new JsonRequest("listgroups")); - return Json.Deserialize(ProcessRequest(request)).QueueItems; + return Json.Deserialize(ProcessRequest(request, settings)).QueueItems; } - public string GetVersion() + public VersionResponse GetVersion(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("version")); - return ProcessRequest(request); + return Json.Deserialize(ProcessRequest(request, settings)); } - private string ProcessRequest(IRestRequest restRequest) + private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings) { - var client = BuildClient(); + var client = BuildClient(settings); var response = client.Execute(restRequest); _logger.Trace("Response: {0}", response.Content); @@ -60,14 +55,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return response.Content; } - private IRestClient BuildClient() + private IRestClient BuildClient(NzbgetSettings settings) { var url = String.Format("http://{0}:{1}/jsonrpc", - _configService.NzbgetHost, - _configService.NzbgetPort); + settings.Host, + settings.Port); var client = new RestClient(url); - client.Authenticator = new HttpBasicAuthenticator(_configService.NzbgetUsername, _configService.NzbgetPassword); + client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); return client; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs new file mode 100644 index 000000000..383622cef --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -0,0 +1,59 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetSettingsValidator : AbstractValidator + { + public NzbgetSettingsValidator() + { + RuleFor(c => c.Host).NotEmpty(); + RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Username).NotEmpty(); + RuleFor(c => c.Password).NotEmpty(); + } + } + + public class NzbgetSettings : IProviderConfig + { + private static readonly NzbgetSettingsValidator Validator = new NzbgetSettingsValidator(); + + public NzbgetSettings() + { + Host = "localhost"; + Port = 6789; + TvCategory = "tv"; + RecentTvPriority = (int)NzbgetPriority.Normal; + OlderTvPriority = (int)NzbgetPriority.Normal; + } + + [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] + public String Host { get; set; } + + [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] + public Int32 Port { get; set; } + + [FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)] + public String Username { get; set; } + + [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] + public String Password { get; set; } + + [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)] + public String TvCategory { get; set; } + + [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority))] + public Int32 RecentTvPriority { get; set; } + + [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority))] + public Int32 OlderTvPriority { get; set; } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs new file mode 100644 index 000000000..805b4d19a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs @@ -0,0 +1,21 @@ +using System; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class TestNzbgetCommand : Command + { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public String Host { get; set; } + public Int32 Port { get; set; } + public String Username { get; set; } + public String Password { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/VersionModel.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs similarity index 83% rename from src/NzbDrone.Core/Download/Clients/Nzbget/VersionModel.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs index 9e7d90064..780fd90ad 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/VersionModel.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { - public class VersionModel + public class VersionResponse { public String Version { get; set; } public String Result { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs similarity index 63% rename from src/NzbDrone.Core/Download/Clients/PneumaticClient.cs rename to src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 8ebc5b409..3c875b9c8 100644 --- a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -6,12 +6,13 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; -namespace NzbDrone.Core.Download.Clients +namespace NzbDrone.Core.Download.Clients.Pneumatic { - public class PneumaticClient : IDownloadClient + public class Pneumatic : DownloadClientBase, IExecute { private readonly IConfigService _configService; private readonly IHttpProvider _httpProvider; @@ -19,7 +20,7 @@ namespace NzbDrone.Core.Download.Clients private static readonly Logger logger = NzbDroneLogger.GetLogger(); - public PneumaticClient(IConfigService configService, IHttpProvider httpProvider, + public Pneumatic(IConfigService configService, IHttpProvider httpProvider, IDiskProvider diskProvider) { _configService = configService; @@ -27,20 +28,20 @@ namespace NzbDrone.Core.Download.Clients _diskProvider = diskProvider; } - public string DownloadNzb(RemoteEpisode remoteEpisode) + public override string DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; if (remoteEpisode.ParsedEpisodeInfo.FullSeason) { - throw new NotImplementedException("Full season Pneumatic releases are not supported."); + throw new NotImplementedException("Full season releases are not supported with Pneumatic."); } title = FileNameBuilder.CleanFilename(title); //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) - var filename = Path.Combine(_configService.PneumaticFolder, title + ".nzb"); + var filename = Path.Combine(Settings.Folder, title + ".nzb"); logger.Trace("Downloading NZB from: {0} to: {1}", url, filename); _httpProvider.DownloadFile(url, filename); @@ -57,31 +58,33 @@ namespace NzbDrone.Core.Download.Clients { get { - return !string.IsNullOrWhiteSpace(_configService.PneumaticFolder); + return !string.IsNullOrWhiteSpace(Settings.Folder); } } - public IEnumerable GetQueue() + public override IEnumerable GetQueue() { return new QueueItem[0]; } - public IEnumerable GetHistory(int start = 0, int limit = 0) + public override IEnumerable GetHistory(int start = 0, int limit = 10) { return new HistoryItem[0]; } - public void RemoveFromQueue(string id) + public override void RemoveFromQueue(string id) { } - public void RemoveFromHistory(string id) + public override void RemoveFromHistory(string id) { } - public virtual bool IsInQueue(RemoteEpisode newEpisode) + public void Execute(TestPneumaticCommand message) { - return false; + var testPath = Path.Combine(message.Folder, "drone_test.txt"); + _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); + _diskProvider.DeleteFile(testPath); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/TestPneumaticCommand.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/TestPneumaticCommand.cs new file mode 100644 index 000000000..097a39aa5 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/TestPneumaticCommand.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download.Clients.Pneumatic +{ + public class TestPneumaticCommand : Command + { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public String Folder { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/ConnectionInfoModel.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/ConnectionInfoModel.cs deleted file mode 100644 index cb03c1953..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/ConnectionInfoModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class ConnectionInfoModel - { - public string Address { get; set; } - public int Port { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs index e5c5f8b74..17557abcc 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var priorityType = (SabPriorityType)value; + var priorityType = (SabnzbdPriority)value; writer.WriteValue(priorityType.ToString()); } @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters { var queuePriority = reader.Value.ToString(); - SabPriorityType output; + SabnzbdPriority output; Enum.TryParse(queuePriority, out output); return output; @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters public override bool CanConvert(Type objectType) { - return objectType == typeof(SabPriorityType); + return objectType == typeof(SabnzbdPriority); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdAddResponse.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdAddResponse.cs new file mode 100644 index 000000000..147bfce68 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdAddResponse.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd.Responses +{ + public class SabnzbdAddResponse + { + public SabnzbdAddResponse() + { + Ids = new List(); + } + + public bool Status { get; set; } + + [JsonProperty(PropertyName = "nzo_ids")] + public List Ids { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdCategoryResponse.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdCategoryResponse.cs new file mode 100644 index 000000000..03d71bee5 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdCategoryResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd.Responses +{ + public class SabnzbdCategoryResponse + { + public SabnzbdCategoryResponse() + { + Categories = new List(); + } + + public List Categories { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdVersionResponse.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdVersionResponse.cs new file mode 100644 index 000000000..fd281a58f --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Responses/SabnzbdVersionResponse.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Download.Clients.Sabnzbd.Responses +{ + public class SabnzbdVersionResponse + { + public string Version { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabAddResponse.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabAddResponse.cs deleted file mode 100644 index 040b2b2b4..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabAddResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class SabAddResponse - { - public SabAddResponse() - { - Ids = new List(); - } - - public bool Status { get; set; } - - [JsonProperty(PropertyName = "nzo_ids")] - public List Ids { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabAutoConfigureService.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabAutoConfigureService.cs deleted file mode 100644 index 224cae5c5..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabAutoConfigureService.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.Instrumentation; - -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class SabAutoConfigureService - { - private static readonly Logger Logger = NzbDroneLogger.GetLogger(); - - public SabModel AutoConfigureSab() - { - var info = GetConnectionList(); - return FindApiKey(info); - } - - private List GetConnectionList() - { - IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); - var info = - ipProperties.GetActiveTcpListeners().Select( - p => - new ConnectionInfoModel { Address = p.Address.ToString().Replace("0.0.0.0", "127.0.0.1"), Port = p.Port }).Distinct(). - ToList(); - - info.RemoveAll(i => i.Port == 135); - info.RemoveAll(i => i.Port == 139); - info.RemoveAll(i => i.Port == 445); - info.RemoveAll(i => i.Port == 3389); - info.RemoveAll(i => i.Port == 5900); - info.RemoveAll(i => i.Address.Contains("::")); - - info.Reverse(); - - return info; - } - - private SabModel FindApiKey(List info) - { - foreach (var connection in info) - { - var apiKey = GetApiKey(connection.Address, connection.Port); - if (!String.IsNullOrEmpty(apiKey)) - return new SabModel - { - Host = connection.Address, - Port = connection.Port, - ApiKey = apiKey - }; - } - return null; - } - - private string GetApiKey(string ipAddress, int port) - { - var request = String.Format("http://{0}:{1}/config/general/", ipAddress, port); - var result = DownloadString(request); - - Regex regex = - new Regex("\\\\w+)\\W", - RegexOptions.IgnoreCase - | RegexOptions.Compiled); - var match = regex.Match(result); - - if (match.Success) - { - return match.Groups["apikey"].Value; - } - - return String.Empty; - } - - private string DownloadString(string url) - { - try - { - var request = WebRequest.Create(url); - request.Timeout = 2000; - - var response = request.GetResponse(); - - var reader = new StreamReader(response.GetResponseStream()); - return reader.ReadToEnd(); - } - catch (Exception ex) - { - Logger.Trace("Failed to get response from: {0}", url); - Logger.Trace(ex.Message, ex); - } - - return String.Empty; - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCategoryModel.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCategoryModel.cs deleted file mode 100644 index 83d7b3e03..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCategoryModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class SabCategoryModel - { - public List categories { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs deleted file mode 100644 index 8e181a25e..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.IO; -using NLog; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; -using RestSharp; - -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public interface ISabCommunicationProxy - { - SabAddResponse DownloadNzb(Stream nzb, string name, string category, int priority); - void RemoveFrom(string source, string id); - string ProcessRequest(IRestRequest restRequest, string action); - } - - public class SabCommunicationProxy : ISabCommunicationProxy - { - private readonly IConfigService _configService; - private readonly Logger _logger; - - public SabCommunicationProxy(IConfigService configService, Logger logger) - { - _configService = configService; - _logger = logger; - } - - public SabAddResponse DownloadNzb(Stream nzb, string title, string category, int priority) - { - var request = new RestRequest(Method.POST); - var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority); - - request.AddFile("name", ReadFully(nzb), title, "application/x-nzb"); - - SabAddResponse response; - - if (!Json.TryDeserialize(ProcessRequest(request, action), out response)) - { - response = new SabAddResponse(); - response.Status = true; - } - - return response; - } - - public void RemoveFrom(string source, string id) - { - var request = new RestRequest(); - var action = String.Format("mode={0}&name=delete&del_files=1&value={1}", source, id); - - ProcessRequest(request, action); - } - - public string ProcessRequest(IRestRequest restRequest, string action) - { - var client = BuildClient(action); - var response = client.Execute(restRequest); - _logger.Trace("Response: {0}", response.Content); - - CheckForError(response); - - return response.Content; - } - - private IRestClient BuildClient(string action) - { - var protocol = _configService.SabUseSsl ? "https" : "http"; - - var url = string.Format(@"{0}://{1}:{2}/api?{3}&apikey={4}&ma_username={5}&ma_password={6}&output=json", - protocol, - _configService.SabHost, - _configService.SabPort, - action, - _configService.SabApiKey, - _configService.SabUsername, - _configService.SabPassword); - - _logger.Trace(url); - - return new RestClient(url); - } - - private void CheckForError(IRestResponse response) - { - if (response.ResponseStatus != ResponseStatus.Completed) - { - throw new ApplicationException("Unable to connect to SABnzbd, please check your settings"); - } - - SabJsonError result; - - if (!Json.TryDeserialize(response.Content, out result)) - { - //Handle plain text responses from SAB - result = new SabJsonError(); - - if (response.Content.StartsWith("error", StringComparison.InvariantCultureIgnoreCase)) - { - result.Status = "false"; - result.Error = response.Content.Replace("error: ", ""); - } - - else - { - result.Status = "true"; - } - - result.Error = response.Content.Replace("error: ", ""); - } - - if (result.Failed) - throw new ApplicationException(result.Error); - } - - //TODO: Find a better home for this - private byte[] ReadFully(Stream input) - { - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - return ms.ToArray(); - } - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabModel.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabModel.cs deleted file mode 100644 index 158535065..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class SabModel - { - public string Host { get; set; } - public int Port { get; set; } - public string ApiKey { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabVersionModel.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabVersionModel.cs deleted file mode 100644 index 9d8cad8fd..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabVersionModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class SabVersionModel - { - public string Version { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs new file mode 100644 index 000000000..69601663f --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using Omu.ValueInjecter; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public class Sabnzbd : DownloadClientBase, IExecute + { + private readonly IHttpProvider _httpProvider; + private readonly IParsingService _parsingService; + private readonly ISabnzbdProxy _sabnzbdProxy; + private readonly ICached> _queueCache; + private readonly Logger _logger; + + public Sabnzbd(IHttpProvider httpProvider, + ICacheManger cacheManger, + IParsingService parsingService, + ISabnzbdProxy sabnzbdProxy, + Logger logger) + { + _httpProvider = httpProvider; + _parsingService = parsingService; + _sabnzbdProxy = sabnzbdProxy; + _queueCache = cacheManger.GetCache>(GetType(), "queue"); + _logger = logger; + } + + public override string DownloadNzb(RemoteEpisode remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + var category = Settings.TvCategory; + var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; + + using (var nzb = _httpProvider.DownloadStream(url)) + { + _logger.Info("Adding report [{0}] to the queue.", title); + var response = _sabnzbdProxy.DownloadNzb(nzb, title, category, priority, Settings); + + if (response != null && response.Ids.Any()) + { + return response.Ids.First(); + } + + return null; + } + } + + public override IEnumerable GetQueue() + { + return _queueCache.Get("queue", () => + { + var sabQueue = _sabnzbdProxy.GetQueue(0, 0, Settings).Items; + + var queueItems = new List(); + + foreach (var sabQueueItem in sabQueue) + { + var queueItem = new QueueItem(); + queueItem.Id = sabQueueItem.Id; + queueItem.Title = sabQueueItem.Title; + queueItem.Size = sabQueueItem.Size; + queueItem.Sizeleft = sabQueueItem.Sizeleft; + queueItem.Timeleft = sabQueueItem.Timeleft; + queueItem.Status = sabQueueItem.Status; + + var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title.Replace("ENCRYPTED / ", "")); + if (parsedEpisodeInfo == null) continue; + + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); + if (remoteEpisode.Series == null) continue; + + queueItem.RemoteEpisode = remoteEpisode; + + queueItems.Add(queueItem); + } + + return queueItems; + }, TimeSpan.FromSeconds(10)); + } + + public override IEnumerable GetHistory(int start = 0, int limit = 10) + { + var items = _sabnzbdProxy.GetHistory(start, limit, Settings).Items; + var historyItems = new List(); + + foreach (var sabHistoryItem in items) + { + var historyItem = new HistoryItem(); + historyItem.Id = sabHistoryItem.Id; + historyItem.Title = sabHistoryItem.Title; + historyItem.Size = sabHistoryItem.Size; + historyItem.DownloadTime = sabHistoryItem.DownloadTime; + historyItem.Storage = sabHistoryItem.Storage; + historyItem.Category = sabHistoryItem.Category; + historyItem.Message = sabHistoryItem.FailMessage; + historyItem.Status = sabHistoryItem.Status == "Failed" ? HistoryStatus.Failed : HistoryStatus.Completed; + + historyItems.Add(historyItem); + } + + return historyItems; + } + + public override void RemoveFromQueue(string id) + { + _sabnzbdProxy.RemoveFrom("queue", id, Settings); + } + + public override void RemoveFromHistory(string id) + { + _sabnzbdProxy.RemoveFrom("history", id, Settings); + } + + public void Execute(TestSabnzbdCommand message) + { + var settings = new SabnzbdSettings(); + settings.InjectFrom(message); + + _sabnzbdProxy.GetCategories(settings); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs deleted file mode 100644 index 5535eb00b..000000000 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using NLog; -using NzbDrone.Common; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Clients.Sabnzbd -{ - public class SabnzbdClient : IDownloadClient - { - private readonly IConfigService _configService; - private readonly IHttpProvider _httpProvider; - private readonly IParsingService _parsingService; - private readonly ISabCommunicationProxy _sabCommunicationProxy; - private readonly ICached> _queueCache; - private readonly Logger _logger; - - public SabnzbdClient(IConfigService configService, - IHttpProvider httpProvider, - ICacheManger cacheManger, - IParsingService parsingService, - ISabCommunicationProxy sabCommunicationProxy, - Logger logger) - { - _configService = configService; - _httpProvider = httpProvider; - _parsingService = parsingService; - _sabCommunicationProxy = sabCommunicationProxy; - _queueCache = cacheManger.GetCache>(GetType(), "queue"); - _logger = logger; - } - - public bool IsConfigured - { - get - { - return !string.IsNullOrWhiteSpace(_configService.SabHost) - && _configService.SabPort != 0; - } - } - - public string DownloadNzb(RemoteEpisode remoteEpisode) - { - var url = remoteEpisode.Release.DownloadUrl; - var title = remoteEpisode.Release.Title; - var category = _configService.SabTvCategory; - var priority = remoteEpisode.IsRecentEpisode() ? (int)_configService.SabRecentTvPriority : (int)_configService.SabOlderTvPriority; - - using (var nzb = _httpProvider.DownloadStream(url)) - { - _logger.Info("Adding report [{0}] to the queue.", title); - var response = _sabCommunicationProxy.DownloadNzb(nzb, title, category, priority); - - if (response != null && response.Ids.Any()) - { - return response.Ids.First(); - } - - return null; - } - } - - public IEnumerable GetQueue() - { - return _queueCache.Get("queue", () => - { - string action = String.Format("mode=queue&output=json&start={0}&limit={1}", 0, 0); - string request = GetSabRequest(action); - string response = _httpProvider.DownloadString(request); - - CheckForError(response); - - var sabQueue = Json.Deserialize(JObject.Parse(response).SelectToken("queue").ToString()).Items; - - var queueItems = new List(); - - foreach (var sabQueueItem in sabQueue) - { - var queueItem = new QueueItem(); - queueItem.Id = sabQueueItem.Id; - queueItem.Title = sabQueueItem.Title; - queueItem.Size = sabQueueItem.Size; - queueItem.Sizeleft = sabQueueItem.Sizeleft; - queueItem.Timeleft = sabQueueItem.Timeleft; - queueItem.Status = sabQueueItem.Status; - - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title.Replace("ENCRYPTED / ", "")); - if (parsedEpisodeInfo == null) continue; - - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) continue; - - queueItem.RemoteEpisode = remoteEpisode; - - queueItems.Add(queueItem); - } - - return queueItems; - }, TimeSpan.FromSeconds(10)); - } - - public IEnumerable GetHistory(int start = 0, int limit = 0) - { - string action = String.Format("mode=history&output=json&start={0}&limit={1}", start, limit); - string request = GetSabRequest(action); - string response = _httpProvider.DownloadString(request); - - CheckForError(response); - - var items = Json.Deserialize(JObject.Parse(response).SelectToken("history").ToString()).Items; - var historyItems = new List(); - - foreach (var sabHistoryItem in items) - { - var historyItem = new HistoryItem(); - historyItem.Id = sabHistoryItem.Id; - historyItem.Title = sabHistoryItem.Title; - historyItem.Size = sabHistoryItem.Size; - historyItem.DownloadTime = sabHistoryItem.DownloadTime; - historyItem.Storage = sabHistoryItem.Storage; - historyItem.Category = sabHistoryItem.Category; - historyItem.Message = sabHistoryItem.FailMessage; - historyItem.Status = sabHistoryItem.Status == "Failed" ? HistoryStatus.Failed : HistoryStatus.Completed; - - historyItems.Add(historyItem); - } - - return historyItems; - } - - public void RemoveFromQueue(string id) - { - _sabCommunicationProxy.RemoveFrom("queue", id); - } - - public void RemoveFromHistory(string id) - { - _sabCommunicationProxy.RemoveFrom("history", id); - } - - public virtual SabCategoryModel GetCategories(string host = null, int port = 0, string apiKey = null, string username = null, string password = null) - { - //Get saved values if any of these are defaults - if (host == null) - host = _configService.SabHost; - - if (port == 0) - port = _configService.SabPort; - - if (apiKey == null) - apiKey = _configService.SabApiKey; - - if (username == null) - username = _configService.SabUsername; - - if (password == null) - password = _configService.SabPassword; - - const string action = "mode=get_cats&output=json"; - - var command = string.Format(@"http://{0}:{1}/api?{2}&apikey={3}&ma_username={4}&ma_password={5}", - host, port, action, apiKey, username, password); - - var response = _httpProvider.DownloadString(command); - - if (String.IsNullOrWhiteSpace(response)) - return new SabCategoryModel { categories = new List() }; - - var categories = Json.Deserialize(response); - - return categories; - } - - public virtual SabVersionModel GetVersion(string host = null, int port = 0, string apiKey = null, string username = null, string password = null) - { - //Get saved values if any of these are defaults - if (host == null) - host = _configService.SabHost; - - if (port == 0) - port = _configService.SabPort; - - if (apiKey == null) - apiKey = _configService.SabApiKey; - - if (username == null) - username = _configService.SabUsername; - - if (password == null) - password = _configService.SabPassword; - - const string action = "mode=version&output=json"; - - var command = string.Format(@"http://{0}:{1}/api?{2}&apikey={3}&ma_username={4}&ma_password={5}", - host, port, action, apiKey, username, password); - - var response = _httpProvider.DownloadString(command); - - if (String.IsNullOrWhiteSpace(response)) - return null; - - var version = Json.Deserialize(response); - - return version; - } - - public virtual string Test(string host, int port, string apiKey, string username, string password) - { - try - { - var version = GetVersion(host, port, apiKey, username, password); - return version.Version; - } - catch (Exception ex) - { - _logger.DebugException("Failed to Test SABnzbd", ex); - } - - return String.Empty; - } - - private string GetSabRequest(string action) - { - var protocol = _configService.SabUseSsl ? "https" : "http"; - - return string.Format(@"{0}://{1}:{2}/api?{3}&apikey={4}&ma_username={5}&ma_password={6}", - protocol, - _configService.SabHost, - _configService.SabPort, - action, - _configService.SabApiKey, - _configService.SabUsername, - _configService.SabPassword); - } - - private void CheckForError(string response) - { - var result = Json.Deserialize(response); - - if (result.Failed) - throw new ApplicationException(result.Error); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabQueue.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistory.cs similarity index 70% rename from src/NzbDrone.Core/Download/Clients/Sabnzbd/SabQueue.cs rename to src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistory.cs index d19fa608c..b19786739 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabQueue.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistory.cs @@ -3,11 +3,11 @@ using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public class SabQueue + public class SabnzbdHistory { public bool Paused { get; set; } [JsonProperty(PropertyName = "slots")] - public List Items { get; set; } + public List Items { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs similarity index 95% rename from src/NzbDrone.Core/Download/Clients/Sabnzbd/SabHistoryItem.cs rename to src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs index fa94cbc2f..166b25c94 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabHistoryItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public class SabHistoryItem + public class SabnzbdHistoryItem { [JsonProperty(PropertyName = "fail_message")] public string FailMessage { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabJsonError.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdJsonError.cs similarity index 92% rename from src/NzbDrone.Core/Download/Clients/Sabnzbd/SabJsonError.cs rename to src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdJsonError.cs index 8ad40b398..853c7e104 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabJsonError.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdJsonError.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public class SabJsonError + public class SabnzbdJsonError { public string Status { get; set; } public string Error { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabPriorityType.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdPriority.cs similarity index 84% rename from src/NzbDrone.Core/Download/Clients/Sabnzbd/SabPriorityType.cs rename to src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdPriority.cs index d16be5f2f..b769a78db 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabPriorityType.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdPriority.cs @@ -1,6 +1,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public enum SabPriorityType + public enum SabnzbdPriority { Default = -100, Paused = -2, diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs new file mode 100644 index 000000000..6d43e443e --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; +using Newtonsoft.Json.Linq; +using NLog; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; +using RestSharp; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public interface ISabnzbdProxy + { + SabnzbdAddResponse DownloadNzb(Stream nzb, string name, string category, int priority, SabnzbdSettings settings); + void RemoveFrom(string source, string id, SabnzbdSettings settings); + string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings); + SabnzbdVersionResponse GetVersion(SabnzbdSettings settings); + SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings); + SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings); + SabnzbdHistory GetHistory(int start, int limit, SabnzbdSettings settings); + } + + public class SabnzbdProxy : ISabnzbdProxy + { + private readonly Logger _logger; + + public SabnzbdProxy(Logger logger) + { + _logger = logger; + } + + public SabnzbdAddResponse DownloadNzb(Stream nzb, string title, string category, int priority, SabnzbdSettings settings) + { + var request = new RestRequest(Method.POST); + var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority); + + request.AddFile("name", ReadFully(nzb), title, "application/x-nzb"); + + SabnzbdAddResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, action, settings), out response)) + { + response = new SabnzbdAddResponse(); + response.Status = true; + } + + return response; + } + + public void RemoveFrom(string source, string id, SabnzbdSettings settings) + { + var request = new RestRequest(); + var action = String.Format("mode={0}&name=delete&del_files=1&value={1}", source, id); + + ProcessRequest(request, action, settings); + } + + public string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings) + { + var client = BuildClient(action, settings); + var response = client.Execute(restRequest); + _logger.Trace("Response: {0}", response.Content); + + CheckForError(response); + + return response.Content; + } + + public SabnzbdVersionResponse GetVersion(SabnzbdSettings settings) + { + var request = new RestRequest(); + var action = "mode=version"; + + SabnzbdVersionResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, action, settings), out response)) + { + response = new SabnzbdVersionResponse(); + } + + return response; + } + + public SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings) + { + var request = new RestRequest(); + var action = "mode=get_cats"; + + var response = Json.Deserialize(ProcessRequest(request, action, settings)); + + return response; + } + + public SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings) + { + var request = new RestRequest(); + var action = String.Format("mode=queue&start={0}&limit={1}", start, limit); + + var response = ProcessRequest(request, action, settings); + return Json.Deserialize(JObject.Parse(response).SelectToken("queue").ToString()); + + } + + public SabnzbdHistory GetHistory(int start, int limit, SabnzbdSettings settings) + { + var request = new RestRequest(); + var action = String.Format("mode=history&start={0}&limit={1}", start, limit); + + var response = ProcessRequest(request, action, settings); + return Json.Deserialize(JObject.Parse(response).SelectToken("history").ToString()); + } + + private IRestClient BuildClient(string action, SabnzbdSettings settings) + { + var protocol = settings.UseSsl ? "https" : "http"; + + var url = string.Format(@"{0}://{1}:{2}/api?{3}&apikey={4}&ma_username={5}&ma_password={6}&output=json", + protocol, + settings.Host, + settings.Port, + action, + settings.ApiKey, + settings.Username, + settings.Password); + + _logger.Trace(url); + + return new RestClient(url); + } + + private void CheckForError(IRestResponse response) + { + if (response.ResponseStatus != ResponseStatus.Completed) + { + throw new ApplicationException("Unable to connect to SABnzbd, please check your settings"); + } + + SabnzbdJsonError result; + + if (!Json.TryDeserialize(response.Content, out result)) + { + //Handle plain text responses from SAB + result = new SabnzbdJsonError(); + + if (response.Content.StartsWith("error", StringComparison.InvariantCultureIgnoreCase)) + { + result.Status = "false"; + result.Error = response.Content.Replace("error: ", ""); + } + + else + { + result.Status = "true"; + } + + result.Error = response.Content.Replace("error: ", ""); + } + + if (result.Failed) + throw new ApplicationException(result.Error); + } + + //TODO: Find a better home for this + private byte[] ReadFully(Stream input) + { + byte[] buffer = new byte[16 * 1024]; + using (MemoryStream ms = new MemoryStream()) + { + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabHistory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueue.cs similarity index 70% rename from src/NzbDrone.Core/Download/Clients/Sabnzbd/SabHistory.cs rename to src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueue.cs index f90a2d1ce..edbdab5da 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabHistory.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueue.cs @@ -3,11 +3,11 @@ using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public class SabHistory + public class SabnzbdQueue { public bool Paused { get; set; } [JsonProperty(PropertyName = "slots")] - public List Items { get; set; } + public List Items { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs similarity index 91% rename from src/NzbDrone.Core/Download/Clients/Sabnzbd/SabQueueItem.cs rename to src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs index bc233eb84..a3a74452f 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters; namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public class SabQueueItem + public class SabnzbdQueueItem { public string Status { get; set; } public int Index { get; set; } @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public string Title { get; set; } [JsonConverter(typeof(SabnzbdPriorityTypeConverter))] - public SabPriorityType Priority { get; set; } + public SabnzbdPriority Priority { get; set; } [JsonProperty(PropertyName = "cat")] public string Category { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs new file mode 100644 index 000000000..de5e87fb8 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -0,0 +1,76 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public class SabnzbdSettingsValidator : AbstractValidator + { + public SabnzbdSettingsValidator() + { + RuleFor(c => c.Host).NotEmpty(); + RuleFor(c => c.Port).GreaterThan(0); + + RuleFor(c => c.ApiKey).NotEmpty() + .WithMessage("API Key is required when username/password are not configured") + .When(c => String.IsNullOrWhiteSpace(c.Username)); + + RuleFor(c => c.Username).NotEmpty() + .WithMessage("Username is required when API key is not configured") + .When(c => String.IsNullOrWhiteSpace(c.ApiKey)); + + + RuleFor(c => c.Password).NotEmpty() + .WithMessage("Password is required when API key is not configured") + .When(c => String.IsNullOrWhiteSpace(c.ApiKey)); + } + } + + public class SabnzbdSettings : IProviderConfig + { + private static readonly SabnzbdSettingsValidator Validator = new SabnzbdSettingsValidator(); + + public SabnzbdSettings() + { + Host = "localhost"; + Port = 8080; + TvCategory = "tv"; + RecentTvPriority = (int)SabnzbdPriority.Default; + OlderTvPriority = (int)SabnzbdPriority.Default; + } + + [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] + public String Host { get; set; } + + [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] + public Int32 Port { get; set; } + + [FieldDefinition(2, Label = "API Key", Type = FieldType.Textbox)] + public String ApiKey { get; set; } + + [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] + public String Username { get; set; } + + [FieldDefinition(4, Label = "Password", Type = FieldType.Password)] + public String Password { get; set; } + + [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)] + public String TvCategory { get; set; } + + [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority))] + public Int32 RecentTvPriority { get; set; } + + [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority))] + public Int32 OlderTvPriority { get; set; } + + [FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)] + public Boolean UseSsl { get; set; } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs new file mode 100644 index 000000000..2c1d2eb9d --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs @@ -0,0 +1,23 @@ +using System; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public class TestSabnzbdCommand : Command + { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public String Host { get; set; } + public Int32 Port { get; set; } + public String ApiKey { get; set; } + public String Username { get; set; } + public String Password { get; set; } + public Boolean UseSsl { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs new file mode 100644 index 000000000..157b1e855 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Download +{ + public abstract class DownloadClientBase : IDownloadClient where TSettings : IProviderConfig, new() + { + public Type ConfigContract + { + get + { + return typeof(TSettings); + } + } + + public IEnumerable DefaultDefinitions + { + get + { + return new List(); + } + } + + public ProviderDefinition Definition { get; set; } + + protected TSettings Settings + { + get + { + return (TSettings)Definition.Settings; + } + } + + public override string ToString() + { + return GetType().Name; + } + + public abstract string DownloadNzb(RemoteEpisode remoteEpisode); + public abstract IEnumerable GetQueue(); + public abstract IEnumerable GetHistory(int start = 0, int limit = 10); + public abstract void RemoveFromQueue(string id); + public abstract void RemoveFromHistory(string id); + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs new file mode 100644 index 000000000..479d10925 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs @@ -0,0 +1,12 @@ +using System; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientDefinition : ProviderDefinition + { + public Boolean Enable { get; set; } + public DownloadProtocol Protocol { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs new file mode 100644 index 000000000..07c56096e --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Composition; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Download +{ + public interface IDownloadClientFactory : IProviderFactory + { + List Enabled(); + } + + public class DownloadClientFactory : ProviderFactory, IDownloadClientFactory + { + private readonly IDownloadClientRepository _providerRepository; + + public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable providers, IContainer container, Logger logger) + : base(providerRepository, providers, container, logger) + { + _providerRepository = providerRepository; + } + + public List Enabled() + { + return GetAvailableProviders().Where(n => ((DownloadClientDefinition)n.Definition).Enable).ToList(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 12f9260b8..8a220d8b0 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -1,4 +1,6 @@ -using NzbDrone.Core.Configuration; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Download.Clients.Sabnzbd; @@ -12,42 +14,16 @@ namespace NzbDrone.Core.Download public class DownloadClientProvider : IProvideDownloadClient { + private readonly IDownloadClientFactory _downloadClientFactory; - private readonly SabnzbdClient _sabnzbdClient; - private readonly IConfigService _configService; - private readonly BlackholeProvider _blackholeProvider; - private readonly PneumaticClient _pneumaticClient; - private readonly NzbgetClient _nzbgetClient; - - - public DownloadClientProvider(SabnzbdClient sabnzbdClient, IConfigService configService, - BlackholeProvider blackholeProvider, - PneumaticClient pneumaticClient, - NzbgetClient nzbgetClient) + public DownloadClientProvider(IDownloadClientFactory downloadClientFactory) { - _sabnzbdClient = sabnzbdClient; - _configService = configService; - _blackholeProvider = blackholeProvider; - _pneumaticClient = pneumaticClient; - _nzbgetClient = nzbgetClient; + _downloadClientFactory = downloadClientFactory; } public IDownloadClient GetDownloadClient() { - switch (_configService.DownloadClient) - { - case DownloadClientType.Blackhole: - return _blackholeProvider; - - case DownloadClientType.Pneumatic: - return _pneumaticClient; - - case DownloadClientType.Nzbget: - return _nzbgetClient; - - default: - return _sabnzbdClient; - } + return _downloadClientFactory.Enabled().FirstOrDefault(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadClientRepository.cs b/src/NzbDrone.Core/Download/DownloadClientRepository.cs new file mode 100644 index 000000000..25c1ea15c --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientRepository.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + + +namespace NzbDrone.Core.Download +{ + public interface IDownloadClientRepository : IProviderRepository + { + + } + + public class DownloadClientRepository : ProviderRepository, IDownloadClientRepository + { + public DownloadClientRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 15acf13b2..71e633d47 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -36,9 +36,9 @@ namespace NzbDrone.Core.Download var downloadTitle = remoteEpisode.Release.Title; var downloadClient = _downloadClientProvider.GetDownloadClient(); - if (!downloadClient.IsConfigured) + if (downloadClient == null) { - _logger.Warn("Download client {0} isn't configured yet.", downloadClient.GetType().Name); + _logger.Warn("Download client isn't configured yet."); return; } diff --git a/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs new file mode 100644 index 000000000..0475ceaf2 --- /dev/null +++ b/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Download.Events +{ + public class DownloadFailedEvent : IEvent + { + public Int32 SeriesId { get; set; } + public List EpisodeIds { get; set; } + public QualityModel Quality { get; set; } + public String SourceTitle { get; set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } + public String Message { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs b/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs new file mode 100644 index 000000000..887e42362 --- /dev/null +++ b/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download.Events +{ + public class EpisodeGrabbedEvent : IEvent + { + public RemoteEpisode Episode { get; private set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } + + public EpisodeGrabbedEvent(RemoteEpisode episode) + { + Episode = episode; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 487da94cb..373ddc567 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Net; using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.History; @@ -46,7 +45,14 @@ namespace NzbDrone.Core.Download private void CheckQueue(List grabbedHistory, List failedHistory) { - var downloadClientQueue = GetDownloadClient().GetQueue().ToList(); + var downloadClient = GetDownloadClient(); + + if (downloadClient == null) + { + return; + } + + var downloadClientQueue = downloadClient.GetQueue().ToList(); var failedItems = downloadClientQueue.Where(q => q.Title.StartsWith("ENCRYPTED / ")).ToList(); if (!failedItems.Any()) @@ -78,14 +84,21 @@ namespace NzbDrone.Core.Download if (_configService.RemoveFailedDownloads) { _logger.Info("Removing encrypted download from queue: {0}", failedItem.Title.Replace("ENCRYPTED / ", "")); - GetDownloadClient().RemoveFromQueue(failedItem.Id); + downloadClient.RemoveFromQueue(failedItem.Id); } } } private void CheckHistory(List grabbedHistory, List failedHistory) { - var downloadClientHistory = GetDownloadClient().GetHistory(0, 20).ToList(); + var downloadClient = GetDownloadClient(); + + if (downloadClient == null) + { + return; + } + + var downloadClientHistory = downloadClient.GetHistory(0, 20).ToList(); var failedItems = downloadClientHistory.Where(h => h.Status == HistoryStatus.Failed).ToList(); if (!failedItems.Any()) @@ -117,7 +130,7 @@ namespace NzbDrone.Core.Download if (_configService.RemoveFailedDownloads) { _logger.Info("Removing failed download from history: {0}", failedItem.Title); - GetDownloadClient().RemoveFromHistory(failedItem.Id); + downloadClient.RemoveFromHistory(failedItem.Id); } } } @@ -152,7 +165,14 @@ namespace NzbDrone.Core.Download private IDownloadClient GetDownloadClient() { - return _downloadClientProvider.GetDownloadClient(); + var downloadClient = _downloadClientProvider.GetDownloadClient(); + + if (downloadClient == null) + { + _logger.Trace("No download client is configured"); + } + + return downloadClient; } public void Execute(CheckForFailedDownloadCommand message) diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 42107372b..d13fe243e 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Download { - public interface IDownloadClient + public interface IDownloadClient : IProvider { string DownloadNzb(RemoteEpisode remoteEpisode); - bool IsConfigured { get; } IEnumerable GetQueue(); IEnumerable GetHistory(int start = 0, int limit = 0); void RemoveFromQueue(string id); diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs new file mode 100644 index 000000000..3ccb0c181 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs @@ -0,0 +1,31 @@ +using NLog; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupOrphanedBlacklist : IHousekeepingTask + { + private readonly IDatabase _database; + private readonly Logger _logger; + + public CleanupOrphanedBlacklist(IDatabase database, Logger logger) + { + _database = database; + _logger = logger; + } + + public void Clean() + { + _logger.Trace("Running orphaned blacklist cleanup"); + + var mapper = _database.GetDataMapper(); + + mapper.ExecuteNonQuery(@"DELETE FROM Blacklist + WHERE Id IN ( + SELECT Blacklist.Id FROM Blacklist + LEFT OUTER JOIN Series + ON Blacklist.SeriesId = Series.Id + WHERE Series.Id IS NULL)"); + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index c86256099..689d36ab3 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions } } - private static string GetQueryTitle(string title) + public static string GetQueryTitle(string title) { Ensure.That(title,() => title).IsNotNullOrWhiteSpace(); diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs new file mode 100644 index 000000000..93bdfd0e0 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.IndexerSearch.Definitions +{ + public class SpecialEpisodeSearchCriteria : SearchCriteriaBase + { + public string[] EpisodeQueryTitles { get; set; } + + public override string ToString() + { + return string.Format("[{0} : {1}]", SceneTitle, String.Join(",", EpisodeQueryTitles)); + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index 0981c5eb9..006f2b9a8 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -64,6 +64,12 @@ namespace NzbDrone.Core.IndexerSearch return SearchDaily(series, episode); } + if (episode.SeasonNumber == 0) + { + // search for special episodes in season 0 + return SearchSpecial(series, new List{episode}); + } + return SearchSingle(series, episode); } @@ -103,11 +109,28 @@ namespace NzbDrone.Core.IndexerSearch return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); } + private List SearchSpecial(Series series, List episodes) + { + var searchSpec = Get(series, episodes); + // build list of queries for each episode in the form: " " + searchSpec.EpisodeQueryTitles = episodes.Where(e => !String.IsNullOrWhiteSpace(e.Title)) + .Select(e => searchSpec.QueryTitle + " " + SearchCriteriaBase.GetQueryTitle(e.Title)) + .ToArray(); + + return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); + } + public List SeasonSearch(int seriesId, int seasonNumber) { var series = _seriesService.GetSeries(seriesId); var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber); + if (seasonNumber == 0) + { + // search for special episodes in season 0 + return SearchSpecial(series, episodes); + } + var searchSpec = Get(series, episodes); searchSpec.SeasonNumber = seasonNumber; diff --git a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs index 5f141eacb..f926d911e 100644 --- a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs +++ b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs @@ -54,5 +54,10 @@ namespace NzbDrone.Core.Indexers.Eztv //EZTV doesn't support searching based on actual episode airdate. they only support release date. return new string[0]; } + + public override IEnumerable GetSearchUrls(string query, int offset) + { + return new List(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 2f850d8de..a4a9f1c6c 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -14,5 +14,6 @@ namespace NzbDrone.Core.Indexers IEnumerable GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber); IEnumerable GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date); IEnumerable GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset); + IEnumerable GetSearchUrls(string query, int offset = 0); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index e59bcf4c9..9c6527a17 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -50,6 +50,7 @@ namespace NzbDrone.Core.Indexers public abstract IEnumerable GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber); public abstract IEnumerable GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date); public abstract IEnumerable GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset); + public abstract IEnumerable GetSearchUrls(string query, int offset); public override string ToString() { @@ -59,7 +60,7 @@ namespace NzbDrone.Core.Indexers public enum DownloadProtocol { - Usenet, - Torrent + Usenet = 1, + Torrent = 2 } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs index 57158fec0..7009e748c 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers IList Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria); IList Fetch(IIndexer indexer, SingleEpisodeSearchCriteria searchCriteria); IList Fetch(IIndexer indexer, DailyEpisodeSearchCriteria searchCriteria); + IList Fetch(IIndexer indexer, SpecialEpisodeSearchCriteria searchCriteria); } public class FetchFeedService : IFetchFeedFromIndexers @@ -76,9 +77,8 @@ namespace NzbDrone.Core.Indexers var searchUrls = indexer.GetEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber); var result = Fetch(indexer, searchUrls); - - _logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count); + return result; } @@ -93,6 +93,20 @@ namespace NzbDrone.Core.Indexers return result; } + public IList Fetch(IIndexer indexer, SpecialEpisodeSearchCriteria searchCriteria) + { + var queryUrls = new List(); + foreach (var episodeQueryTitle in searchCriteria.EpisodeQueryTitles) + { + _logger.Debug("Performing query of {0} for {1}", indexer, episodeQueryTitle); + queryUrls.AddRange(indexer.GetSearchUrls(episodeQueryTitle)); + } + + var result = Fetch(indexer, queryUrls); + _logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count); + return result; + } + private List Fetch(IIndexer indexer, IEnumerable urls) { var result = new List(); diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index 985ee208b..7e6f8caad 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -111,6 +111,15 @@ namespace NzbDrone.Core.Indexers.Newznab return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&ep={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, episodeNumber)); } + public override IEnumerable GetSearchUrls(string query, int offset) + { + // encode query (replace the + with spaces first) + query = query.Replace("+", " "); + query = System.Web.HttpUtility.UrlEncode(query); + return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query)); + } + + public override IEnumerable GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date) { if (tvRageId > 0) diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs index 4869b7c2b..689138c03 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs @@ -67,8 +67,13 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs return searchUrls; } - public override bool SupportsPaging + public override IEnumerable GetSearchUrls(string query, int offset) { + return new List(); + } + + public override bool SupportsPaging + { get { return false; diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs index 17180ed99..9b7ade53f 100644 --- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs +++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs @@ -49,5 +49,10 @@ namespace NzbDrone.Core.Indexers.Wombles { return new List(); } + + public override IEnumerable GetSearchUrls(string query, int offset) + { + return new List(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Jobs/JobRepository.cs b/src/NzbDrone.Core/Jobs/JobRepository.cs index 8e2aa5858..b09e598b4 100644 --- a/src/NzbDrone.Core/Jobs/JobRepository.cs +++ b/src/NzbDrone.Core/Jobs/JobRepository.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Jobs public ScheduledTask GetDefinition(Type type) { - return Query.Single(c => c.TypeName == type.FullName); + return Query.Where(c => c.TypeName == type.FullName).Single(); } public void SetLastExecutionTime(int id, DateTime executionTime) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 8b7c620bb..85f0a4fc0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport try { - var parsedEpisode = _parsingService.GetEpisodes(file, series, sceneSource); + var parsedEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource); if (parsedEpisode != null) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index 282c954a4..32d6b9054 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (freeSpace < localEpisode.Size + 100.Megabytes()) { - _logger.Warn("Not enough free space to import: {0}", localEpisode); + _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localEpisode, localEpisode.Size); return false; } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index 735b80400..a897a1582 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -62,7 +62,7 @@ namespace NzbDrone.Core.MediaFiles continue; } -// var localEpsiode = _parsingService.GetEpisodes(episodeFile.Path, series); +// var localEpsiode = _parsingService.GetLocalEpisode(episodeFile.Path, series); // // if (localEpsiode == null || episodes.Count != localEpsiode.Episodes.Count) // { diff --git a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs index 43573921e..6195f5d3a 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs @@ -75,6 +75,13 @@ namespace NzbDrone.Core.MediaFiles foreach (var file in files) { var episodesInFile = episodes.Where(e => e.EpisodeFileId == file.Id).ToList(); + + if (!episodesInFile.Any()) + { + _logger.Warn("File ({0}) is not linked to any episodes", file.Path); + continue; + } + var seasonNumber = episodesInFile.First().SeasonNumber; var newName = _filenameBuilder.BuildFilename(episodesInFile, series, file); var newPath = _filenameBuilder.BuildFilePath(series, seasonNumber, newName, Path.GetExtension(file.Path)); diff --git a/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs b/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs index 2eeb539ac..6ae86b093 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs @@ -1,10 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; -using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; @@ -16,19 +11,12 @@ namespace NzbDrone.Core.Metadata.Consumers.Fake { public class FakeMetadata : MetadataBase { - private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; - public FakeMetadata(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) : base(diskProvider, httpProvider, logger) { - _diskProvider = diskProvider; - _httpProvider = httpProvider; - _logger = logger; } - public override void OnSeriesUpdated(Series series, List existingMetadataFiles) + public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) { throw new NotImplementedException(); } @@ -38,7 +26,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Fake throw new NotImplementedException(); } - public override void AfterRename(Series series) + public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index e9f58aa39..d2bac9d22 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -9,6 +10,7 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; @@ -25,6 +27,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; private readonly IHttpProvider _httpProvider; + private readonly IEpisodeService _episodeService; private readonly Logger _logger; public XbmcMetadata(IEventAggregator eventAggregator, @@ -33,6 +36,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc IMetadataFileService metadataFileService, IDiskProvider diskProvider, IHttpProvider httpProvider, + IEpisodeService episodeService, Logger logger) : base(diskProvider, httpProvider, logger) { @@ -42,6 +46,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc _metadataFileService = metadataFileService; _diskProvider = diskProvider; _httpProvider = httpProvider; + _episodeService = episodeService; _logger = logger; } @@ -49,68 +54,120 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?\d{2,}|-all|-specials)-(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public override void OnSeriesUpdated(Series series, List existingMetadataFiles) + public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) { + var metadataFiles = new List(); + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Info("Series folder does not exist, skipping metadata creation"); + return; + } + if (Settings.SeriesMetadata) { - EnsureFolder(series.Path); - WriteTvShowNfo(series, existingMetadataFiles); + metadataFiles.Add(WriteTvShowNfo(series, existingMetadataFiles)); } if (Settings.SeriesImages) { - EnsureFolder(series.Path); - WriteSeriesImages(series, existingMetadataFiles); + metadataFiles.AddRange(WriteSeriesImages(series, existingMetadataFiles)); } if (Settings.SeasonImages) { - EnsureFolder(series.Path); - WriteSeasonImages(series, existingMetadataFiles); + metadataFiles.AddRange(WriteSeasonImages(series, existingMetadataFiles)); } + + foreach (var episodeFile in episodeFiles) + { + if (Settings.EpisodeMetadata) + { + metadataFiles.Add(WriteEpisodeNfo(series, episodeFile, existingMetadataFiles)); + } + } + + foreach (var episodeFile in episodeFiles) + { + if (Settings.EpisodeImages) + { + var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles); + + if (metadataFile != null) + { + metadataFiles.Add(metadataFile); + } + } + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); } public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) { + var metadataFiles = new List(); + if (Settings.EpisodeMetadata) { - WriteEpisodeNfo(series, episodeFile); + metadataFiles.Add(WriteEpisodeNfo(series, episodeFile, new List())); } if (Settings.EpisodeImages) { - WriteEpisodeImages(series, episodeFile); + var metadataFile = WriteEpisodeImages(series, episodeFile, new List()); + + if (metadataFile != null) + { + metadataFiles.Add(metadataFile); + } + WriteEpisodeImages(series, episodeFile, new List()); } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); } - public override void AfterRename(Series series) + public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { - var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); - var episodeFilesMetadata = _metadataFileService.GetFilesBySeries(series.Id).Where(c => c.EpisodeFileId > 0).ToList(); + var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); + var updatedMetadataFiles = new List(); foreach (var episodeFile in episodeFiles) { var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); - var episodeFilenameWithoutExtension = - Path.GetFileNameWithoutExtension(DiskProviderBase.GetRelativePath(series.Path, episodeFile.Path)); foreach (var metadataFile in metadataFiles) { - var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(metadataFile.RelativePath); - var extension = Path.GetExtension(metadataFile.RelativePath); + string newFilename; - if (!fileNameWithoutExtension.Equals(episodeFilenameWithoutExtension)) + if (metadataFile.Type == MetadataType.EpisodeImage) { - var source = Path.Combine(series.Path, metadataFile.RelativePath); - var destination = Path.Combine(series.Path, fileNameWithoutExtension + extension); + newFilename = GetEpisodeImageFilename(episodeFile.Path); + } - _diskProvider.MoveFile(source, destination); - metadataFile.RelativePath = fileNameWithoutExtension + extension; + else if (metadataFile.Type == MetadataType.EpisodeMetadata) + { + newFilename = GetEpisodeNfoFilename(episodeFile.Path); + } - _eventAggregator.PublishEvent(new MetadataFileUpdated(metadataFile)); + else + { + _logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath); + continue; + } + + var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); + + if (!newFilename.PathEquals(existingFilename)) + { + _diskProvider.MoveFile(existingFilename, newFilename); + metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + + updatedMetadataFiles.Add(metadataFile); } } } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); } public override MetadataFile FindMetadataFile(Series series, string path) @@ -178,7 +235,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc return null; } - private void WriteTvShowNfo(Series series, List existingMetadataFiles) + private MetadataFile WriteTvShowNfo(Series series, List existingMetadataFiles) { _logger.Trace("Generating tvshow.nfo for: {0}", series.Title); var sb = new StringBuilder(); @@ -191,7 +248,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc var tvShow = new XElement("tvshow"); tvShow.Add(new XElement("title", series.Title)); - tvShow.Add(new XElement("rating", series.Ratings.Percentage)); + tvShow.Add(new XElement("rating", (decimal)series.Ratings.Percentage/10)); tvShow.Add(new XElement("plot", series.Overview)); //Todo: probably will need to use TVDB to use this feature... @@ -239,11 +296,11 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) }; - _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); + return metadata; } } - private void WriteSeriesImages(Series series, List existingMetadataFiles) + private IEnumerable WriteSeriesImages(Series series, List existingMetadataFiles) { foreach (var image in series.Images) { @@ -268,11 +325,11 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination) }; - _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); + yield return metadata; } } - private void WriteSeasonImages(Series series, List existingMetadataFiles) + private IEnumerable WriteSeasonImages(Series series, List existingMetadataFiles) { foreach (var season in series.Seasons) { @@ -300,14 +357,28 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) }; - _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); + yield return metadata; } } } - private void WriteEpisodeNfo(Series series, EpisodeFile episodeFile) + private MetadataFile WriteEpisodeNfo(Series series, EpisodeFile episodeFile, List existingMetadataFiles) { - var filename = episodeFile.Path.Replace(Path.GetExtension(episodeFile.Path), ".nfo"); + var filename = GetEpisodeNfoFilename(episodeFile.Path); + var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!filename.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, filename); + existingMetadata.RelativePath = relativePath; + } + } _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); @@ -322,6 +393,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc using (var xw = XmlWriter.Create(sb, xws)) { var doc = new XDocument(); + var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); var details = new XElement("episodedetails"); details.Add(new XElement("title", episode.Title)); @@ -329,11 +401,23 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc details.Add(new XElement("episode", episode.EpisodeNumber)); details.Add(new XElement("aired", episode.AirDate)); details.Add(new XElement("plot", episode.Overview)); - details.Add(new XElement("displayseason", episode.SeasonNumber)); - details.Add(new XElement("displayepisode", episode.EpisodeNumber)); - details.Add(new XElement("thumb", episode.Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot).Url)); + + //If trakt ever gets airs before information for specials we should add set it + details.Add(new XElement("displayseason")); + details.Add(new XElement("displayepisode")); + + if (image == null) + { + details.Add(new XElement("thumb")); + } + + else + { + details.Add(new XElement("thumb", image.Url)); + } + details.Add(new XElement("watched", "false")); - details.Add(new XElement("rating", episode.Ratings.Percentage)); + details.Add(new XElement("rating", (decimal)episode.Ratings.Percentage/10)); //Todo: get guest stars, writer and director //details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault())); @@ -350,36 +434,68 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc _logger.Debug("Saving episodedetails to: {0}", filename); _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); - var metadata = new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.SeasonImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; - - _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); - } - - private void WriteEpisodeImages(Series series, EpisodeFile episodeFile) - { - var screenshot = episodeFile.Episodes.Value.First().Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot); - - var filename = Path.ChangeExtension(episodeFile.Path, "").Trim('.') + "-thumb.jpg"; - - DownloadImage(series, screenshot.Url, filename); - - var metadata = new MetadataFile + var metadata = existingMetadata ?? + new MetadataFile { SeriesId = series.Id, EpisodeFileId = episodeFile.Id, Consumer = GetType().Name, - Type = MetadataType.SeasonImage, + Type = MetadataType.EpisodeMetadata, RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) }; - _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); + return metadata; + } + + private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + + if (screenshot == null) + { + _logger.Trace("Episode screenshot not available"); + return null; + } + + var filename = GetEpisodeImageFilename(episodeFile.Path); + var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!filename.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, filename); + existingMetadata.RelativePath = relativePath; + } + } + + DownloadImage(series, screenshot.Url, filename); + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.EpisodeImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) + }; + + return metadata; + } + + private string GetEpisodeNfoFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "nfo"); + } + + private string GetEpisodeImageFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "").Trim('.') + "-thumb.jpg"; } } } diff --git a/src/NzbDrone.Core/MetaData/Files/CleanMetadataService.cs b/src/NzbDrone.Core/MetaData/Files/CleanMetadataService.cs new file mode 100644 index 000000000..cdcbb0088 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Files/CleanMetadataService.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Files +{ + public interface ICleanMetadataService + { + void Clean(Series series); + } + + public class CleanMetadataService : ICleanMetadataService + { + private readonly IMetadataFileService _metadataFileService; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public CleanMetadataService(IMetadataFileService metadataFileService, + IDiskProvider diskProvider, + Logger logger) + { + _metadataFileService = metadataFileService; + _diskProvider = diskProvider; + _logger = logger; + } + + public void Clean(Series series) + { + _logger.Trace("Cleaning missing metadata files for series: {0}", series.Title); + + var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + + foreach (var metadataFile in metadataFiles) + { + if (!_diskProvider.FileExists(Path.Combine(series.Path, metadataFile.RelativePath))) + { + _logger.Trace("Deleting metadata file from database: {0}", metadataFile.RelativePath); + _metadataFileService.Delete(metadataFile.Id); + } + } + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Files/MetadataFilesUpdated.cs b/src/NzbDrone.Core/MetaData/Files/MetadataFilesUpdated.cs new file mode 100644 index 000000000..98427d7dd --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Files/MetadataFilesUpdated.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Metadata.Files +{ + public class MetadataFilesUpdated : IEvent + { + public List MetadataFiles { get; set; } + + public MetadataFilesUpdated(List metadataFiles) + { + MetadataFiles = metadataFiles; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/IMetadata.cs b/src/NzbDrone.Core/MetaData/IMetadata.cs index 02a51554c..63fa19d73 100644 --- a/src/NzbDrone.Core/MetaData/IMetadata.cs +++ b/src/NzbDrone.Core/MetaData/IMetadata.cs @@ -8,9 +8,9 @@ namespace NzbDrone.Core.Metadata { public interface IMetadata : IProvider { - void OnSeriesUpdated(Series series, List existingMetadataFiles); + void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles); void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); - void AfterRename(Series series); + void AfterRename(Series series, List existingMetadataFiles, List episodeFiles); MetadataFile FindMetadataFile(Series series, string path); } } diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs index 08708ab3e..681eed8d0 100644 --- a/src/NzbDrone.Core/MetaData/MetadataService.cs +++ b/src/NzbDrone.Core/MetaData/MetadataService.cs @@ -1,38 +1,51 @@ -using System.IO; +using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; -using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Metadata { - public class NotificationService + public class MetadataService : IHandle, IHandle, IHandle { private readonly IMetadataFactory _metadataFactory; - private readonly MetadataFileService _metadataFileService; + private readonly IMetadataFileService _metadataFileService; + private readonly ICleanMetadataService _cleanMetadataService; + private readonly IMediaFileService _mediaFileService; + private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public NotificationService(IMetadataFactory metadataFactory, MetadataFileService metadataFileService, Logger logger) + public MetadataService(IMetadataFactory metadataFactory, + IMetadataFileService metadataFileService, + ICleanMetadataService cleanMetadataService, + IMediaFileService mediaFileService, + IEpisodeService episodeService, + Logger logger) { _metadataFactory = metadataFactory; _metadataFileService = metadataFileService; + _cleanMetadataService = cleanMetadataService; + _mediaFileService = mediaFileService; + _episodeService = episodeService; _logger = logger; } public void Handle(MediaCoversUpdatedEvent message) { + _cleanMetadataService.Clean(message.Series); var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); foreach (var consumer in _metadataFactory.Enabled()) { - consumer.OnSeriesUpdated(message.Series, seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList()); + consumer.OnSeriesUpdated(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), GetEpisodeFiles(message.Series.Id)); } } @@ -46,10 +59,31 @@ namespace NzbDrone.Core.Metadata public void Handle(SeriesRenamedEvent message) { + var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); + foreach (var consumer in _metadataFactory.Enabled()) { - consumer.AfterRename(message.Series); + consumer.AfterRename(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), GetEpisodeFiles(message.Series.Id)); } } + + private List GetEpisodeFiles(int seriesId) + { + var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); + var episodes = _episodeService.GetEpisodeBySeries(seriesId); + + foreach (var episodeFile in episodeFiles) + { + var localEpisodeFile = episodeFile; + episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); + } + + return episodeFiles; + } + + private List GetMetadataFilesForConsumer(IMetadata consumer, List seriesMetadata) + { + return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); + } } } diff --git a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs index 8331c03a2..4c5d89aa1 100644 --- a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs +++ b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs @@ -12,7 +12,7 @@ using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Metadata { - public class ExistingMetadataService : IHandleAsync + public class ExistingMetadataService : IHandle { private readonly IDiskProvider _diskProvider; private readonly IMetadataFileService _metadataFileService; @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Metadata _consumers = consumers.ToList(); } - public void HandleAsync(SeriesUpdatedEvent message) + public void Handle(SeriesUpdatedEvent message) { if (!_diskProvider.FolderExists(message.Series.Path)) return; @@ -42,7 +42,9 @@ namespace NzbDrone.Core.Metadata var filesOnDisk = _diskProvider.GetFiles(message.Series.Path, SearchOption.AllDirectories); var possibleMetadataFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower())).ToList(); var filteredFiles = _metadataFileService.FilterExistingFiles(possibleMetadataFiles, message.Series); - + + var metadataFiles = new List(); + foreach (var possibleMetadataFile in filteredFiles) { foreach (var consumer in _consumers) @@ -54,7 +56,7 @@ namespace NzbDrone.Core.Metadata if (metadata.Type == MetadataType.EpisodeImage || metadata.Type == MetadataType.EpisodeMetadata) { - var localEpisode = _parsingService.GetEpisodes(possibleMetadataFile, message.Series, false); + var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series, false); if (localEpisode == null) { @@ -71,9 +73,11 @@ namespace NzbDrone.Core.Metadata metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId; } - _metadataFileService.Upsert(metadata); + metadataFiles.Add(metadata); } } + + _metadataFileService.Upsert(metadataFiles); } } } diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs index 38889fbb3..64f7b871e 100644 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Metadata.Files public MetadataFile FindByPath(string path) { - return Query.SingleOrDefault(c => c.RelativePath == path); + return Query.Where(c => c.RelativePath == path).SingleOrDefault(); } } } diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs index f888bfdb8..e53c403af 100644 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs @@ -18,13 +18,14 @@ namespace NzbDrone.Core.Metadata.Files List GetFilesByEpisodeFile(int episodeFileId); MetadataFile FindByPath(string path); List FilterExistingFiles(List files, Series series); - MetadataFile Upsert(MetadataFile metadataFile); + void Upsert(List metadataFiles); + void Delete(int id); } public class MetadataFileService : IMetadataFileService, IHandleAsync, IHandleAsync, - IHandle + IHandle { private readonly IMetadataFileRepository _repository; private readonly ISeriesService _seriesService; @@ -66,10 +67,17 @@ namespace NzbDrone.Core.Metadata.Files return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); } - public MetadataFile Upsert(MetadataFile metadataFile) + public void Upsert(List metadataFiles) { - metadataFile.LastUpdated = DateTime.UtcNow; - return _repository.Upsert(metadataFile); + metadataFiles.ForEach(m => m.LastUpdated = DateTime.UtcNow); + + _repository.InsertMany(metadataFiles.Where(m => m.Id == 0).ToList()); + _repository.UpdateMany(metadataFiles.Where(m => m.Id > 0).ToList()); + } + + public void Delete(int id) + { + _repository.Delete(id); } public void HandleAsync(SeriesDeletedEvent message) @@ -97,9 +105,9 @@ namespace NzbDrone.Core.Metadata.Files _repository.DeleteForEpisodeFile(episodeFile.Id); } - public void Handle(MetadataFileUpdated message) + public void Handle(MetadataFilesUpdated message) { - Upsert(message.Metadata); + Upsert(message.MetadataFiles); } } } diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileUpdated.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileUpdated.cs deleted file mode 100644 index 7f7b4b189..000000000 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFileUpdated.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Metadata.Files -{ - public class MetadataFileUpdated : IEvent - { - public MetadataFile Metadata { get; set; } - - public MetadataFileUpdated(MetadataFile metadata) - { - Metadata = metadata; - } - } -} diff --git a/src/NzbDrone.Core/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Metadata/MetadataBase.cs index dbd613f1d..04be5860a 100644 --- a/src/NzbDrone.Core/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Metadata/MetadataBase.cs @@ -42,9 +42,9 @@ namespace NzbDrone.Core.Metadata public ProviderDefinition Definition { get; set; } - public abstract void OnSeriesUpdated(Series series, List existingMetadataFiles); + public abstract void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles); public abstract void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); - public abstract void AfterRename(Series series); + public abstract void AfterRename(Series series, List existingMetadataFiles, List episodeFiles); public abstract MetadataFile FindMetadataFile(Series series, string path); protected TSettings Settings @@ -55,11 +55,6 @@ namespace NzbDrone.Core.Metadata } } - protected virtual void EnsureFolder(string path) - { - _diskProvider.CreateFolder(path); - } - protected virtual void DownloadImage(Series series, string url, string path) { try diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index a731a0c86..86d7cc94f 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -4,13 +4,12 @@ using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; +using System.Web; using NLog; using NzbDrone.Common; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.Trakt; -using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Tv; -using Omu.ValueInjecter; using RestSharp; using Episode = NzbDrone.Core.Tv.Episode; using NzbDrone.Core.Rest; @@ -20,7 +19,8 @@ namespace NzbDrone.Core.MetadataSource public class TraktProxy : ISearchForNewSeries, IProvideSeriesInfo { private readonly Logger _logger; - private static readonly Regex InvalidSearchCharRegex = new Regex(@"[^a-zA-Z0-9\s-\.]", RegexOptions.Compiled); + private static readonly Regex CollapseSpaceRegex = new Regex(@"\s+", RegexOptions.Compiled); + private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!)", RegexOptions.Compiled); public TraktProxy(Logger logger) { @@ -166,9 +166,9 @@ namespace NzbDrone.Core.MetadataSource private static string GetSearchTerm(string phrase) { phrase = phrase.RemoveAccent().ToLower(); - phrase = phrase.Replace("&", "and"); - phrase = InvalidSearchCharRegex.Replace(phrase, string.Empty); - phrase = phrase.CleanSpaces().Replace(" ", "+"); + phrase = InvalidSearchCharRegex.Replace(phrase, ""); + phrase = CollapseSpaceRegex.Replace(phrase, " ").Trim().ToLower(); + phrase = HttpUtility.UrlEncode(phrase); return phrase; } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index f7fb1ed3b..58f28c7e4 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Notifications.Xbmc var parameters = new JObject( new JProperty("title", title), new JProperty("message", message), - new JProperty("image", "https://raw.github.com/NzbDrone/NzbDrone/master/Logo/64.png"), + new JProperty("image", "https://raw.github.com/NzbDrone/NzbDrone/develop/Logo/64.png"), new JProperty("displaytime", settings.DisplayTime * 1000)); var postJson = BuildJsonRequest("GUI.ShowNotification", parameters); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 89cb0b967..0c22a0648 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -198,6 +198,11 @@ + + + + Code + @@ -240,13 +245,27 @@ + + + + + + + + + + + + + + + + - - + - - + @@ -264,12 +283,14 @@ + + @@ -318,15 +339,16 @@ + + - @@ -476,10 +498,10 @@ - - - - + + + + @@ -529,18 +551,13 @@ - - - - - - - - - + + + + + - - + @@ -556,13 +573,10 @@ Code - - Code - Code - + Code @@ -631,7 +645,7 @@ Code - + @@ -652,6 +666,11 @@ + + + + + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 1ad4f3ec4..e2ebcdc31 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -247,7 +247,7 @@ namespace NzbDrone.Core.Organizer { string result = name; string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; - string[] goodCharacters = { "+", "+", "{", "}", "!", "@", "-", "#", "`" }; + string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" }; for (int i = 0; i < badCharacters.Length; i++) result = result.Replace(badCharacters[i], goodCharacters[i]); diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index fffc79c96..47adfe26b 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -34,6 +34,14 @@ namespace NzbDrone.Core.Parser.Model return AbsoluteEpisodeNumbers.Any(); } + public bool IsPossibleSpecialEpisode() + { + // if we dont have eny episode numbers we are likely a special episode and need to do a search by episode title + return String.IsNullOrWhiteSpace(AirDate) && + (EpisodeNumbers.Length == 0 || SeasonNumber == 0) && + String.IsNullOrWhiteSpace(SeriesTitle); + } + public override string ToString() { string episodeString = "[Unknown Episode]"; diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index d62f12f3d..db3b60d73 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports 1103/1113 naming - new Regex(@"^(?.+?)?(?:\W?(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)?(?:\W(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //4-digit episode number @@ -114,6 +114,13 @@ namespace NzbDrone.Core.Parser private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|\|)+", RegexOptions.Compiled); + private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); + private static readonly Regex CommonWordRegex = new Regex(@"\b(a|an|the|and|or|of)\b\s?", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition)\b\s?", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static ParsedEpisodeInfo ParsePath(string path) { var fileInfo = new FileInfo(path); @@ -219,6 +226,15 @@ namespace NzbDrone.Core.Parser return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); } + public static string NormalizeEpisodeTitle(string title) + { + string singleSpaces = WordDelimiterRegex.Replace(title, " "); + string noPunctuation = PunctuationRegex.Replace(singleSpaces, String.Empty); + string noCommonWords = CommonWordRegex.Replace(noPunctuation, String.Empty); + string normalized = SpecialEpisodeWordRegex.Replace(noCommonWords, String.Empty); + return normalized.Trim().ToLower(); + } + public static string ParseReleaseGroup(string title) { const string defaultReleaseGroup = "DRONE"; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index f8053db3d..e51a9c855 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using NLog; using NzbDrone.Common; @@ -13,37 +14,49 @@ namespace NzbDrone.Core.Parser { public interface IParsingService { - LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource); + LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource); Series GetSeries(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria = null); List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); + ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null); + ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, Series series); } public class ParsingService : IParsingService { private readonly IEpisodeService _episodeService; private readonly ISeriesService _seriesService; - private readonly IDiskProvider _diskProvider; private readonly ISceneMappingService _sceneMappingService; private readonly Logger _logger; public ParsingService(IEpisodeService episodeService, ISeriesService seriesService, - IDiskProvider diskProvider, ISceneMappingService sceneMappingService, Logger logger) { _episodeService = episodeService; _seriesService = seriesService; - _diskProvider = diskProvider; _sceneMappingService = sceneMappingService; _logger = logger; } - public LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource) + public LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource) { var parsedEpisodeInfo = Parser.ParsePath(filename); + // do we have a possible special episode? + if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode()) + { + // try to parse as a special episode + var title = Path.GetFileNameWithoutExtension(filename); + var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series); + if (specialEpisodeInfo != null) + { + // use special episode + parsedEpisodeInfo = specialEpisodeInfo; + } + } + if (parsedEpisodeInfo == null) { return null; @@ -212,6 +225,64 @@ namespace NzbDrone.Core.Parser return result; } + public ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null) + { + if (searchCriteria != null) + { + var tvdbId = _sceneMappingService.GetTvDbId(title); + if (tvdbId.HasValue) + { + if (searchCriteria.Series.TvdbId == tvdbId) + { + return ParseSpecialEpisodeTitle(title, searchCriteria.Series); + } + } + + if (tvRageId == searchCriteria.Series.TvRageId) + { + return ParseSpecialEpisodeTitle(title, searchCriteria.Series); + } + } + + var series = _seriesService.FindByTitleInexact(title); + if (series == null && tvRageId > 0) + { + series = _seriesService.FindByTvRageId(tvRageId); + } + + if (series == null) + { + _logger.Trace("No matching series {0}", title); + return null; + } + + return ParseSpecialEpisodeTitle(title, series); + } + + public ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, Series series) + { + // find special episode in series season 0 + var episode = _episodeService.FindEpisodeByName(series.Id, 0, title); + if (episode != null) + { + // create parsed info from tv episode + var info = new ParsedEpisodeInfo(); + info.SeriesTitle = series.Title; + info.SeriesTitleInfo = new SeriesTitleInfo(); + info.SeriesTitleInfo.Title = info.SeriesTitle; + info.SeasonNumber = episode.SeasonNumber; + info.EpisodeNumbers = new int[1] { episode.EpisodeNumber }; + info.FullSeason = false; + info.Quality = QualityParser.ParseQuality(title); + info.ReleaseGroup = Parser.ParseReleaseGroup(title); + + _logger.Info("Found special episode {0} for title '{1}'", info, title); + return info; + } + + return null; + } + private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria) { var tvdbId = _sceneMappingService.GetTvDbId(parsedEpisodeInfo.SeriesTitle); diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs index f73fb2de8..0b669f331 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Qualities { try { - return Query.Single(q => (int)q.Quality == qualityId); + return Query.Where(q => (int) q.Quality == qualityId).Single(); } catch (InvalidOperationException e) { diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 070c90fb6..963a2a747 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NLog; using NzbDrone.Core.Download; @@ -23,9 +24,24 @@ namespace NzbDrone.Core.Queue public List<Queue> GetQueue() { var downloadClient = _downloadClientProvider.GetDownloadClient(); - var queueItems = downloadClient.GetQueue(); - return MapQueue(queueItems); + if (downloadClient == null) + { + _logger.Trace("Download client is not configured."); + return new List<Queue>(); + } + + try + { + var queueItems = downloadClient.GetQueue(); + + return MapQueue(queueItems); + } + catch (Exception ex) + { + _logger.Error("Error getting queue from download client: " + downloadClient.ToString(), ex); + return new List<Queue>(); + } } private List<Queue> MapQueue(IEnumerable<QueueItem> queueItems) diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index b659f809d..ea5285327 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.RootFolders private readonly IDiskProvider _diskProvider; private readonly ISeriesRepository _seriesRepository; private readonly IConfigService _configService; + private readonly Logger _logger; private static readonly HashSet<string> SpecialFolders = new HashSet<string> { "$recycle.bin", "system volume information", "recycler", "lost+found" }; @@ -36,12 +37,14 @@ namespace NzbDrone.Core.RootFolders public RootFolderService(IRootFolderRepository rootFolderRepository, IDiskProvider diskProvider, ISeriesRepository seriesRepository, - IConfigService configService) + IConfigService configService, + Logger logger) { _rootFolderRepository = rootFolderRepository; _diskProvider = diskProvider; _seriesRepository = seriesRepository; _configService = configService; + _logger = logger; } public List<RootFolder> All() @@ -57,7 +60,7 @@ namespace NzbDrone.Core.RootFolders rootFolders.ForEach(folder => { - if (_diskProvider.FolderExists(folder.Path)) + if (folder.Path.IsPathValid() && _diskProvider.FolderExists(folder.Path)) { folder.FreeSpace = _diskProvider.GetAvailableSpace(folder.Path); folder.UnmappedFolders = GetUnmappedFolders(folder.Path); diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 58099d8c3..dcbe99e1e 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -37,22 +37,31 @@ namespace NzbDrone.Core.Tv public Episode Find(int seriesId, int season, int episodeNumber) { - return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.SeasonNumber == season && s.EpisodeNumber == episodeNumber); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.SeasonNumber == season) + .AndWhere(s => s.EpisodeNumber == episodeNumber) + .SingleOrDefault(); } public Episode Find(int seriesId, int absoluteEpisodeNumber) { - return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.AbsoluteEpisodeNumber == absoluteEpisodeNumber); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.AbsoluteEpisodeNumber == absoluteEpisodeNumber) + .SingleOrDefault(); } public Episode Get(int seriesId, String date) { - return Query.Single(s => s.SeriesId == seriesId && s.AirDate == date); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.AirDate == date) + .Single(); } public Episode Find(int seriesId, String date) { - return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.AirDate == date); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.AirDate == date) + .SingleOrDefault(); } public List<Episode> GetEpisodes(int seriesId) @@ -62,7 +71,9 @@ namespace NzbDrone.Core.Tv public List<Episode> GetEpisodes(int seriesId, int seasonNumber) { - return Query.Where(s => s.SeriesId == seriesId && s.SeasonNumber == seasonNumber).ToList(); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.SeasonNumber == seasonNumber) + .ToList(); } public List<Episode> GetEpisodeByFileId(int fileId) @@ -88,10 +99,12 @@ namespace NzbDrone.Core.Tv public Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber) { - return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.SceneSeasonNumber == seasonNumber && s.SceneEpisodeNumber == episodeNumber); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.SceneSeasonNumber == seasonNumber) + .AndWhere(s => s.SceneEpisodeNumber == episodeNumber) + .SingleOrDefault(); } - public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate) { return Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 96bfb1e52..df27033a9 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Tv Episode GetEpisode(int id); Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber, bool useScene = false); Episode FindEpisode(int seriesId, int absoluteEpisodeNumber); + Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle); Episode GetEpisode(int seriesId, String date); Episode FindEpisode(int seriesId, String date); List<Episode> GetEpisodeBySeries(int seriesId); @@ -88,6 +89,20 @@ namespace NzbDrone.Core.Tv return _episodeRepository.GetEpisodes(seriesId, seasonNumber); } + public Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle) + { + // TODO: can replace this search mechanism with something smarter/faster/better + var search = Parser.Parser.NormalizeEpisodeTitle(episodeTitle); + return _episodeRepository.GetEpisodes(seriesId, seasonNumber) + .FirstOrDefault(e => + { + // normalize episode title + string title = Parser.Parser.NormalizeEpisodeTitle(e.Title); + // find episode title within search string + return (title.Length > 0) && search.Contains(title); + }); + } + public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec) { var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, false); @@ -123,7 +138,6 @@ namespace NzbDrone.Core.Tv var episode = GetEpisode(episodeId); var seasonEpisodes = GetEpisodesBySeason(episode.SeriesId, episode.SeasonNumber); - //Ensure that this is either the first episode //or is the last episode in a season that has 10 or more episodes if (seasonEpisodes.First().EpisodeNumber == episode.EpisodeNumber || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().EpisodeNumber == episode.EpisodeNumber)) diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs index dbdc1c191..1819ac081 100644 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs @@ -25,28 +25,34 @@ namespace NzbDrone.Core.Tv public bool SeriesPathExists(string path) { - return Query.Any(c => c.Path == path); + return Query.Where(c => c.Path == path).Any(); } public Series FindByTitle(string cleanTitle) { - return Query.SingleOrDefault(s => s.CleanTitle.Equals(cleanTitle, StringComparison.InvariantCultureIgnoreCase)); + cleanTitle = cleanTitle.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanTitle) + .SingleOrDefault(); } public Series FindByTitle(string cleanTitle, int year) { - return Query.SingleOrDefault(s => s.CleanTitle.Equals(cleanTitle, StringComparison.InvariantCultureIgnoreCase) && - s.Year == year); + cleanTitle = cleanTitle.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanTitle) + .AndWhere(s => s.Year == year) + .SingleOrDefault(); } public Series FindByTvdbId(int tvdbId) { - return Query.SingleOrDefault(s => s.TvdbId.Equals(tvdbId)); + return Query.Where(s => s.TvdbId == tvdbId).SingleOrDefault(); } public Series FindByTvRageId(int tvRageId) { - return Query.SingleOrDefault(s => s.TvRageId.Equals(tvRageId)); + return Query.Where(s => s.TvRageId == tvRageId).SingleOrDefault(); } public void SetSeriesType(int seriesId, SeriesTypes seriesType) diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index ee7af6841..eeaa24054 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Tv Series FindByTvRageId(int tvRageId); Series FindByTitle(string title); Series FindByTitle(string title, int year); + Series FindByTitleInexact(string title); void SetSeriesType(int seriesId, SeriesTypes seriesTypes); void DeleteSeries(int seriesId, bool deleteFiles); List<Series> GetAllSeries(); @@ -100,6 +101,51 @@ namespace NzbDrone.Core.Tv return _seriesRepository.FindByTitle(Parser.Parser.CleanSeriesTitle(title)); } + public Series FindByTitleInexact(string title) + { + // find any series clean title within the provided release title + string cleanTitle = Parser.Parser.CleanSeriesTitle(title); + var list = _seriesRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList(); + if (!list.Any()) + { + // no series matched + return null; + } + else if (list.Count == 1) + { + // return the first series if there is only one + return list.Single(); + } + else + { + // build ordered list of series by position in the search string + var query = + list.Select(series => new + { + position = cleanTitle.IndexOf(series.CleanTitle), + length = series.CleanTitle.Length, + series = series + }) + .Where(s => (s.position>=0)) + .ToList() + .OrderBy(s => s.position) + .ThenByDescending(s => s.length) + .ToList(); + + // get the leftmost series that is the longest + // series are usually the first thing in release title, so we select the leftmost and longest match + var match = query.First().series; + + _logger.Trace("Multiple series matched {0} from title {1}", match.Title, title); + foreach (var entry in list) + { + _logger.Trace("Multiple series match candidate: {0} cleantitle: {1}", entry.Title, entry.CleanTitle); + } + + return match; + } + } + public Series FindByTitle(string title, int year) { return _seriesRepository.FindByTitle(title, year); diff --git a/src/NzbDrone.Api/Validation/PathValidator.cs b/src/NzbDrone.Core/Validation/FolderValidator.cs similarity index 74% rename from src/NzbDrone.Api/Validation/PathValidator.cs rename to src/NzbDrone.Core/Validation/FolderValidator.cs index f7cf37eab..daa3645bf 100644 --- a/src/NzbDrone.Api/Validation/PathValidator.cs +++ b/src/NzbDrone.Core/Validation/FolderValidator.cs @@ -1,11 +1,11 @@ using FluentValidation.Validators; using NzbDrone.Common; -namespace NzbDrone.Api.Validation +namespace NzbDrone.Core.Validation { - public class PathValidator : PropertyValidator + public class FolderValidator : PropertyValidator { - public PathValidator() + public FolderValidator() : base("Invalid Path") { } diff --git a/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs b/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs new file mode 100644 index 000000000..ec5447774 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs @@ -0,0 +1,29 @@ +using System; +using FluentValidation.Validators; +using NzbDrone.Common; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Validation.Paths +{ + public class DroneFactoryValidator : PropertyValidator + { + private readonly IConfigService _configService; + + public DroneFactoryValidator(IConfigService configService) + : base("Path is already used for drone factory") + { + _configService = configService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return false; + + var droneFactory = _configService.DownloadedEpisodesFolder; + + if (String.IsNullOrWhiteSpace(droneFactory)) return true; + + return !droneFactory.PathEquals(context.PropertyValue.ToString()); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/PathExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/PathExistsValidator.cs new file mode 100644 index 000000000..8e3e39aed --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/PathExistsValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Validation.Paths +{ + public class PathExistsValidator : PropertyValidator + { + private readonly IDiskProvider _diskProvider; + + public PathExistsValidator(IDiskProvider diskProvider) + : base("Path does not exist") + { + _diskProvider = diskProvider; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return false; + + return (_diskProvider.FolderExists(context.PropertyValue.ToString())); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/PathValidator.cs b/src/NzbDrone.Core/Validation/Paths/PathValidator.cs new file mode 100644 index 000000000..a77c76ae5 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/PathValidator.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using FluentValidation.Validators; +using NzbDrone.Common; + +namespace NzbDrone.Core.Validation.Paths +{ + public static class PathValidation + { + public static IRuleBuilderOptions<T, string> IsValidPath<T>(this IRuleBuilder<T, string> ruleBuilder) + { + return ruleBuilder.SetValidator(new PathValidator()); + } + } + + public class PathValidator : PropertyValidator + { + public PathValidator() + : base("Invalid Path") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return false; + return context.PropertyValue.ToString().IsPathValid(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs b/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs new file mode 100644 index 000000000..382056e24 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/RootFolderValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation.Validators; +using NzbDrone.Common; +using NzbDrone.Core.RootFolders; + +namespace NzbDrone.Core.Validation.Paths +{ + public class RootFolderValidator : PropertyValidator + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderValidator(IRootFolderService rootFolderService) + : base("Path is already configured as a root folder") + { + _rootFolderService = rootFolderService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return (!_rootFolderService.All().Exists(r => r.Path.PathEquals(context.PropertyValue.ToString()))); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs index 79a2b3579..1804c077d 100644 --- a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs +++ b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NLog; using NzbDrone.Common.Model; using NzbDrone.Common.Processes; @@ -9,10 +10,12 @@ namespace NzbDrone.Mono public class NzbDroneProcessProvider : INzbDroneProcessProvider { private readonly IProcessProvider _processProvider; + private readonly Logger _logger; - public NzbDroneProcessProvider(IProcessProvider processProvider) + public NzbDroneProcessProvider(IProcessProvider processProvider, Logger logger) { _processProvider = processProvider; + _logger = logger; } public List<ProcessInfo> FindNzbDroneProcesses() @@ -21,10 +24,19 @@ namespace NzbDrone.Mono return monoProcesses.Where(c => { - var processArgs = _processProvider.StartAndCapture("ps", String.Format("-p {0} -o args=", c.Id)); + try + { + var processArgs = _processProvider.StartAndCapture("ps", String.Format("-p {0} -o args=", c.Id)); - return processArgs.Standard.Any(p => p.Contains(ProcessProvider.NZB_DRONE_PROCESS_NAME + ".exe") || - p.Contains(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME + ".exe")); + return processArgs.Standard.Any(p => p.Contains(ProcessProvider.NZB_DRONE_PROCESS_NAME + ".exe") || + p.Contains(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME + ".exe")); + } + catch (InvalidOperationException ex) + { + _logger.WarnException("Error getting process arguments", ex); + return false; + } + }).ToList(); } } diff --git a/src/UI/.idea/jsLinters/jshint.xml b/src/UI/.idea/jsLinters/jshint.xml index e85398a55..4e0df49ad 100644 --- a/src/UI/.idea/jsLinters/jshint.xml +++ b/src/UI/.idea/jsLinters/jshint.xml @@ -8,16 +8,16 @@ <option es3="false" /> <option forin="true" /> <option immed="true" /> + <option latedef="true" /> <option newcap="true" /> <option noarg="true" /> <option noempty="false" /> <option nonew="true" /> <option plusplus="false" /> <option undef="true" /> + <option unused="true" /> <option strict="true" /> <option trailing="false" /> - <option latedef="true" /> - <option unused="true" /> <option quotmark="single" /> <option maxdepth="3" /> <option asi="false" /> diff --git a/src/UI/AddSeries/AddSeriesLayoutTemplate.html b/src/UI/AddSeries/AddSeriesLayoutTemplate.html index 88dc147aa..b9ccc6c17 100644 --- a/src/UI/AddSeries/AddSeriesLayoutTemplate.html +++ b/src/UI/AddSeries/AddSeriesLayoutTemplate.html @@ -20,3 +20,4 @@ <!--</div>--> </div> <div id="add-series-workspace"/> + diff --git a/src/UI/AddSeries/AddSeriesView.js b/src/UI/AddSeries/AddSeriesView.js index 179cc4a52..951d3431b 100644 --- a/src/UI/AddSeries/AddSeriesView.js +++ b/src/UI/AddSeries/AddSeriesView.js @@ -71,7 +71,6 @@ define( }, onShow: function () { - this.searchResult.show(this.resultCollectionView); this.ui.seriesSearch.focus(); }, diff --git a/src/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js b/src/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js index 45659ffd8..7de5aee81 100644 --- a/src/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js +++ b/src/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js @@ -6,9 +6,15 @@ define( 'AddSeries/Existing/UnmappedFolderCollection' ], function (Marionette, AddSeriesView, UnmappedFolderCollection) { - return Marionette.CollectionView.extend({ + return Marionette.CompositeView.extend({ - itemView: AddSeriesView, + itemView : AddSeriesView, + itemViewContainer: '.x-loading-folders', + template : 'AddSeries/Existing/AddExistingSeriesCollectionViewTemplate', + + ui: { + loadingFolders: '.x-loading-folders' + }, initialize: function () { this.collection = new UnmappedFolderCollection(); @@ -19,6 +25,10 @@ define( this._showAndSearch(0); }, + appendHtml: function(collectionView, itemView, index){ + collectionView.ui.loadingFolders.before(itemView.el); + }, + _showAndSearch: function (index) { var self = this; var model = this.collection.at(index); @@ -35,6 +45,10 @@ define( } }); } + + else { + this.ui.loadingFolders.hide(); + } }, itemViewOptions: { diff --git a/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.html b/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.html new file mode 100644 index 000000000..ca693b1f6 --- /dev/null +++ b/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.html @@ -0,0 +1,5 @@ +<div class="x-existing-folders"> + <div class="loading-folders x-loading-folders"> + Loading search results from trakt for your series, this may take a few minutes. + </div> +</div> \ No newline at end of file diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html index 390b3c056..c8ba616c5 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html +++ b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html @@ -4,6 +4,7 @@ </div> <div class="modal-body root-folders-modal"> <div class="validation-errors"></div> + <div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> <div class="input-prepend input-append x-path control-group"> <span class="add-on"> <i class="icon-folder-open"></i></span> <input class="span9" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> diff --git a/src/UI/AddSeries/SearchResultViewTemplate.html b/src/UI/AddSeries/SearchResultViewTemplate.html index 04747d8cb..5a266ecf0 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.html +++ b/src/UI/AddSeries/SearchResultViewTemplate.html @@ -17,7 +17,17 @@ </h2> </div> <div class="row new-series-overview x-overview"> - {{overview}} + <div class="overview-internal"> + {{overview}} + </div> + </div> + <div class="row labels"> + {{#unless path}} + <div class="span4">Path</div> + {{/unless}} + + <div class="span1 starting-season starting-season-label">Starting Season</div> + <div class="span2">Quality Profile</div> </div> <div class="row"> <form class="form-inline"> diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index ee04cb289..f690e6413 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -57,6 +57,8 @@ .search-item { + padding-bottom : 20px; + .series-title { .label { margin-left: 15px; @@ -65,8 +67,13 @@ } .new-series-overview { - overflow : hidden; - height : 120px; + overflow : hidden; + height : 103px; + + .overview-internal { + overflow : hidden; + height : 80px; + } } .new-series-poster { @@ -77,13 +84,14 @@ margin : 10px; } - padding-bottom : 20px; a { color : #343434; } + a:hover { text-decoration : none; } + select { font-size : 16px; } @@ -102,7 +110,22 @@ .starting-season { width: 140px; + + &.starting-season-label { + display: inline-block; + } } + + .labels { + [class*="span"] { + margin-left: 3px; + } + } + } + + .loading-folders { + margin : 30px 0px; + text-align: center; } } diff --git a/src/UI/Form/FormBuilder.js b/src/UI/Form/FormBuilder.js index bb391f162..3eade017f 100644 --- a/src/UI/Form/FormBuilder.js +++ b/src/UI/Form/FormBuilder.js @@ -35,6 +35,13 @@ define( ]); } + if (field.type === 'path') { + return _templateRenderer.apply(field, + [ + 'Form/PathTemplate' + ]); + } + return _templateRenderer.apply(field, [ 'Form/TextboxTemplate' diff --git a/src/UI/Form/PathTemplate.html b/src/UI/Form/PathTemplate.html new file mode 100644 index 000000000..6b5d16bc6 --- /dev/null +++ b/src/UI/Form/PathTemplate.html @@ -0,0 +1,12 @@ +<div class="control-group"> + <label class="control-label">{{label}}</label> + + <div class="controls"> + <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="x-path"/> + {{#if helpText}} + <span class="help-inline"> + <i class="icon-nd-form-info" title="{{helpText}}"/> + </span> + {{/if}} + </div> +</div> diff --git a/src/UI/History/Queue/QueueCollection.js b/src/UI/History/Queue/QueueCollection.js index a66393435..b5c32e6a3 100644 --- a/src/UI/History/Queue/QueueCollection.js +++ b/src/UI/History/Queue/QueueCollection.js @@ -3,13 +3,20 @@ define( [ 'underscore', 'backbone', + 'backbone.pageable', 'History/Queue/QueueModel', 'Mixins/backbone.signalr.mixin' - ], function (_, Backbone, QueueModel) { - var QueueCollection = Backbone.Collection.extend({ + ], function (_, Backbone, PageableCollection, QueueModel) { + var QueueCollection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/queue', model: QueueModel, + state: { + pageSize: 15 + }, + + mode: 'client', + findEpisode: function (episodeId) { return _.find(this.models, function (queueModel) { return queueModel.get('episode').id === episodeId; diff --git a/src/UI/History/Queue/QueueLayout.js b/src/UI/History/Queue/QueueLayout.js index 612ec0297..b20044fd8 100644 --- a/src/UI/History/Queue/QueueLayout.js +++ b/src/UI/History/Queue/QueueLayout.js @@ -9,7 +9,8 @@ define( 'Cells/EpisodeTitleCell', 'Cells/QualityCell', 'History/Queue/QueueStatusCell', - 'History/Queue/TimeleftCell' + 'History/Queue/TimeleftCell', + 'Shared/Grid/Pager' ], function (Marionette, Backgrid, QueueCollection, @@ -18,12 +19,14 @@ define( EpisodeTitleCell, QualityCell, QueueStatusCell, - TimeleftCell) { + TimeleftCell, + GridPager) { return Marionette.Layout.extend({ template: 'History/Queue/QueueLayoutTemplate', regions: { - table: '#x-queue' + table: '#x-queue', + pager: '#x-queue-pager' }, columns: @@ -65,7 +68,6 @@ define( } ], - initialize: function () { this.listenTo(QueueCollection, 'sync', this._showTable); }, @@ -80,6 +82,11 @@ define( collection: QueueCollection, className : 'table table-hover' })); + + this.pager.show(new GridPager({ + columns : this.columns, + collection: QueueCollection + })); } }); }); diff --git a/src/UI/History/Queue/QueueLayoutTemplate.html b/src/UI/History/Queue/QueueLayoutTemplate.html index 113673518..89041b644 100644 --- a/src/UI/History/Queue/QueueLayoutTemplate.html +++ b/src/UI/History/Queue/QueueLayoutTemplate.html @@ -3,3 +3,9 @@ <div id="x-queue"/> </div> </div> + +<div class="row"> + <div class="span12"> + <div id="x-queue-pager"/> + </div> +</div> \ No newline at end of file diff --git a/src/UI/History/Table/HistoryTableLayout.js b/src/UI/History/Table/HistoryTableLayout.js index 3571c69d1..f4a9dcd04 100644 --- a/src/UI/History/Table/HistoryTableLayout.js +++ b/src/UI/History/Table/HistoryTableLayout.js @@ -79,7 +79,6 @@ define( } ], - initialize: function () { this.collection = new HistoryCollection({ tableName: 'history' }); this.listenTo(this.collection, 'sync', this._showTable); @@ -104,6 +103,5 @@ define( this.history.show(new LoadingView()); this.collection.fetch(); } - }); }); diff --git a/src/UI/Navbar/Search.js b/src/UI/Navbar/Search.js index db986c87b..324c7c5d4 100644 --- a/src/UI/Navbar/Search.js +++ b/src/UI/Navbar/Search.js @@ -10,7 +10,7 @@ define( return; } - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey || e.altKey) { return; } diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js new file mode 100644 index 000000000..e6f557dc1 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'marionette', + 'Settings/DownloadClient/Add/DownloadClientAddItemView' +], function (Marionette, AddItemView) { + + return Marionette.CompositeView.extend({ + itemView : AddItemView, + itemViewContainer: '.add-download-client .items', + template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate', + + itemViewOptions: function () { + return { + downloadClientCollection: this.downloadClientCollection + }; + }, + + initialize: function (options) { + this.downloadClientCollection = options.downloadClientCollection; + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html new file mode 100644 index 000000000..0dc1bb250 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html @@ -0,0 +1,12 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add Download Client</h3> +</div> +<div class="modal-body"> + <div class="add-download-client add-thingies"> + <ul class="items"></ul> + </div> +</div> +<div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> +</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js new file mode 100644 index 000000000..beab52273 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js @@ -0,0 +1,36 @@ +'use strict'; + +define([ + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Edit/DownloadClientEditView' +], function (AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', + tagName : 'li', + + events: { + 'click': '_add' + }, + + initialize: function (options) { + this.downloadClientCollection = options.downloadClientCollection; + }, + + _add: function (e) { + if (this.$(e.target).hasClass('icon-info-sign')) { + return; + } + + this.model.set({ + id : undefined, + name : this.model.get('implementationName'), + enable : true + }); + + var editView = new EditView({ model: this.model, downloadClientCollection: this.downloadClientCollection }); + AppLayout.modalRegion.show(editView); + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html new file mode 100644 index 000000000..dfaee211e --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html @@ -0,0 +1,10 @@ +<div class="add-thingy span3"> + <div class="row"> + <div class="span3"> + {{implementation}} + {{#if link}} + <a href="{{link}}"><i class="icon-info-sign"/></a> + {{/if}} + </div> + </div> +</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/SchemaModal.js b/src/UI/Settings/DownloadClient/Add/SchemaModal.js new file mode 100644 index 000000000..dac0dca63 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/SchemaModal.js @@ -0,0 +1,20 @@ +'use strict'; +define([ + 'AppLayout', + 'Settings/DownloadClient/DownloadClientCollection', + 'Settings/DownloadClient/Add/DownloadClientAddCollectionView' +], function (AppLayout, DownloadClientCollection, DownloadClientAddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new DownloadClientCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var view = new DownloadClientAddCollectionView({ collection: schemaCollection, downloadClientCollection: collection}); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/BlackholeView.js b/src/UI/Settings/DownloadClient/BlackholeView.js deleted file mode 100644 index c9d91001b..000000000 --- a/src/UI/Settings/DownloadClient/BlackholeView.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AutoComplete', - 'bootstrap' - ], function (Marionette, AsModelBoundView) { - - var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/BlackholeViewTemplate', - - ui: { - 'blackholeFolder': '.x-path' - }, - - onShow: function () { - this.ui.blackholeFolder.autoComplete('/directories'); - } - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/DownloadClient/BlackholeViewTemplate.html b/src/UI/Settings/DownloadClient/BlackholeViewTemplate.html deleted file mode 100644 index e41520b2f..000000000 --- a/src/UI/Settings/DownloadClient/BlackholeViewTemplate.html +++ /dev/null @@ -1,13 +0,0 @@ -<fieldset> - <legend>Blackhole</legend> - <div class="control-group"> - <label class="control-label">Blackhole Folder</label> - - <div class="controls"> - <input type="text" name="blackholeFolder" class="x-path"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="The folder where your download client will pickup .nzb files"/> - </span> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js new file mode 100644 index 000000000..502d57e7f --- /dev/null +++ b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js @@ -0,0 +1,23 @@ +'use strict'; +define( + [ + 'vent', + 'marionette' + ], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', + + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } + }); + }); diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html new file mode 100644 index 000000000..b4fff099b --- /dev/null +++ b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html @@ -0,0 +1,11 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Delete Download Client</h3> +</div> +<div class="modal-body"> + <p>Are you sure you want to delete '{{name}}'?</p> +</div> +<div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-delete">delete</button> +</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollection.js b/src/UI/Settings/DownloadClient/DownloadClientCollection.js new file mode 100644 index 000000000..6166da3e4 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientCollection.js @@ -0,0 +1,12 @@ +'use strict'; +define( + [ + 'backbone', + 'Settings/DownloadClient/DownloadClientModel' + ], function (Backbone, DownloadClientModel) { + + return Backbone.Collection.extend({ + model: DownloadClientModel, + url : window.NzbDrone.ApiRoot + '/downloadclient' + }); + }); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js new file mode 100644 index 000000000..4a11cb167 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js @@ -0,0 +1,31 @@ +'use strict'; +define( + [ + 'underscore', + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/DownloadClientItemView', + 'Settings/DownloadClient/Add/SchemaModal' + ], function (_, AppLayout, Marionette, DownloadClientItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : DownloadClientItemView, + itemViewContainer: '#x-download-clients', + template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', + + ui: { + 'addCard': '.x-add-card' + }, + + events: { + 'click .x-add-card': '_openSchemaModal' + }, + + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } + }); + }); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html new file mode 100644 index 000000000..9b9692658 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html @@ -0,0 +1,16 @@ +<fieldset> + <legend>Download Clients</legend> + <div class="row"> + <div class="span12"> + <ul id="x-download-clients" class="download-client-list"> + <li> + <div class="download-client-item thingy add-card x-add-card"> + <span class="center well"> + <i class="icon-plus" title="Add Download Client"/> + </span> + </div> + </li> + </ul> + </div> + </div> +</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemView.js b/src/UI/Settings/DownloadClient/DownloadClientItemView.js new file mode 100644 index 000000000..0d021c059 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientItemView.js @@ -0,0 +1,34 @@ +'use strict'; + +define( + [ + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Edit/DownloadClientEditView', + 'Settings/DownloadClient/Delete/DownloadClientDeleteView' + ], function (AppLayout, Marionette, EditView, DeleteView) { + + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/DownloadClientItemViewTemplate', + tagName : 'li', + + events: { + 'click .x-edit' : '_edit', + 'click .x-delete' : '_delete' + }, + + initialize: function () { + this.listenTo(this.model, 'sync', this.render); + }, + + _edit: function () { + var view = new EditView({ model: this.model, downloadClientCollection: this.model.collection }); + AppLayout.modalRegion.show(view); + }, + + _delete: function () { + var view = new DeleteView({ model: this.model}); + AppLayout.modalRegion.show(view); + } + }); + }); diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html new file mode 100644 index 000000000..e8550d7f5 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html @@ -0,0 +1,17 @@ +<div class="download-client-item thingy"> + <div> + <h3>{{name}}</h3> + <span class="btn-group pull-right"> + <button class="btn btn-mini btn-icon-only x-edit"><i class="icon-nd-edit"/></button> + <button class="btn btn-mini btn-icon-only x-delete"><i class="icon-nd-delete"/></button> + </span> + </div> + + <div class="settings"> + {{#if enable}} + <span class="label label-success">Enabled</span> + {{else}} + <span class="label">Not Enabled</span> + {{/if}} + </div> +</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayout.js b/src/UI/Settings/DownloadClient/DownloadClientLayout.js new file mode 100644 index 000000000..e632371dc --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientLayout.js @@ -0,0 +1,32 @@ +'use strict'; + +define( + [ + 'marionette', + 'Settings/DownloadClient/DownloadClientCollection', + 'Settings/DownloadClient/DownloadClientCollectionView', + 'Settings/DownloadClient/Options/DownloadClientOptionsView', + 'Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView' + ], function (Marionette, DownloadClientCollection, DownloadClientCollectionView, DownloadClientOptionsView, FailedDownloadHandlingView) { + + return Marionette.Layout.extend({ + template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', + + regions: { + downloadClients : '#x-download-clients-region', + downloadClientOptions : '#x-download-client-options-region', + failedDownloadHandling : '#x-failed-download-handling-region' + }, + + initialize: function () { + this.downloadClientCollection = new DownloadClientCollection(); + this.downloadClientCollection.fetch(); + }, + + onShow: function () { + this.downloadClients.show(new DownloadClientCollectionView({ collection: this.downloadClientCollection })); + this.downloadClientOptions.show(new DownloadClientOptionsView({ model: this.model })); + this.failedDownloadHandling.show(new FailedDownloadHandlingView({ model: this.model })); + } + }); + }); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html new file mode 100644 index 000000000..365590417 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html @@ -0,0 +1,6 @@ +<div id="x-download-clients-region"></div> +<div class="form-horizontal"> + <div id="x-download-client-options-region"></div> + <div id="x-failed-download-handling-region"></div> +</div> + diff --git a/src/UI/Settings/DownloadClient/DownloadClientModel.js b/src/UI/Settings/DownloadClient/DownloadClientModel.js new file mode 100644 index 000000000..5e08858af --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientModel.js @@ -0,0 +1,10 @@ +'use strict'; +define( + [ + 'backbone.deepmodel' + ], function (DeepModel) { + return DeepModel.DeepModel.extend({ + + }); + }); + diff --git a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js new file mode 100644 index 000000000..8a3b066b3 --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'Settings/SettingsModelBase' + ], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/downloadclient', + successMessage: 'Download client settings saved', + errorMessage : 'Failed to save download client settings' + }); + }); diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js new file mode 100644 index 000000000..6f75aaf8f --- /dev/null +++ b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js @@ -0,0 +1,97 @@ +'use strict'; + +define( + [ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Delete/DownloadClientDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder', + 'Mixins/AutoComplete', + 'bootstrap' + ], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', + + ui: { + path : '.x-path', + modalBody : '.modal-body' + }, + + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-test' : '_test' + }, + + initialize: function (options) { + this.downloadClientCollection = options.downloadClientCollection; + }, + + onShow: function () { + //Hack to deal with modals not overflowing + if (this.ui.path.length > 0) { + this.ui.modalBody.addClass('modal-overflow'); + } + + this.ui.path.autoComplete('/directories'); + }, + + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.downloadClientCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.notificationCollection.add(self.model, { merge: true }); + + require('Settings/DownloadClient/Add/SchemaModal').open(self.downloadClientCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + require('Settings/DownloadClient/Add/SchemaModal').open(this.downloadClientCollection); + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html new file mode 100644 index 000000000..53aa004dd --- /dev/null +++ b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html @@ -0,0 +1,59 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + {{#if id}} + <h3>Edit - {{implementation}}</h3> + {{else}} + <h3>Add - {{implementation}}</h3> + {{/if}} +</div> +<div class="modal-body download-client-modal"> + <div class="form-horizontal"> + <div class="control-group"> + <label class="control-label">Name</label> + + <div class="controls"> + <input type="text" name="name"/> + </div> + </div> + + <div class="control-group"> + <label class="control-label">Enable</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enable"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div> + + {{formBuilder}} + </div> +</div> +<div class="modal-footer"> + {{#if id}} + <button class="btn btn-danger pull-left x-delete">delete</button> + {{else}} + <button class="btn pull-left x-back">back</button> + {{/if}} + + <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> + <button class="btn" data-dismiss="modal">cancel</button> + + <div class="btn-group"> + <button class="btn btn-primary x-save">save</button> + <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li class="save-and-add x-save-and-add"> + save and add + </li> + </ul> + </div> +</div> diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js new file mode 100644 index 000000000..9af62d5dc --- /dev/null +++ b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js @@ -0,0 +1,37 @@ +'use strict'; +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (Marionette, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate', + + ui: { + failedDownloadHandlingCheckbox: '.x-failed-download-handling', + failedDownloadOptions : '.x-failed-download-options' + }, + + events: { + 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' + }, + + _setFailedDownloadOptionsVisibility: function () { + var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.failedDownloadOptions.slideDown(); + } + + else { + this.ui.failedDownloadOptions.slideUp(); + } + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html new file mode 100644 index 000000000..90c7764e0 --- /dev/null +++ b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html @@ -0,0 +1,65 @@ +<fieldset class="advanced-setting"> + <legend>Failed Download Handling</legend> + + <div class="control-group"> + <label class="control-label">Enable</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enableFailedDownloadHandling" class="x-failed-download-handling"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Process failed downloads and blacklist the release"/> + </span> + </div> + </div> + + <div class="x-failed-download-options"> + <div class="control-group"> + <label class="control-label">Redownload</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="autoRedownloadFailed"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release when a download fails?"/> + </span> + </div> + </div> + + <div class="control-group"> + <label class="control-label">Remove</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="removeFailedDownloads"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Automatically remove failed downloads from history and encrypted downloads from queue?"/> + </span> + </div> + </div> + </div> +</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Layout.js b/src/UI/Settings/DownloadClient/Layout.js deleted file mode 100644 index 730ec1cd7..000000000 --- a/src/UI/Settings/DownloadClient/Layout.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Settings/DownloadClient/SabView', - 'Settings/DownloadClient/BlackholeView', - 'Settings/DownloadClient/PneumaticView', - 'Settings/DownloadClient/NzbgetView', - 'Mixins/AsModelBoundView', - 'Mixins/AutoComplete', - 'bootstrap' - ], function (Marionette, SabView, BlackholeView, PneumaticView, NzbgetView, AsModelBoundView) { - - var view = Marionette.Layout.extend({ - template : 'Settings/DownloadClient/LayoutTemplate', - - regions: { - downloadClient: '#download-client-settings-region' - }, - - ui: { - downloadClientSelect: '.x-download-client', - downloadedEpisodesFolder: '.x-path' - }, - - events: { - 'change .x-download-client': 'downloadClientChanged' - }, - - onShow: function () { - this.sabView = new SabView({ model: this.model}); - this.blackholeView = new BlackholeView({ model: this.model}); - this.pneumaticView = new PneumaticView({ model: this.model}); - this.nzbgetView = new NzbgetView({ model: this.model}); - - this.ui.downloadedEpisodesFolder.autoComplete('/directories'); - - var client = this.model.get('downloadClient'); - this.refreshUIVisibility(client); - }, - - downloadClientChanged: function () { - var clientId = this.ui.downloadClientSelect.val(); - this.refreshUIVisibility(clientId); - }, - - refreshUIVisibility: function (clientId) { - - if (!clientId) { - clientId = 'sabnzbd'; - } - - switch (clientId.toString()) { - case 'sabnzbd': - this.downloadClient.show(this.sabView); - break; - - case 'blackhole': - this.downloadClient.show(this.blackholeView); - break; - - case 'pneumatic': - this.downloadClient.show(this.pneumaticView); - break; - - case 'nzbget': - this.downloadClient.show(this.nzbgetView); - break; - - default : - throw 'unknown download client id' + clientId; - } - } - }); - - return AsModelBoundView.call(view); - }); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/LayoutTemplate.html b/src/UI/Settings/DownloadClient/LayoutTemplate.html deleted file mode 100644 index 54c0c81f6..000000000 --- a/src/UI/Settings/DownloadClient/LayoutTemplate.html +++ /dev/null @@ -1,29 +0,0 @@ -<fieldset class="form-horizontal"> - <legend>General</legend> - - <div class="control-group"> - <label class="control-label">Download Client</label> - - <div class="controls"> - <select class="inputClass x-download-client" name="downloadClient"> - <option value="sabnzbd">SABnzbd</option> - <option value="blackhole">Blackhole</option> - <option value="pneumatic">Pneumatic</option> - <option value="nzbget">NZBGet</option> - </select> - </div> - </div> - <div class="control-group"> - <label class="control-label">Drone Factory</label> - - <div class="controls"> - <input type="text" name="downloadedEpisodesFolder" class="x-path"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="The folder where your download client downloads TV shows to (Completed Download Directory)"/> - <i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i> - </span> - </div> - </div> -</fieldset> - -<div id="download-client-settings-region" class="form-horizontal"></div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/NzbgetView.js b/src/UI/Settings/DownloadClient/NzbgetView.js deleted file mode 100644 index 601df8457..000000000 --- a/src/UI/Settings/DownloadClient/NzbgetView.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'bootstrap' - ], function (Marionette, AsModelBoundView) { - - var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/NzbgetViewTemplate' - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/DownloadClient/NzbgetViewTemplate.html b/src/UI/Settings/DownloadClient/NzbgetViewTemplate.html deleted file mode 100644 index cae6ec715..000000000 --- a/src/UI/Settings/DownloadClient/NzbgetViewTemplate.html +++ /dev/null @@ -1,86 +0,0 @@ -<fieldset> - <legend>NZBGet</legend> - <div class="control-group"> - <label class="control-label">Host</label> - - <div class="controls"> - <input type="text" name="nzbgetHost"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Port</label> - - <div class="controls"> - <input type="text" name="nzbgetPort"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">API Key</label> - - <div class="controls"> - <input type="text" name="nzbgetApiKey"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Username</label> - - <div class="controls"> - <input type="text" name="nzbgetUsername"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Password</label> - - <div class="controls"> - <input type="password" name="nzbgetPassword"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">TV Category</label> - - <div class="controls"> - <input type="text" name="nzbgetTvCategory"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Download Priority</label> - - <div class="controls"> - <select name="nzbgetRecentTvPriority"> - <option value="default">Default</option> - <option value="pasued">Paused</option> - <option value="low">Low</option> - <option value="normal">Normal</option> - <option value="high">High</option> - <option value="force">Force</option> - </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Priority to use when sending episodes that aired within the last 14 days"/> - </span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Older Download Priority</label> - - <div class="controls"> - <select name="nzbgetOlderTvPriority"> - <option value="default">Default</option> - <option value="pasued">Paused</option> - <option value="low">Low</option> - <option value="normal">Normal</option> - <option value="high">High</option> - <option value="force">Force</option> - </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Priority to use when sending episodes that aired over 14 days ago"/> - </span> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js b/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js new file mode 100644 index 000000000..444bed1d8 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js @@ -0,0 +1,26 @@ +'use strict'; +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'Mixins/AutoComplete' + ], function (Marionette, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate', + + ui: { + droneFactory : '.x-path' + }, + + onShow: function () { + this.ui.droneFactory.autoComplete('/directories'); + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html b/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html new file mode 100644 index 000000000..888161027 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html @@ -0,0 +1,14 @@ +<fieldset"> + <legend>Options</legend> + <div class="control-group"> + <label class="control-label">Drone Factory</label> + + <div class="controls"> + <input type="text" name="downloadedEpisodesFolder" class="x-path"/> + <span class="help-inline"> + <i class="icon-nd-form-info" title="The folder where your download client downloads TV shows to (Completed Download Directory)"/> + <i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i> + </span> + </div> + </div> +</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/PneumaticView.js b/src/UI/Settings/DownloadClient/PneumaticView.js deleted file mode 100644 index 524c19eae..000000000 --- a/src/UI/Settings/DownloadClient/PneumaticView.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AutoComplete', - 'bootstrap' - ], function (Marionette, AsModelBoundView) { - - var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/PneumaticViewTemplate', - - ui: { - 'pneumaticFolder': '.x-path' - }, - - onShow: function () { - this.ui.pneumaticFolder.autoComplete('/directories'); - } - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/DownloadClient/PneumaticViewTemplate.html b/src/UI/Settings/DownloadClient/PneumaticViewTemplate.html deleted file mode 100644 index 8fc612fc3..000000000 --- a/src/UI/Settings/DownloadClient/PneumaticViewTemplate.html +++ /dev/null @@ -1,13 +0,0 @@ -<fieldset> - <legend>Pneumatic</legend> - <div class="control-group"> - <label class="control-label">Nzb Folder</label> - - <div class="controls"> - <input type="text" name="pneumaticFolder" class="x-path"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Folder to save NZBs for Pneumatic<br/>must be accessible from XBMC"></i> - </span> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/DownloadClient/SabView.js b/src/UI/Settings/DownloadClient/SabView.js deleted file mode 100644 index ce6da37e5..000000000 --- a/src/UI/Settings/DownloadClient/SabView.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'bootstrap' - ], function (Marionette, AsModelBoundView) { - - var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/SabViewTemplate' - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/DownloadClient/SabViewTemplate.html b/src/UI/Settings/DownloadClient/SabViewTemplate.html deleted file mode 100644 index 9b997ae07..000000000 --- a/src/UI/Settings/DownloadClient/SabViewTemplate.html +++ /dev/null @@ -1,120 +0,0 @@ -<fieldset> - <legend>SABnzbd</legend> - - {{!<div class="control-group"> - <label class="control-label">Auto-Configure</label> - - <div class="controls"> - <input type="button" value="Auto-Configure" class="btn btn-inverse"/> - <span class="help-inline"> - <i class="icon-nd-form-info" - title="(Windows only) If access to SABnzbd doesn't require a username & password and it is on the same system as NzbDrone, you can auto-configure it"/> - </span> - </div> - </div>}} - - <div class="control-group"> - <label class="control-label">Host</label> - - <div class="controls"> - <input type="text" name="sabHost"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Port</label> - - <div class="controls"> - <input type="text" name="sabPort"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">API Key</label> - - <div class="controls"> - <input type="text" name="sabApiKey"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Username</label> - - <div class="controls"> - <input type="text" name="sabUsername"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Password</label> - - <div class="controls"> - <input type="password" name="sabPassword"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">TV Category</label> - - <div class="controls"> - <input type="text" name="sabTvCategory" placeholder="This is not the dropdownlist you're looking for"/> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Download Priority</label> - - <div class="controls"> - <select name="sabRecentTvPriority"> - <option value="default">Default</option> - <option value="paused">Paused</option> - <option value="low">Low</option> - <option value="normal">Normal</option> - </option> <option value="high">High</option> - <option value="force">Force</option> - </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Priority to use when sending episodes that aired within the last 14 days"/> - </span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Older Download Priority</label> - - <div class="controls"> - <select name="sabOlderTvPriority"> - <option value="default">Default</option> - <option value="paused">Paused</option> - <option value="low">Low</option> - <option value="normal">Normal</option> - <option value="high">High</option> - <option value="force">Force</option> - </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Priority to use when sending episodes that aired over 14 days ago"/> - </span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Use SSL</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="sabUseSsl" class="x-ssl"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Connect to SABnzbd over SSL"/> - </span> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less new file mode 100644 index 000000000..a41bec135 --- /dev/null +++ b/src/UI/Settings/DownloadClient/downloadclient.less @@ -0,0 +1,27 @@ +.download-client-list { + li { + display: inline-block; + vertical-align: top; + } +} + +.download-client-item { + + width: 290px; + height: 90px; + padding: 10px 15px; + + h3 { + width: 230px; + } + + &.add-card { + .center { + margin-top: 15px; + } + } +} + +.modal-overflow { + overflow-y: visible; +} \ No newline at end of file diff --git a/src/UI/Settings/General/GeneralSettingsModel.js b/src/UI/Settings/General/GeneralSettingsModel.js index c458cec87..5274cfbd8 100644 --- a/src/UI/Settings/General/GeneralSettingsModel.js +++ b/src/UI/Settings/General/GeneralSettingsModel.js @@ -5,9 +5,8 @@ define( ], function (SettingsModelBase) { return SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/settings/host', + url : window.NzbDrone.ApiRoot + '/config/host', successMessage: 'General settings saved', errorMessage : 'Failed to save general settings' - }); }); diff --git a/src/UI/Settings/General/GeneralView.js b/src/UI/Settings/General/GeneralView.js index 6971dc5f5..df61a4c71 100644 --- a/src/UI/Settings/General/GeneralView.js +++ b/src/UI/Settings/General/GeneralView.js @@ -2,10 +2,11 @@ define( [ 'marionette', - 'Mixins/AsModelBoundView' - ], function (Marionette, AsModelBoundView) { + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ - template: 'Settings/General/GeneralTemplate', + template: 'Settings/General/GeneralViewTemplate', events: { 'change .x-auth': '_setAuthOptionsVisibility', @@ -56,6 +57,9 @@ define( } }); - return AsModelBoundView.call(view); + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; }); diff --git a/src/UI/Settings/General/GeneralTemplate.html b/src/UI/Settings/General/GeneralViewTemplate.html similarity index 100% rename from src/UI/Settings/General/GeneralTemplate.html rename to src/UI/Settings/General/GeneralViewTemplate.html diff --git a/src/UI/Settings/Indexers/CollectionTemplate.html b/src/UI/Settings/Indexers/CollectionTemplate.html index 7713572fe..f2f4aff77 100644 --- a/src/UI/Settings/Indexers/CollectionTemplate.html +++ b/src/UI/Settings/Indexers/CollectionTemplate.html @@ -2,7 +2,7 @@ <legend>Indexers</legend> <div class="row"> <div class="span12"> - <ul id="x-indexers" class="indexer-list"> + <ul id="x-indexers" class="indexer-list thingies"> <li> <div class="indexer-settings-item add-card x-add-card"> <span class="center well"> diff --git a/src/UI/Settings/Indexers/CollectionView.js b/src/UI/Settings/Indexers/CollectionView.js index 0fd21e6b4..662dd5298 100644 --- a/src/UI/Settings/Indexers/CollectionView.js +++ b/src/UI/Settings/Indexers/CollectionView.js @@ -6,9 +6,8 @@ define( 'Settings/Indexers/ItemView', 'Settings/Indexers/EditView', 'Settings/Indexers/Collection', - 'System/StatusModel', 'underscore' - ], function (AppLayout, Marionette, IndexerItemView, IndexerEditView, IndexerCollection, StatusModel, _) { + ], function (AppLayout, Marionette, IndexerItemView, IndexerEditView, IndexerCollection, _) { return Marionette.CompositeView.extend({ itemView : IndexerItemView, itemViewContainer: '#x-indexers', @@ -28,12 +27,14 @@ define( _openSchemaModal: function () { var self = this; - //TODO: Is there a better way to deal with changing URLs? var schemaCollection = new IndexerCollection(); - schemaCollection.url = StatusModel.get('urlBase') + '/api/indexer/schema'; + var originalUrl = schemaCollection.url; + + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch({ success: function (collection) { - collection.url = StatusModel.get('urlBase') + '/api/indexer'; + collection.url = originalUrl; var model = _.first(collection.models); model.set({ diff --git a/src/UI/Settings/Indexers/IndexerSettingsModel.js b/src/UI/Settings/Indexers/IndexerSettingsModel.js new file mode 100644 index 000000000..34ede06ee --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerSettingsModel.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'Settings/SettingsModelBase' + ], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/indexer', + successMessage: 'Indexer settings saved', + errorMessage : 'Failed to save indexer settings' + }); + }); diff --git a/src/UI/Settings/Indexers/ItemTemplate.html b/src/UI/Settings/Indexers/ItemTemplate.html index 87acb9e0b..6ab71d071 100644 --- a/src/UI/Settings/Indexers/ItemTemplate.html +++ b/src/UI/Settings/Indexers/ItemTemplate.html @@ -1,4 +1,4 @@ -<div class="indexer-settings-item"> +<div class="indexer-settings-item thingy"> <div> <h3>{{name}}</h3> {{#if_eq implementation compare="Newznab"}} diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsView.js b/src/UI/Settings/Indexers/Options/IndexerOptionsView.js index 2fa8f3a59..92b7ab9d9 100644 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsView.js +++ b/src/UI/Settings/Indexers/Options/IndexerOptionsView.js @@ -2,12 +2,16 @@ define( [ 'marionette', - 'Mixins/AsModelBoundView' - ], function (Marionette, AsModelBoundView) { + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ template: 'Settings/Indexers/Options/IndexerOptionsViewTemplate' }); - return AsModelBoundView.call(view); + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; }); diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less index d037d3b43..79923b909 100644 --- a/src/UI/Settings/Indexers/indexers.less +++ b/src/UI/Settings/Indexers/indexers.less @@ -1,31 +1,11 @@ -@import "../../Shared/Styles/card"; - -.indexer-list { - li { - display: inline-block; - vertical-align: top; - } -} - .indexer-settings-item { - .card; - width: 220px; height: 260px; padding: 10px 15px; h3 { - margin-top: 0px; - display: inline-block; width: 190px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .btn-group { - margin-top: 8px; } &.add-card { diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js b/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js index 8cd2c120e..c458724f3 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js @@ -3,37 +3,24 @@ define( [ 'marionette', 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', 'Mixins/AutoComplete' - ], function (Marionette, AsModelBoundView) { + ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate', ui: { - recyclingBin : '.x-path', - failedDownloadHandlingCheckbox: '.x-failed-download-handling', - failedDownloadOptions : '.x-failed-download-options' - }, - - events: { - 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' + recyclingBin : '.x-path' }, onShow: function () { this.ui.recyclingBin.autoComplete('/directories'); - }, - - _setFailedDownloadOptionsVisibility: function () { - var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); - if (checked) { - this.ui.failedDownloadOptions.slideDown(); - } - - else { - this.ui.failedDownloadOptions.slideUp(); - } } }); - return AsModelBoundView.call(view); + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; }); diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index a150cee6e..c984fbef1 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -52,69 +52,3 @@ </div> </div> </fieldset> - -<fieldset class="advanced-setting"> - <legend>Failed Download Handling</legend> - - <div class="control-group"> - <label class="control-label">Enable</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableFailedDownloadHandling" class="x-failed-download-handling"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Process failed downloads and blacklist the release"/> - </span> - </div> - </div> - - <div class="x-failed-download-options"> - <div class="control-group"> - <label class="control-label">Redownload</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoRedownloadFailed"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release when a download fails?"/> - </span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Remove</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="removeFailedDownloads"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically remove failed downloads from history and encrypted downloads from queue?"/> - </span> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayout.js b/src/UI/Settings/MediaManagement/MediaManagementLayout.js index 3b5ac123e..e184c7b2f 100644 --- a/src/UI/Settings/MediaManagement/MediaManagementLayout.js +++ b/src/UI/Settings/MediaManagement/MediaManagementLayout.js @@ -4,7 +4,7 @@ define( [ 'marionette', 'Settings/MediaManagement/Naming/NamingView', - 'Settings/MediaManagement/Sorting/View', + 'Settings/MediaManagement/Sorting/SortingView', 'Settings/MediaManagement/FileManagement/FileManagementView', 'Settings/MediaManagement/Permissions/PermissionsView' ], function (Marionette, NamingView, SortingView, FileManagementView, PermissionsView) { diff --git a/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js b/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js new file mode 100644 index 000000000..1f27e23c3 --- /dev/null +++ b/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'Settings/SettingsModelBase' + ], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/mediamanagement', + successMessage: 'Media management settings saved', + errorMessage : 'Failed to save media managemnent settings' + }); + }); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 2ae43fbef..14229fd41 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -19,13 +19,15 @@ define( singleEpisodeExample : '.x-single-episode-example', multiEpisodeExample : '.x-multi-episode-example', dailyEpisodeExample : '.x-daily-episode-example', - namingTokenHelper : '.x-naming-token-helper' + namingTokenHelper : '.x-naming-token-helper', + multiEpisodeStyle : '.x-multi-episode-style' }, events: { 'change .x-rename-episodes' : '_setFailedDownloadOptionsVisibility', 'click .x-show-wizard' : '_showWizard', - 'click .x-naming-token-helper a' : '_addToken' + 'click .x-naming-token-helper a' : '_addToken', + 'change .x-multi-episode-style' : '_multiEpisodeFomatChanged' }, regions: { @@ -58,10 +60,6 @@ define( }, _updateSamples: function () { - if (!_.has(this.model.changed, 'standardEpisodeFormat') && !_.has(this.model.changed, 'dailyEpisodeFormat')) { - return; - } - this.namingSampleModel.fetch({ data: this.model.toJSON() }); }, @@ -92,6 +90,10 @@ define( this.ui.namingTokenHelper.removeClass('open'); input.focus(); + }, + + multiEpisodeFormatChanged: function () { + this.model.set('multiEpisodeStyle', this.ui.multiEpisodeStyle.val()); } }); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index 00908678c..c72a6524e 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -88,7 +88,7 @@ <label class="control-label">Multi-Episode Style</label> <div class="controls"> - <select class="inputClass" name="multiEpisodeStyle"> + <select class="inputClass x-multi-episode-style" name="multiEpisodeStyle"> <option value="0">Extend</option> <option value="1">Duplicate</option> <option value="2">Repeat</option> diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js index e1a098106..f4ef2d225 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js @@ -3,8 +3,9 @@ define( [ 'marionette', 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', 'Mixins/AutoComplete' - ], function (Marionette, AsModelBoundView) { + ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ template: 'Settings/MediaManagement/Permissions/PermissionsViewTemplate', @@ -35,5 +36,8 @@ define( } }); - return AsModelBoundView.call(view); + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; }); diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingView.js b/src/UI/Settings/MediaManagement/Sorting/SortingView.js new file mode 100644 index 000000000..fdb95b98c --- /dev/null +++ b/src/UI/Settings/MediaManagement/Sorting/SortingView.js @@ -0,0 +1,17 @@ +'use strict'; +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (Marionette, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/MediaManagement/Sorting/SortingViewTemplate' + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); diff --git a/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.html similarity index 100% rename from src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html rename to src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.html diff --git a/src/UI/Settings/MediaManagement/Sorting/View.js b/src/UI/Settings/MediaManagement/Sorting/View.js deleted file mode 100644 index 18e7d3d37..000000000 --- a/src/UI/Settings/MediaManagement/Sorting/View.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; -define( - [ - 'marionette', - 'Mixins/AsModelBoundView' - ], function (Marionette, AsModelBoundView) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/MediaManagement/Sorting/ViewTemplate' - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/Notifications/AddItemTemplate.html b/src/UI/Settings/Notifications/AddItemTemplate.html index 31fb15419..dfaee211e 100644 --- a/src/UI/Settings/Notifications/AddItemTemplate.html +++ b/src/UI/Settings/Notifications/AddItemTemplate.html @@ -1,4 +1,4 @@ -<div class="add-notification-item span3"> +<div class="add-thingy span3"> <div class="row"> <div class="span3"> {{implementation}} diff --git a/src/UI/Settings/Notifications/AddTemplate.html b/src/UI/Settings/Notifications/AddTemplate.html index 2f3ba9f31..06a241428 100644 --- a/src/UI/Settings/Notifications/AddTemplate.html +++ b/src/UI/Settings/Notifications/AddTemplate.html @@ -3,7 +3,7 @@ <h3>Add Notification</h3> </div> <div class="modal-body"> - <div class="add-notifications"> + <div class="add-notifications add-thingies"> <ul class="items"></ul> </div> </div> diff --git a/src/UI/Settings/Notifications/CollectionTemplate.html b/src/UI/Settings/Notifications/CollectionTemplate.html index 510190305..2d4cc25f4 100644 --- a/src/UI/Settings/Notifications/CollectionTemplate.html +++ b/src/UI/Settings/Notifications/CollectionTemplate.html @@ -1,8 +1,8 @@ <div class="row"> <div class="span12"> - <ul class="notifications"> + <ul class="notifications thingies"> <li> - <div class="notification-item add-card x-add-card"> + <div class="notification-item thingy add-card x-add-card"> <span class="center well"> <i class="icon-plus" title="Add Connection"/> </span> diff --git a/src/UI/Settings/Notifications/ItemTemplate.html b/src/UI/Settings/Notifications/ItemTemplate.html index 0b9c83d49..64125cea6 100644 --- a/src/UI/Settings/Notifications/ItemTemplate.html +++ b/src/UI/Settings/Notifications/ItemTemplate.html @@ -1,4 +1,4 @@ -<div class="notification-item"> +<div class="notification-item thingy"> <div> <h3>{{name}}</h3> <span class="btn-group pull-right"> diff --git a/src/UI/Settings/Notifications/NotificationEditView.js b/src/UI/Settings/Notifications/NotificationEditView.js index 03de47197..161ebde96 100644 --- a/src/UI/Settings/Notifications/NotificationEditView.js +++ b/src/UI/Settings/Notifications/NotificationEditView.js @@ -22,7 +22,7 @@ define( }, events: { - 'click .x-save' : '_saveNotification', + 'click .x-save' : '_saveClient', 'click .x-save-and-add': '_saveAndAddNotification', 'click .x-delete' : '_deleteNotification', 'click .x-back' : '_back', @@ -38,7 +38,7 @@ define( this._onDownloadChanged(); }, - _saveNotification: function () { + _saveClient: function () { var self = this; var promise = this.model.saveSettings(); diff --git a/src/UI/Settings/Notifications/SchemaModal.js b/src/UI/Settings/Notifications/SchemaModal.js index c8f6979da..923072ec4 100644 --- a/src/UI/Settings/Notifications/SchemaModal.js +++ b/src/UI/Settings/Notifications/SchemaModal.js @@ -2,16 +2,16 @@ define([ 'AppLayout', 'Settings/Notifications/Collection', - 'Settings/Notifications/AddView', - 'System/StatusModel' -], function (AppLayout, NotificationCollection, AddSelectionNotificationView, StatusModel) { + 'Settings/Notifications/AddView' +], function (AppLayout, NotificationCollection, AddSelectionNotificationView) { return ({ open: function (collection) { var schemaCollection = new NotificationCollection(); - schemaCollection.url = StatusModel.get('urlBase') + '/api/notification/schema'; + var orginalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; schemaCollection.fetch(); - schemaCollection.url = StatusModel.get('urlBase') + '/api/notification'; + schemaCollection.url = orginalUrl; var view = new AddSelectionNotificationView({ collection: schemaCollection, notificationCollection: collection}); AppLayout.modalRegion.show(view); diff --git a/src/UI/Settings/Notifications/notifications.less b/src/UI/Settings/Notifications/notifications.less index 24e54738f..c8e059594 100644 --- a/src/UI/Settings/Notifications/notifications.less +++ b/src/UI/Settings/Notifications/notifications.less @@ -1,70 +1,17 @@ -@import "../../Shared/Styles/card.less"; -@import "../../Shared/Styles/clickable.less"; - -.add-notification-item { - .card; - cursor: pointer; - font-size: 24px; - font-weight: lighter; - text-align: center; - - a { - font-size: 16px; - color: #595959; - - i { - .clickable; - } - } - - a:hover { - text-decoration: none; - } -} - -.add-notifications { - text-align: center; - - .items { - list-style-type: none; - margin: 0px; - - li { - display: inline-block; - vertical-align: top; - } - } -} - .notifications { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; - - li { - display: inline-block; - vertical-align: top; - } } -.notification-item { - .card; +.notification-item { width: 290px; height: 90px; padding: 20px 20px; h3 { - margin-top: 0px; - display: inline-block; width: 230px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .btn-group { - margin-top: 8px; } .settings { diff --git a/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html b/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html index 0c403edcf..7a00ff46b 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html +++ b/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html @@ -2,9 +2,9 @@ <legend>Quality Profiles</legend> <div class="row"> <div class="span12"> - <ul class="quality-profiles"> + <ul class="quality-profiles thingies"> <li> - <div class="quality-profile-item add-card x-add-card"> + <div class="quality-profile-item thingy add-card x-add-card"> <span class="center well"> <i class="icon-plus" title="Add Profile"/> </span> diff --git a/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html index 23acb6942..6d5247760 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html @@ -1,4 +1,4 @@ -<div class="quality-profile-item"> +<div class="quality-profile-item thingy"> <div> <h3 name="name"></h3> <span class="btn-group pull-right"> diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 199f87201..c10c990f1 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -1,33 +1,14 @@ -@import "../../Shared/Styles/card"; @import "../../Content/Bootstrap/mixins"; @import "../../Content/FontAwesome/font-awesome"; -.quality-profiles { - li { - display: inline-block; - vertical-align: top; - } -} - .quality-profile-item { - .card; - width: 300px; height: 120px; padding: 10px 15px; h3 { - margin-top: 0px; - display: inline-block; width: 240px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .btn-group { - margin-top: 8px; } &.add-card { diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 47973005e..122d84307 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -2,17 +2,20 @@ define( [ 'jquery', + 'underscore', 'vent', 'marionette', 'backbone', - 'Settings/SettingsModel', 'Settings/General/GeneralSettingsModel', 'Settings/MediaManagement/Naming/NamingModel', 'Settings/MediaManagement/MediaManagementLayout', + 'Settings/MediaManagement/MediaManagementSettingsModel', 'Settings/Quality/QualityLayout', 'Settings/Indexers/IndexerLayout', 'Settings/Indexers/Collection', - 'Settings/DownloadClient/Layout', + 'Settings/Indexers/IndexerSettingsModel', + 'Settings/DownloadClient/DownloadClientLayout', + 'Settings/DownloadClient/DownloadClientSettingsModel', 'Settings/Notifications/CollectionView', 'Settings/Notifications/Collection', 'Settings/Metadata/MetadataLayout', @@ -20,17 +23,20 @@ define( 'Shared/LoadingView', 'Config' ], function ($, + _, vent, Marionette, Backbone, - SettingsModel, GeneralSettingsModel, NamingModel, MediaManagementLayout, + MediaManagementSettingsModel, QualityLayout, IndexerLayout, IndexerCollection, + IndexerSettingsModel, DownloadClientLayout, + DownloadClientSettingsModel, NotificationCollectionView, NotificationCollection, MetadataLayout, @@ -84,26 +90,31 @@ define( this.loading.show(new LoadingView()); var self = this; - this.settings = new SettingsModel(); - this.generalSettings = new GeneralSettingsModel(); + this.mediaManagementSettings = new MediaManagementSettingsModel(); this.namingSettings = new NamingModel(); - this.indexerSettings = new IndexerCollection(); - this.notificationSettings = new NotificationCollection(); + this.indexerSettings = new IndexerSettingsModel(); + this.indexerCollection = new IndexerCollection(); + this.downloadClientSettings = new DownloadClientSettingsModel(); + this.notificationCollection = new NotificationCollection(); + this.generalSettings = new GeneralSettingsModel(); - Backbone.$.when(this.settings.fetch(), - this.generalSettings.fetch(), + Backbone.$.when( + this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), - this.notificationSettings.fetch() + this.indexerCollection.fetch(), + this.downloadClientSettings.fetch(), + this.notificationCollection.fetch(), + this.generalSettings.fetch() ).done(function () { if(!self.isClosed) { self.loading.$el.hide(); - self.mediaManagement.show(new MediaManagementLayout({ settings: self.settings, namingSettings: self.namingSettings })); - self.quality.show(new QualityLayout({ settings: self.settings })); - self.indexers.show(new IndexerLayout({ settings: self.settings, indexersCollection: self.indexerSettings })); - self.downloadClient.show(new DownloadClientLayout({ model: self.settings })); - self.notifications.show(new NotificationCollectionView({ collection: self.notificationSettings })); + self.mediaManagement.show(new MediaManagementLayout({ settings: self.mediaManagementSettings, namingSettings: self.namingSettings })); + self.quality.show(new QualityLayout()); + self.indexers.show(new IndexerLayout({ settings: self.indexerSettings, indexersCollection: self.indexerCollection })); + self.downloadClient.show(new DownloadClientLayout({ model: self.downloadClientSettings })); + self.notifications.show(new NotificationCollectionView({ collection: self.notificationCollection })); self.metadata.show(new MetadataLayout()); self.general.show(new GeneralView({ model: self.generalSettings })); } @@ -204,7 +215,7 @@ define( }, _navigate:function(route){ - Backbone.history.navigate(route, { trigger: true, replace: true }); + Backbone.history.navigate(route, { trigger: false, replace: true }); }, _save: function () { diff --git a/src/UI/Settings/SettingsModel.js b/src/UI/Settings/SettingsModel.js deleted file mode 100644 index 0b5da8dd2..000000000 --- a/src/UI/Settings/SettingsModel.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'Settings/SettingsModelBase' - ], function (SettingsModelBase) { - return SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/settings', - successMessage: 'Settings saved', - errorMessage : 'Failed to save settings' - }); - }); diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less index 69a44d8ca..9f0ce6ac2 100644 --- a/src/UI/Settings/settings.less +++ b/src/UI/Settings/settings.less @@ -4,6 +4,8 @@ @import "Quality/quality"; @import "Notifications/notifications"; @import "Metadata/metadata"; +@import "DownloadClient/downloadclient"; +@import "thingy"; li.save-and-add { .clickable; diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less new file mode 100644 index 000000000..6e8dad971 --- /dev/null +++ b/src/UI/Settings/thingy.less @@ -0,0 +1,65 @@ +@import "../Shared/Styles/card"; +@import "../Shared/Styles/clickable"; + +.add-thingy { + .card; + cursor: pointer; + font-size: 24px; + font-weight: lighter; + text-align: center; + + a { + font-size: 16px; + color: #595959; + + i { + .clickable; + } + } + + a:hover { + text-decoration: none; + } +} + +.add-thingies { + text-align: center; + + .items { + list-style-type: none; + margin: 0px; + + li { + display: inline-block; + vertical-align: top; + } + } +} + +.thingy { + + .card; + + h3 { + margin-top: 0px; + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .btn-group { + margin-top: 8px; + } + + .settings { + margin-top: 5px; + } +} + +.thingies { + li { + display: inline-block; + vertical-align: top; + } +} \ No newline at end of file