From 08b2e293d3f1d1d43023b1e4740ad01f96ed1868 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Jul 2014 18:02:47 -0700 Subject: [PATCH 001/105] Verify disk scan won't scan if root folder doesn't exist --- .../DiskScanServiceTests/ScanFixture.cs | 44 +++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../Configuration/ConfigFileProvider.cs | 4 +- .../MediaFiles/DiskScanService.cs | 4 +- 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs new file mode 100644 index 000000000..ea1a07f8d --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests +{ + [TestFixture] + public class ScanFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Path = @"C:\Test\TV\Series") + .Build(); + + Mocker.GetMock() + .Setup(s => s.GetParentFolder(It.IsAny())) + .Returns((String path) => Directory.GetParent(path).FullName); + } + + [Test] + public void should_not_scan_if_series_root_folder_does_not_exist() + { + Subject.Scan(_series); + + ExceptionVerification.ExpectedWarns(1); + + Mocker.GetMock() + .Verify(v => v.PublishCommand(It.IsAny()), Times.Never()); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 59e3a0599..a36695aa6 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -155,6 +155,7 @@ + diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index d0954c221..ecb23d0a2 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -168,9 +168,9 @@ namespace NzbDrone.Core.Configuration { get { - var urlBase = GetValue("UrlBase", ""); + var urlBase = GetValue("UrlBase", "").Trim('/'); - if (String.IsNullOrEmpty(urlBase)) + if (urlBase.IsNullOrWhiteSpace()) { return urlBase; } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index c8d94c2ed..4bdfe04b7 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.Linq; using NLog; From 41583a8c67cbc7bf77d10b64bd77bed0fe9436dc Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Jul 2014 23:08:32 -0700 Subject: [PATCH 002/105] New: Pushalot notification support --- .../Notifications/Pushalot/Pushalot.cs | 50 +++++++++ .../Pushalot/PushalotPriority.cs | 9 ++ .../Notifications/Pushalot/PushalotProxy.cs | 102 ++++++++++++++++++ .../Pushalot/PushalotResponse.cs | 11 ++ .../Pushalot/PushalotSettings.cs | 40 +++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + 6 files changed, 217 insertions(+) create mode 100644 src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushalot/PushalotPriority.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushalot/PushalotResponse.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs diff --git a/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs b/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs new file mode 100644 index 000000000..0f54e344e --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Notifications.Pushalot +{ + public class Pushalot : NotificationBase + { + private readonly IPushalotProxy _proxy; + + public Pushalot(IPushalotProxy proxy) + { + _proxy = proxy; + } + + public override string Link + { + get { return "https://www.Pushalot.com/"; } + } + + public override void OnGrab(String message) + { + const string title = "Episode Grabbed"; + + _proxy.SendNotification(title, message, Settings); + } + + public override void OnDownload(DownloadMessage message) + { + const string title = "Episode Downloaded"; + + _proxy.SendNotification(title, message.Message, Settings); + } + + public override void AfterRename(Series series) + { + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_proxy.Test(Settings)); + + return new ValidationResult(failures); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushalot/PushalotPriority.cs b/src/NzbDrone.Core/Notifications/Pushalot/PushalotPriority.cs new file mode 100644 index 000000000..58effba2f --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushalot/PushalotPriority.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Pushalot +{ + public enum PushalotPriority + { + Silent = -1, + Normal = 0, + Important = 1 + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs b/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs new file mode 100644 index 000000000..aeb3a71d0 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs @@ -0,0 +1,102 @@ +using System; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Rest; +using RestSharp; + +namespace NzbDrone.Core.Notifications.Pushalot +{ + public interface IPushalotProxy + { + void SendNotification(String title, String message, PushalotSettings settings); + ValidationFailure Test(PushalotSettings settings); + } + + public class PushalotProxy : IPushalotProxy + { + private readonly Logger _logger; + private const string URL = "https://pushalot.com/api/sendmessage"; + + public PushalotProxy(Logger logger) + { + _logger = logger; + } + + public void SendNotification(String title, String message, PushalotSettings settings) + { + var client = RestClientFactory.BuildClient(URL); + var request = BuildRequest(); + + request.AddParameter("Source", "NzbDrone"); + request.AddParameter("Image", "https://raw.githubusercontent.com/NzbDrone/NzbDrone/master/Logo/128.png"); + + request.AddParameter("Title", title); + request.AddParameter("Body", message); + request.AddParameter("AuthorizationToken", settings.AuthToken); + + if ((PushalotPriority)settings.Priority == PushalotPriority.Important) + { + request.AddParameter("IsImportant", true); + } + + if ((PushalotPriority)settings.Priority == PushalotPriority.Silent) + { + request.AddParameter("IsSilent", true); + } + + client.ExecuteAndValidate(request); + } + + public RestRequest BuildRequest() + { + var request = new RestRequest(Method.POST); + + return request; + } + + public ValidationFailure Test(PushalotSettings settings) + { + try + { + const string title = "Test Notification"; + const string body = "This is a test message from NzbDrone"; + + SendNotification(title, body, settings); + } + catch (RestException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.ErrorException("Authentication Token is invalid: " + ex.Message, ex); + return new ValidationFailure("AuthToken", "Authentication Token is invalid"); + } + + if (ex.Response.StatusCode == HttpStatusCode.NotAcceptable) + { + _logger.ErrorException("Message limit reached: " + ex.Message, ex); + return new ValidationFailure("AuthToken", "Message limit reached"); + } + + if (ex.Response.StatusCode == HttpStatusCode.Gone) + { + _logger.ErrorException("Authorization Token is no longer valid: " + ex.Message, ex); + return new ValidationFailure("AuthToken", "Authorization Token is no longer valid, please use a new one."); + } + + var response = Json.Deserialize(ex.Response.Content); + + _logger.ErrorException("Unable to send test message: " + ex.Message, ex); + return new ValidationFailure("AuthToken", response.Description); + } + catch (Exception ex) + { + _logger.ErrorException("Unable to send test message: " + ex.Message, ex); + return new ValidationFailure("", "Unable to send test message"); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushalot/PushalotResponse.cs b/src/NzbDrone.Core/Notifications/Pushalot/PushalotResponse.cs new file mode 100644 index 000000000..f98807a5a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushalot/PushalotResponse.cs @@ -0,0 +1,11 @@ +using System; + +namespace NzbDrone.Core.Notifications.Pushalot +{ + public class PushalotResponse + { + public Boolean Success { get; set; } + public Int32 Status { get; set; } + public String Description { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs b/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs new file mode 100644 index 000000000..7e5e9c8cd --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs @@ -0,0 +1,40 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Notifications.Pushalot +{ + public class PushalotSettingsValidator : AbstractValidator + { + public PushalotSettingsValidator() + { + RuleFor(c => c.AuthToken).NotEmpty(); + } + } + + public class PushalotSettings : IProviderConfig + { + private static readonly PushalotSettingsValidator Validator = new PushalotSettingsValidator(); + + [FieldDefinition(0, Label = "Authorization Token", HelpLink = "https://pushalot.com/manager/authorizations")] + public String AuthToken { get; set; } + + [FieldDefinition(1, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushalotPriority))] + public Int32 Priority { get; set; } + + public bool IsValid + { + get + { + return !String.IsNullOrWhiteSpace(AuthToken); + } + } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 7e12f2d21..f85d0f043 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -415,6 +415,11 @@ + + + + + From a4500606a9568770195329513a3ebbd2684aa4d3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Jul 2014 23:37:32 -0700 Subject: [PATCH 003/105] Fixed: mono version check will check running mono version instead running another version --- src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + .../Processes/IRuntimeProvider.cs | 9 ++++ .../Checks/MonoVersionCheckFixture.cs | 39 +++++------------- .../HealthCheck/Checks/MonoVersionCheck.cs | 23 ++++------- src/NzbDrone.Mono/MonoRuntimeProvider.cs | 41 +++++++++++++++++++ src/NzbDrone.Mono/NzbDrone.Mono.csproj | 1 + 6 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 src/NzbDrone.Common/Processes/IRuntimeProvider.cs create mode 100644 src/NzbDrone.Mono/MonoRuntimeProvider.cs diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index c5ebd8d90..e5ef0d4e7 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -112,6 +112,7 @@ + diff --git a/src/NzbDrone.Common/Processes/IRuntimeProvider.cs b/src/NzbDrone.Common/Processes/IRuntimeProvider.cs new file mode 100644 index 000000000..96f7a1e62 --- /dev/null +++ b/src/NzbDrone.Common/Processes/IRuntimeProvider.cs @@ -0,0 +1,9 @@ +using System; + +namespace NzbDrone.Common.Processes +{ + public interface IRuntimeProvider + { + String GetVersion(); + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs index 8a0bbf7da..3184496ef 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using NUnit.Framework; using NzbDrone.Common.Processes; using NzbDrone.Core.HealthCheck.Checks; @@ -18,16 +17,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks private void GivenOutput(string version) { - Mocker.GetMock() - .Setup(s => s.StartAndCapture("mono", "--version")) - .Returns(new ProcessOutput - { - Standard = new List - { - String.Format("Mono JIT compiler version {0} (Debian {0}-8)", version), - "Copyright (C) 2002-2011 Novell, Inc, Xamarin, Inc and Contributors. www.mono-project.com" - } - }); + Mocker.GetMock() + .Setup(s => s.GetVersion()) + .Returns(String.Format("{0} (tarball Wed Sep 25 16:35:44 CDT 2013)", version)); } [Test] @@ -46,6 +38,14 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Subject.Check().ShouldBeWarning(); } + [Test] + public void should_return_warning_when_mono_2_10_2() + { + GivenOutput("2.10.2"); + + Subject.Check().ShouldBeWarning(); + } + [Test] public void should_return_ok_when_mono_3_2() { @@ -85,22 +85,5 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Subject.Check().ShouldBeOk(); } - - [Test] - public void should_return_ok_when_mono_3_6_1_with_custom_output() - { - Mocker.GetMock() - .Setup(s => s.StartAndCapture("mono", "--version")) - .Returns(new ProcessOutput - { - Standard = new List - { - "Mono JIT compiler version 3.6.1 (master/fce3972 Fri Jul 4 01:12:43 CEST 2014)", - "Copyright (C) 2002-2011 Novell, Inc, Xamarin, Inc and Contributors. www.mono-project.com" - } - }); - - Subject.Check().ShouldBeOk(); - } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs index 733d5e61a..52944faa1 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs @@ -8,13 +8,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { public class MonoVersionCheck : HealthCheckBase { - private readonly IProcessProvider _processProvider; + private readonly IRuntimeProvider _runtimeProvider; private readonly Logger _logger; private static readonly Regex VersionRegex = new Regex(@"(?<=\W)(?\d+\.\d+\.\d+(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public MonoVersionCheck(IProcessProvider processProvider, Logger logger) + public MonoVersionCheck(IRuntimeProvider runtimeProvider, Logger logger) { - _processProvider = processProvider; + _runtimeProvider = runtimeProvider; _logger = logger; } @@ -25,21 +25,16 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - var output = _processProvider.StartAndCapture("mono", "--version"); + var versionMatch = VersionRegex.Match(_runtimeProvider.GetVersion()); - foreach (var line in output.Standard) + if (versionMatch.Success) { - var versionMatch = VersionRegex.Match(line); + var version = new Version(versionMatch.Groups["version"].Value); - if (versionMatch.Success) + if (version >= new Version(3, 2)) { - var version = new Version(versionMatch.Groups["version"].Value); - - if (version >= new Version(3, 2)) - { - _logger.Debug("mono version is 3.2 or better: {0}", version.ToString()); - return new HealthCheck(GetType()); - } + _logger.Debug("mono version is 3.2 or better: {0}", version.ToString()); + return new HealthCheck(GetType()); } } diff --git a/src/NzbDrone.Mono/MonoRuntimeProvider.cs b/src/NzbDrone.Mono/MonoRuntimeProvider.cs new file mode 100644 index 000000000..af0fe4115 --- /dev/null +++ b/src/NzbDrone.Mono/MonoRuntimeProvider.cs @@ -0,0 +1,41 @@ +using System; +using System.Reflection; +using NLog; +using NzbDrone.Common.Processes; + +namespace NzbDrone.Mono +{ + public class MonoRuntimeProvider : IRuntimeProvider + { + private readonly Logger _logger; + + public MonoRuntimeProvider(Logger logger) + { + _logger = logger; + } + + public String GetVersion() + { + try + { + var type = Type.GetType("Mono.Runtime"); + + if (type != null) + { + var displayName = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + + if (displayName != null) + { + return displayName.Invoke(null, null).ToString(); + } + } + } + catch (Exception ex) + { + _logger.ErrorException("Unable to get mono version: " + ex.Message, ex); + } + + return String.Empty; + } + } +} diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index 91d0efeb3..fa37ac15e 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -70,6 +70,7 @@ + From 0d112075cbd855cff205ec0be0077e39b4d3e94f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Jul 2014 23:40:55 -0700 Subject: [PATCH 004/105] Added implementation of IRuntimeProvider for Windows --- src/NzbDrone.Windows/DotNetRuntimeProvider.cs | 13 +++++++++++++ src/NzbDrone.Windows/NzbDrone.Windows.csproj | 1 + 2 files changed, 14 insertions(+) create mode 100644 src/NzbDrone.Windows/DotNetRuntimeProvider.cs diff --git a/src/NzbDrone.Windows/DotNetRuntimeProvider.cs b/src/NzbDrone.Windows/DotNetRuntimeProvider.cs new file mode 100644 index 000000000..40203278d --- /dev/null +++ b/src/NzbDrone.Windows/DotNetRuntimeProvider.cs @@ -0,0 +1,13 @@ +using System; +using NzbDrone.Common.Processes; + +namespace NzbDrone.Windows +{ + public class DotNetRuntimeProvider : IRuntimeProvider + { + public String GetVersion() + { + return Environment.Version.ToString(); + } + } +} diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj index 607f09a4e..4c3d83721 100644 --- a/src/NzbDrone.Windows/NzbDrone.Windows.csproj +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -63,6 +63,7 @@ + From 502c2fa6cf64d95e3de81264f242bc6684fff2b9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Jul 2014 23:57:54 -0700 Subject: [PATCH 005/105] Better regex for mono version parsing --- src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs index 52944faa1..43f385e8e 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { private readonly IRuntimeProvider _runtimeProvider; private readonly Logger _logger; - private static readonly Regex VersionRegex = new Regex(@"(?<=\W)(?\d+\.\d+\.\d+(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex VersionRegex = new Regex(@"(?<=\W|^)(?\d+\.\d+\.\d+(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public MonoVersionCheck(IRuntimeProvider runtimeProvider, Logger logger) { @@ -25,7 +25,8 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - var versionMatch = VersionRegex.Match(_runtimeProvider.GetVersion()); + var versionString = _runtimeProvider.GetVersion(); + var versionMatch = VersionRegex.Match(versionString); if (versionMatch.Success) { From d9f4eeb0f0c41fa4227e0ce743273a1e976b1b56 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 25 Jul 2014 06:37:25 -0700 Subject: [PATCH 006/105] Fixed: Manual search won't fail if release wasn't parsed correctly --- .../DecisionEngine/DownloadDecisionPriorizationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index 2980a1132..9f01356b2 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.DecisionEngine .GroupBy(c => c.RemoteEpisode.Series.Id, (i, s) => s .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.QualityProfile)) .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) - .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / c.RemoteEpisode.Episodes.Count) + .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count)) .ThenBy(c => c.RemoteEpisode.Release.Age)) .SelectMany(c => c) .Union(decisions.Where(c => c.RemoteEpisode.Series == null)) From a929b936957966d7988133a569558fed32c92952 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 25 Jul 2014 07:36:55 -0700 Subject: [PATCH 007/105] Tests are good too. --- .../PrioritizeDownloadDecisionFixture.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index a4c62160d..b91614795 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Tv; -using NzbDrone.Core.Download; using NzbDrone.Core.Qualities; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.DecisionEngine; @@ -17,7 +16,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [TestFixture] public class PrioritizeDownloadDecisionFixture : CoreTest { - private Episode GetEpisode(int id) + private Episode GivenEpisode(int id) { return Builder.CreateNew() .With(e => e.Id = id) @@ -25,7 +24,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Build(); } - private RemoteEpisode GetRemoteEpisode(List episodes, QualityModel quality, int Age = 0, long size = 0) + private RemoteEpisode GivenRemoteEpisode(List episodes, QualityModel quality, int age = 0, long size = 0) { var remoteEpisode = new RemoteEpisode(); remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo(); @@ -35,7 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests remoteEpisode.Episodes.AddRange(episodes); remoteEpisode.Release = new ReleaseInfo(); - remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-Age); + remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-age); remoteEpisode.Release.Size = size; remoteEpisode.Series = Builder.CreateNew() @@ -48,8 +47,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_propers_before_non_propers() { - var remoteEpisode1 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p, false)); - var remoteEpisode2 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p, true)); + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p, false)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p, true)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisode1)); @@ -62,8 +61,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_higher_quality_before_lower() { - var remoteEpisode1 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.SDTV)); - var remoteEpisode2 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.SDTV)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisode1)); @@ -76,8 +75,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_lowest_number_of_episodes() { - var remoteEpisode1 = GetRemoteEpisode(new List { GetEpisode(2) }, new QualityModel(Quality.HDTV720p)); - var remoteEpisode2 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(2) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisode1)); @@ -90,8 +89,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_lowest_number_of_episodes_with_multiple_episodes() { - var remoteEpisode1 = GetRemoteEpisode(new List { GetEpisode(2), GetEpisode(3) }, new QualityModel(Quality.HDTV720p)); - var remoteEpisode2 = GetRemoteEpisode(new List { GetEpisode(1), GetEpisode(2) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(2), GivenEpisode(3) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1), GivenEpisode(2) }, new QualityModel(Quality.HDTV720p)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisode1)); @@ -101,30 +100,29 @@ namespace NzbDrone.Core.Test.DecisionEngineTests qualifiedReports.First().RemoteEpisode.Episodes.First().EpisodeNumber.Should().Be(1); } - [Test] public void should_order_by_smallest_rounded_to_200mb_then_age() { - var remoteEpisodeSd = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.SDTV), size: 100.Megabytes(), Age: 1); - var remoteEpisodeHdSmallOld = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 1200.Megabytes(), Age: 1000); - var remoteEpisodeHdSmallYounge = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 1250.Megabytes(), Age: 10); - var remoteEpisodeHdLargeYounge = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 3000.Megabytes(), Age: 1); + var remoteEpisodeSd = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.SDTV), size: 100.Megabytes(), age: 1); + var remoteEpisodeHdSmallOld = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 1200.Megabytes(), age: 1000); + var remoteEpisodeSmallYoung = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 1250.Megabytes(), age: 10); + var remoteEpisodeHdLargeYoung = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 3000.Megabytes(), age: 1); var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisodeSd)); decisions.Add(new DownloadDecision(remoteEpisodeHdSmallOld)); - decisions.Add(new DownloadDecision(remoteEpisodeHdSmallYounge)); - decisions.Add(new DownloadDecision(remoteEpisodeHdLargeYounge)); + decisions.Add(new DownloadDecision(remoteEpisodeSmallYoung)); + decisions.Add(new DownloadDecision(remoteEpisodeHdLargeYoung)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisodeHdSmallYounge); + qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisodeSmallYoung); } [Test] public void should_order_by_youngest() { - var remoteEpisode1 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p), Age: 10); - var remoteEpisode2 = GetRemoteEpisode(new List { GetEpisode(1) }, new QualityModel(Quality.HDTV720p), Age: 5); + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), age: 10); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), age: 5); var decisions = new List(); @@ -134,5 +132,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var qualifiedReports = Subject.PrioritizeDecisions(decisions); qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisode2); } + + [Test] + public void should_not_throw_if_no_episodes_are_found() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 500.Megabytes()); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 500.Megabytes()); + + remoteEpisode1.Episodes = new List(); + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + Subject.PrioritizeDecisions(decisions); + } } } From daaf4e183121ac04269deb542a5a4a85b4ecd129 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 25 Jul 2014 08:33:51 -0700 Subject: [PATCH 008/105] New: Parse 6 digit date format (yymmdd) --- .../ParserTests/DailyEpisodeParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index 67f3bed2d..56e8e9e7b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020nz", 2011, 12, 2)] [TestCase("Series Title - 2013-10-30 - Episode Title (1) [HDTV-720p]", "Series Title", 2013, 10, 30)] [TestCase("The_Voice_US_04.28.2014_hdtv.x264.Poke.mp4", "The Voice US", 2014, 4, 28)] + [TestCase("At.Midnight.140722.720p.HDTV.x264-YesTV", "At Midnight", 2014, 07, 22)] public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 63576914b..6c9e74532 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -132,6 +132,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex AirDateRegex = new Regex(@"^(.*?)(?\d{4})[_.-](?[0-1][0-9])[_.-](?[0-3][0-9])|(?[0-1][0-9])[_.-](?[0-3][0-9])[_.-](?\d{4}))(?!\d)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex SixDigitAirDateRegex = new Regex(@"^(?:.*?)(?(?\d{2})(?[0-1][0-9])(?[0-3][0-9]))", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?[a-z0-9]+)\b(? Date: Fri, 25 Jul 2014 08:34:29 -0700 Subject: [PATCH 009/105] Using absolute episode number logging moved to Debug --- src/NzbDrone.Core/Parser/ParsingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 5acc542f7..61ede3492 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Parser if (episode != null) { - _logger.Info("Using absolute episode number {0} for: {1} - TVDB: {2}x{3:00}", + _logger.Debug("Using absolute episode number {0} for: {1} - TVDB: {2}x{3:00}", absoluteEpisodeNumber, series.Title, episode.SeasonNumber, From 74a38415cf19093dee2afbc177e2861fbf43a535 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Jun 2014 01:22:55 -0700 Subject: [PATCH 010/105] Profiles Indexes are created with the same uniqueness when copying a table New: Non-English episode support New: Renamed Quality Profiles to Profiles and made them more powerful New: Configurable wait time before grabbing a release to wait for a better quality --- .../MappingTests/ResourceMappingFixture.cs | 17 +- src/NzbDrone.Api/Indexers/ReleaseModule.cs | 6 +- src/NzbDrone.Api/Indexers/ReleaseResource.cs | 2 + src/NzbDrone.Api/NzbDrone.Api.csproj | 11 +- .../Profiles/Languages/LanguageModule.cs | 39 + .../Profiles/Languages/LanguageResource.cs | 14 + .../Profiles/LegacyProfileModule.cs | 36 + src/NzbDrone.Api/Profiles/ProfileModule.cs | 67 + src/NzbDrone.Api/Profiles/ProfileResource.cs | 25 + .../ProfileSchemaModule.cs} | 23 +- .../ProfileValidation.cs} | 8 +- .../Qualities/QualityProfileModule.cs | 60 - .../Qualities/QualityProfileResource.cs | 20 - src/NzbDrone.Api/Queue/QueueModule.cs | 23 +- src/NzbDrone.Api/Series/SeriesModule.cs | 5 +- src/NzbDrone.Api/Series/SeriesResource.cs | 22 +- .../Datastore/MarrDataLazyLoadingFixture.cs | 19 +- .../AlterFixture.cs | 19 + .../DuplicateFixture.cs | 4 +- .../CutoffSpecificationFixture.cs | 11 +- .../HistorySpecificationFixture.cs | 17 +- .../LanguageSpecificationFixture.cs | 44 +- .../NotInQueueSpecificationFixture.cs | 3 +- .../PrioritizeDownloadDecisionFixture.cs | 4 +- ...ityAllowedByProfileSpecificationFixture.cs | 7 +- .../QualityUpgradeSpecificationFixture.cs | 9 +- .../RssSync/DelaySpecificationFixture.cs | 253 +++ .../RssSync/ProperSpecificationFixture.cs | 3 +- .../UpgradeDiskSpecificationFixture.cs | 3 +- .../DownloadApprovedFixture.cs | 26 +- .../ProcessFixture.cs | 165 ++ .../HistoryTests/HistoryServiceFixture.cs | 9 +- .../CleanupOrphanedBlacklistFixture.cs | 42 + .../CleanupOrphanedPendingReleasesFixture.cs | 42 + .../NzbSearchServiceFixture.cs | 7 +- .../ImportDecisionMakerFixture.cs | 3 +- .../UpgradeSpecificationFixture.cs | 5 +- .../ImportApprovedEpisodesFixture.cs | 3 +- .../NzbDrone.Core.Test.csproj | 11 +- .../ProfileRepositoryFixture.cs} | 14 +- .../ProfileServiceFixture.cs} | 27 +- .../Qualities/QualityFixture.cs | 5 +- .../Qualities/QualityModelComparerFixture.cs | 19 +- .../EpisodeProviderTest.cs | 1519 ----------------- .../EpisodesWhereCutoffUnmetFixture.cs | 17 +- ...yFixture.cs => SeriesRepositoryFixture.cs} | 10 +- .../UpdateMultipleSeriesFixture.cs | 2 +- .../Extensions/RelationshipExtensions.cs | 5 - .../036_update_with_quality_converters.cs | 3 +- .../Migration/054_rename_profiles.cs | 31 + .../Migration/055_drop_old_profile_columns.cs | 14 + .../Migration/Framework/SQLiteIndex.cs | 7 +- .../Framework/SQLiteMigrationHelper.cs | 6 - src/NzbDrone.Core/Datastore/TableMapping.cs | 16 +- .../{Specifications => }/DownloadDecision.cs | 25 +- .../DecisionEngine/DownloadDecisionMaker.cs | 16 +- .../DownloadDecisionPriorizationService.cs | 2 +- .../IDecisionEngineSpecification.cs | 5 +- .../DecisionEngine/IRejectWithReason.cs | 2 +- .../QualityUpgradableSpecification.cs | 9 +- src/NzbDrone.Core/DecisionEngine/Rejection.cs | 21 + .../DecisionEngine/RejectionType.cs | 8 + .../AcceptableSizeSpecification.cs | 7 +- .../Specifications/BlacklistSpecification.cs | 2 + .../Specifications/CutoffSpecification.cs | 4 +- .../Specifications/LanguageSpecification.cs | 12 +- .../Specifications/NotInQueueSpecification.cs | 4 +- .../NotRestrictedReleaseSpecification.cs | 2 + .../Specifications/NotSampleSpecification.cs | 2 + .../QualityAllowedByProfileSpecification.cs | 4 +- .../Specifications/RetentionSpecification.cs | 2 + .../Specifications/RetrySpecification.cs | 2 + .../RssSync/DelaySpecification.cs | 116 ++ .../RssSync/HistorySpecification.cs | 6 +- .../RssSync/MonitoredEpisodeSpecification.cs | 2 + .../RssSync/ProperSpecification.cs | 2 + .../Search/DailyEpisodeMatchSpecification.cs | 3 + .../Search/EpisodeRequestedSpecification.cs | 2 + .../Search/SeasonMatchSpecification.cs | 2 + .../Search/SeriesSpecification.cs | 2 + .../SingleEpisodeSearchMatchSpecification.cs | 2 + .../UpgradeDiskSpecification.cs | 4 +- .../Download/DownloadApprovedReports.cs | 67 - .../Download/DownloadClientItem.cs | 3 - .../Download/Pending/PendingRelease.cs | 18 + .../Pending/PendingReleaseRepository.cs | 31 + .../Download/Pending/PendingReleaseService.cs | 219 +++ .../Pending/PendingReleasesUpdatedEvent.cs | 8 + .../Download/ProcessDownloadDecisions.cs | 103 ++ .../Download/ProcessedDecisions.cs | 17 + src/NzbDrone.Core/History/HistoryService.cs | 7 +- .../CleanupOrphanedPendingReleases.cs | 31 + .../IndexerSearch/EpisodeSearchService.cs | 19 +- .../IndexerSearch/SeasonSearchService.cs | 10 +- .../IndexerSearch/SeriesSearchService.cs | 8 +- .../Indexers/RssSyncCompleteEvent.cs | 8 + src/NzbDrone.Core/Indexers/RssSyncService.cs | 38 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../EpisodeImport/ImportDecisionMaker.cs | 3 +- .../Specifications/UpgradeSpecification.cs | 3 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 30 +- src/NzbDrone.Core/Parser/Language.cs | 25 +- src/NzbDrone.Core/Profiles/GrabDelayMode.cs | 9 + src/NzbDrone.Core/Profiles/Profile.cs | 18 + .../Profiles/ProfileInUseException.cs | 13 + .../Profiles/ProfileQualityItem.cs | 11 + .../Profiles/ProfileRepository.cs | 18 + src/NzbDrone.Core/Profiles/ProfileService.cs | 106 ++ .../Qualities/QualityModelComparer.cs | 21 +- src/NzbDrone.Core/Qualities/QualityProfile.cs | 12 - .../Qualities/QualityProfileInUseException.cs | 13 - .../Qualities/QualityProfileItem.cs | 11 - .../Qualities/QualityProfileRepository.cs | 19 - .../Qualities/QualityProfileService.cs | 105 -- src/NzbDrone.Core/Queue/QueueService.cs | 29 +- src/NzbDrone.Core/Tv/EpisodeCutoffService.cs | 17 +- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 2 +- src/NzbDrone.Core/Tv/Series.cs | 6 +- .../Validation/LangaugeValidator.cs | 21 + .../Validation/RuleBuilderExtensions.cs | 6 + .../EpisodeIntegrationTests.cs | 2 +- .../NzbDrone.Integration.Test.csproj | 1 - .../QualityProfileIntegrationTest.cs | 10 - .../SeriesEditorIntegrationTest.cs | 6 +- .../SeriesIntegrationTest.cs | 4 +- src/UI/AddSeries/AddSeriesLayout.js | 6 +- src/UI/AddSeries/SearchResultView.js | 36 +- .../AddSeries/SearchResultViewTemplate.html | 4 +- src/UI/Cells/Edit/QualityCellEditor.js | 14 +- .../{QualityProfileCell.js => ProfileCell.js} | 10 +- src/UI/Config.js | 2 +- .../Summary/EpisodeSummaryLayoutTemplate.html | 2 +- src/UI/Handlebars/Helpers/Quality.js | 10 +- src/UI/Handlebars/Helpers/String.js | 9 + .../backbone.marionette.templates.js | 2 +- src/UI/History/Queue/QueueStatusCell.js | 5 + src/UI/History/Queue/TimeleftCell.js | 11 +- src/UI/Profile/ProfileCollection.js | 18 + .../ProfileModel.js} | 0 .../ProfileSelectionPartial.html} | 2 +- src/UI/Quality/QualityProfileCollection.js | 18 - src/UI/Series/Details/InfoViewTemplate.html | 2 +- src/UI/Series/Edit/EditSeriesView.js | 12 +- .../Series/Edit/EditSeriesViewTemplate.html | 6 +- .../Series/Editor/SeriesEditorFooterView.js | 18 +- .../SeriesEditorFooterViewTemplate.html | 6 +- src/UI/Series/Editor/SeriesEditorLayout.js | 52 +- .../SeriesOverviewItemViewTemplate.html | 2 +- src/UI/Series/Index/SeriesIndexLayout.js | 12 +- .../{Quality => }/Profile/AllowedLabeler.js | 1 + .../DeleteProfileView.js} | 2 +- .../DeleteProfileViewTemplate.html} | 0 .../Edit/EditProfileItemView.js} | 2 +- .../Edit/EditProfileItemViewTemplate.html} | 0 .../Edit/EditProfileLayout.js} | 28 +- .../Edit/EditProfileLayoutTemplate.html} | 0 .../Settings/Profile/Edit/EditProfileView.js | 61 + .../Profile/Edit/EditProfileViewTemplate.html | 71 + .../Edit/QualitySortableCollectionView.js | 22 + .../Profile/Language/LanguageCollection.js | 18 + .../Profile/Language/LanguageModel.js | 10 + src/UI/Settings/Profile/LanguageLabel.js | 20 + .../ProfileCollectionTemplate.html} | 6 +- .../ProfileCollectionView.js} | 14 +- src/UI/Settings/Profile/ProfileLayout.js | 27 + .../Profile/ProfileLayoutTemplate.html | 3 + .../Profile/ProfileSchemaCollection.js | 13 + .../ProfileView.js} | 7 +- .../Settings/Profile/ProfileViewTemplate.html | 16 + src/UI/Settings/Profile/profile.less | 31 + .../Definition/QualityDefinitionView.js | 2 +- .../Profile/Edit/EditQualityProfileView.js | 26 - .../Edit/EditQualityProfileViewTemplate.html | 23 - .../Edit/QualitySortableCollectionView.js | 22 - .../Profile/QualityProfileSchemaCollection.js | 13 - .../Profile/QualityProfileViewTemplate.html | 9 - src/UI/Settings/Quality/QualityLayout.js | 7 +- .../Quality/QualityLayoutTemplate.html | 6 - src/UI/Settings/Quality/quality.less | 24 - src/UI/Settings/SettingsLayout.js | 18 + src/UI/Settings/SettingsLayoutTemplate.html | 2 + src/UI/Settings/settings.less | 1 + 182 files changed, 2493 insertions(+), 2433 deletions(-) create mode 100644 src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs create mode 100644 src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs create mode 100644 src/NzbDrone.Api/Profiles/LegacyProfileModule.cs create mode 100644 src/NzbDrone.Api/Profiles/ProfileModule.cs create mode 100644 src/NzbDrone.Api/Profiles/ProfileResource.cs rename src/NzbDrone.Api/{Qualities/QualityProfileSchemaModule.cs => Profiles/ProfileSchemaModule.cs} (50%) rename src/NzbDrone.Api/{Qualities/QualityProfileValidation.cs => Profiles/ProfileValidation.cs} (69%) delete mode 100644 src/NzbDrone.Api/Qualities/QualityProfileModule.cs delete mode 100644 src/NzbDrone.Api/Qualities/QualityProfileResource.cs create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs create mode 100644 src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/ProcessFixture.cs create mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs create mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs rename src/NzbDrone.Core.Test/{Qualities/QualityProfileRepositoryFixture.cs => Profiles/ProfileRepositoryFixture.cs} (52%) rename src/NzbDrone.Core.Test/{Qualities/QualityProfileServiceFixture.cs => Profiles/ProfileServiceFixture.cs} (61%) delete mode 100644 src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/EpisodeProviderTest.cs rename src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/{QualityProfileRepositoryFixture.cs => SeriesRepositoryFixture.cs} (74%) create mode 100644 src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs rename src/NzbDrone.Core/DecisionEngine/{Specifications => }/DownloadDecision.cs (53%) create mode 100644 src/NzbDrone.Core/DecisionEngine/Rejection.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/RejectionType.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs delete mode 100644 src/NzbDrone.Core/Download/DownloadApprovedReports.cs create mode 100644 src/NzbDrone.Core/Download/Pending/PendingRelease.cs create mode 100644 src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs create mode 100644 src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs create mode 100644 src/NzbDrone.Core/Download/Pending/PendingReleasesUpdatedEvent.cs create mode 100644 src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs create mode 100644 src/NzbDrone.Core/Download/ProcessedDecisions.cs create mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs create mode 100644 src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs create mode 100644 src/NzbDrone.Core/Profiles/GrabDelayMode.cs create mode 100644 src/NzbDrone.Core/Profiles/Profile.cs create mode 100644 src/NzbDrone.Core/Profiles/ProfileInUseException.cs create mode 100644 src/NzbDrone.Core/Profiles/ProfileQualityItem.cs create mode 100644 src/NzbDrone.Core/Profiles/ProfileRepository.cs create mode 100644 src/NzbDrone.Core/Profiles/ProfileService.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualityProfile.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualityProfileInUseException.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualityProfileItem.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualityProfileRepository.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualityProfileService.cs create mode 100644 src/NzbDrone.Core/Validation/LangaugeValidator.cs delete mode 100644 src/NzbDrone.Integration.Test/QualityProfileIntegrationTest.cs rename src/UI/Cells/{QualityProfileCell.js => ProfileCell.js} (51%) create mode 100644 src/UI/Handlebars/Helpers/String.js create mode 100644 src/UI/Profile/ProfileCollection.js rename src/UI/{Quality/QualityProfileModel.js => Profile/ProfileModel.js} (100%) rename src/UI/{Quality/QualityProfileSelectionPartial.html => Profile/ProfileSelectionPartial.html} (53%) delete mode 100644 src/UI/Quality/QualityProfileCollection.js rename src/UI/Settings/{Quality => }/Profile/AllowedLabeler.js (99%) rename src/UI/Settings/{Quality/Profile/DeleteQualityProfileView.js => Profile/DeleteProfileView.js} (86%) rename src/UI/Settings/{Quality/Profile/DeleteQualityProfileViewTemplate.html => Profile/DeleteProfileViewTemplate.html} (100%) rename src/UI/Settings/{Quality/Profile/Edit/EditQualityProfileItemView.js => Profile/Edit/EditProfileItemView.js} (61%) rename src/UI/Settings/{Quality/Profile/Edit/EditQualityProfileItemViewTemplate.html => Profile/Edit/EditProfileItemViewTemplate.html} (100%) rename src/UI/Settings/{Quality/Profile/Edit/EditQualityProfileLayout.js => Profile/Edit/EditProfileLayout.js} (83%) rename src/UI/Settings/{Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html => Profile/Edit/EditProfileLayoutTemplate.html} (100%) create mode 100644 src/UI/Settings/Profile/Edit/EditProfileView.js create mode 100644 src/UI/Settings/Profile/Edit/EditProfileViewTemplate.html create mode 100644 src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js create mode 100644 src/UI/Settings/Profile/Language/LanguageCollection.js create mode 100644 src/UI/Settings/Profile/Language/LanguageModel.js create mode 100644 src/UI/Settings/Profile/LanguageLabel.js rename src/UI/Settings/{Quality/Profile/QualityProfileCollectionTemplate.html => Profile/ProfileCollectionTemplate.html} (64%) rename src/UI/Settings/{Quality/Profile/QualityProfileCollectionView.js => Profile/ProfileCollectionView.js} (69%) create mode 100644 src/UI/Settings/Profile/ProfileLayout.js create mode 100644 src/UI/Settings/Profile/ProfileLayoutTemplate.html create mode 100644 src/UI/Settings/Profile/ProfileSchemaCollection.js rename src/UI/Settings/{Quality/Profile/QualityProfileView.js => Profile/ProfileView.js} (81%) create mode 100644 src/UI/Settings/Profile/ProfileViewTemplate.html create mode 100644 src/UI/Settings/Profile/profile.less delete mode 100644 src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js delete mode 100644 src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html delete mode 100644 src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js delete mode 100644 src/UI/Settings/Quality/Profile/QualityProfileSchemaCollection.js delete mode 100644 src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html diff --git a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs index 00c52d90e..1e02507f0 100644 --- a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs +++ b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs @@ -11,15 +11,16 @@ using NzbDrone.Api.History; using NzbDrone.Api.Indexers; using NzbDrone.Api.Logs; using NzbDrone.Api.Mapping; -using NzbDrone.Api.Qualities; +using NzbDrone.Api.Profiles; using NzbDrone.Api.RootFolders; using NzbDrone.Api.Series; -using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; @@ -41,8 +42,8 @@ namespace NzbDrone.Api.Test.MappingTests [TestCase(typeof(ParsedEpisodeInfo), typeof(ReleaseResource))] [TestCase(typeof(DownloadDecision), typeof(ReleaseResource))] [TestCase(typeof(Core.History.History), typeof(HistoryResource))] - [TestCase(typeof(QualityProfile), typeof(QualityProfileResource))] - [TestCase(typeof(QualityProfileItem), typeof(QualityProfileItemResource))] + [TestCase(typeof(Profile), typeof(ProfileResource))] + [TestCase(typeof(ProfileQualityItem), typeof(ProfileQualityItemResource))] [TestCase(typeof(Log), typeof(LogResource))] [TestCase(typeof(Command), typeof(CommandResource))] public void matching_fields(Type modelType, Type resourceType) @@ -105,16 +106,16 @@ namespace NzbDrone.Api.Test.MappingTests [Test] - public void should_map_qualityprofile() + public void should_map_profile() { - var profileResource = new QualityProfileResource + var profileResource = new ProfileResource { Cutoff = Quality.WEBDL1080p, - Items = new List { new QualityProfileItemResource { Quality = Quality.WEBDL1080p, Allowed = true } } + Items = new List { new ProfileQualityItemResource { Quality = Quality.WEBDL1080p, Allowed = true } } }; - profileResource.InjectTo(); + profileResource.InjectTo(); } diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 0e01fada3..a64222b0b 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -5,7 +5,6 @@ using Nancy; using NLog; using NzbDrone.Api.Mapping; using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Indexers; @@ -15,7 +14,6 @@ using Omu.ValueInjecter; using System.Linq; using Nancy.ModelBinding; using NzbDrone.Api.Extensions; -using NzbDrone.Core.Qualities; namespace NzbDrone.Api.Indexers { @@ -106,14 +104,14 @@ namespace NzbDrone.Api.Indexers release.InjectFrom(downloadDecision.RemoteEpisode.Release); release.InjectFrom(downloadDecision.RemoteEpisode.ParsedEpisodeInfo); release.InjectFrom(downloadDecision); - release.Rejections = downloadDecision.Rejections.ToList(); + release.Rejections = downloadDecision.Rejections.Select(r => r.Reason).ToList(); release.DownloadAllowed = downloadDecision.RemoteEpisode.DownloadAllowed; release.ReleaseWeight = result.Count; if (downloadDecision.RemoteEpisode.Series != null) { - release.QualityWeight = downloadDecision.RemoteEpisode.Series.QualityProfile.Value.Items.FindIndex(v => v.Quality == release.Quality.Quality) * 2; + release.QualityWeight = downloadDecision.RemoteEpisode.Series.Profile.Value.Items.FindIndex(v => v.Quality == release.Quality.Quality) * 2; } if (!release.Quality.Proper) diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index bb6f53379..a07861eb0 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -28,6 +28,8 @@ namespace NzbDrone.Api.Indexers public int[] EpisodeNumbers { get; set; } public int[] AbsoluteEpisodeNumbers { get; set; } public Boolean Approved { get; set; } + public Boolean TemporarilyRejected { get; set; } + public Boolean Rejected { get; set; } public Int32 TvRageId { get; set; } public IEnumerable Rejections { get; set; } public DateTime PublishDate { get; set; } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 75878901e..80bb065fb 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -150,6 +150,9 @@ + + + @@ -175,7 +178,7 @@ - + @@ -183,7 +186,7 @@ - + @@ -202,8 +205,8 @@ - - + + diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs new file mode 100644 index 000000000..147bc69aa --- /dev/null +++ b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Api.Profiles.Languages +{ + public class LanguageModule : NzbDroneRestModule + { + public LanguageModule() + { + GetResourceAll = GetAll; + GetResourceById = GetById; + } + + private LanguageResource GetById(int id) + { + var language = (Language)id; + + return new LanguageResource + { + Id = (int)language, + Name = language.ToString() + }; + } + + private List GetAll() + { + return ((Language[])Enum.GetValues(typeof (Language))) + .Select(l => new LanguageResource + { + Id = (int) l, + Name = l.ToString() + }) + .OrderBy(l => l.Name) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs new file mode 100644 index 000000000..e51d3f555 --- /dev/null +++ b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Profiles.Languages +{ + public class LanguageResource : RestResource + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public Int32 Id { get; set; } + public String Name { get; set; } + public String NameLower { get { return Name.ToLowerInvariant(); } } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/LegacyProfileModule.cs b/src/NzbDrone.Api/Profiles/LegacyProfileModule.cs new file mode 100644 index 000000000..23f76fc3c --- /dev/null +++ b/src/NzbDrone.Api/Profiles/LegacyProfileModule.cs @@ -0,0 +1,36 @@ +using System; +using System.Text; +using Nancy; +using Nancy.Responses; + +namespace NzbDrone.Api.Profiles +{ + class LegacyProfileModule : NzbDroneApiModule + { + public LegacyProfileModule() + : base("qualityprofile") + { + Get["/"] = x => + { + string queryString = ConvertQueryParams(Request.Query); + var url = String.Format("/api/profile?{0}", queryString); + + return Response.AsRedirect(url, RedirectResponse.RedirectType.Permanent); + }; + } + + private string ConvertQueryParams(DynamicDictionary query) + { + var sb = new StringBuilder(); + + foreach (var key in query) + { + var value = query[key]; + + sb.AppendFormat("&{0}={1}", key, value); + } + + return sb.ToString().Trim('&'); + } + } +} diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs new file mode 100644 index 000000000..413d99281 --- /dev/null +++ b/src/NzbDrone.Api/Profiles/ProfileModule.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Api.Mapping; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Api.Profiles +{ + public class ProfileModule : NzbDroneRestModule + { + private readonly IProfileService _profileService; + + public ProfileModule(IProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); + SharedValidator.RuleFor(c => c.Language).ValidLanguage(); + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + } + + private int Create(ProfileResource resource) + { + var model = resource.InjectTo(); + model = _profileService.Add(model); + return model.Id; + } + + private void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + private void Update(ProfileResource resource) + { + var model = _profileService.Get(resource.Id); + + model.Name = resource.Name; + model.Cutoff = (Quality)resource.Cutoff.Id; + model.Items = resource.Items.InjectTo>(); + model.Language = resource.Language; + model.GrabDelay = resource.GrabDelay; + model.GrabDelayMode = resource.GrabDelayMode; + + _profileService.Update(model); + } + + private ProfileResource GetById(int id) + { + return _profileService.Get(id).InjectTo(); + } + + private List GetAll() + { + var profiles = _profileService.All().InjectTo>(); + + return profiles; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs new file mode 100644 index 000000000..432569460 --- /dev/null +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Api.REST; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.Profiles +{ + public class ProfileResource : RestResource + { + public String Name { get; set; } + public Quality Cutoff { get; set; } + public List Items { get; set; } + public Language Language { get; set; } + public Int32 GrabDelay { get; set; } + public GrabDelayMode GrabDelayMode { get; set; } + } + + public class ProfileQualityItemResource : RestResource + { + public Quality Quality { get; set; } + public bool Allowed { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs similarity index 50% rename from src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs rename to src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs index 9fb05c48a..baa115cfe 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs @@ -1,34 +1,37 @@ using System.Collections.Generic; -using NzbDrone.Core.Qualities; -using NzbDrone.Api.Mapping; using System.Linq; +using NzbDrone.Api.Mapping; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; -namespace NzbDrone.Api.Qualities +namespace NzbDrone.Api.Profiles { - public class QualityProfileSchemaModule : NzbDroneRestModule + public class ProfileSchemaModule : NzbDroneRestModule { private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) - : base("/qualityprofile/schema") + public ProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) + : base("/profile/schema") { _qualityDefinitionService = qualityDefinitionService; GetResourceAll = GetAll; } - private List GetAll() + private List GetAll() { var items = _qualityDefinitionService.All() .OrderBy(v => v.Weight) - .Select(v => new QualityProfileItem { Quality = v.Quality, Allowed = false }) + .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) .ToList(); - var profile = new QualityProfile(); + var profile = new Profile(); profile.Cutoff = Quality.Unknown; profile.Items = items; + profile.Language = Language.English; - return new List { profile.InjectTo() }; + return new List { profile.InjectTo() }; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileValidation.cs b/src/NzbDrone.Api/Profiles/ProfileValidation.cs similarity index 69% rename from src/NzbDrone.Api/Qualities/QualityProfileValidation.cs rename to src/NzbDrone.Api/Profiles/ProfileValidation.cs index c90ebda61..003c96f39 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileValidation.cs +++ b/src/NzbDrone.Api/Profiles/ProfileValidation.cs @@ -3,11 +3,11 @@ using System.Linq; using FluentValidation; using FluentValidation.Validators; -namespace NzbDrone.Api.Qualities +namespace NzbDrone.Api.Profiles { - public static class QualityProfileValidation + public static class ProfileValidation { - public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) + public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); @@ -25,7 +25,7 @@ namespace NzbDrone.Api.Qualities protected override bool IsValid(PropertyValidatorContext context) { - var list = context.PropertyValue as IList; + var list = context.PropertyValue as IList; if (list == null) { diff --git a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs deleted file mode 100644 index 6cb1b16df..000000000 --- a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Qualities; -using NzbDrone.Api.Mapping; -using FluentValidation; - -namespace NzbDrone.Api.Qualities -{ - public class QualityProfileModule : NzbDroneRestModule - { - private readonly IQualityProfileService _qualityProfileService; - - public QualityProfileModule(IQualityProfileService qualityProfileService) - { - _qualityProfileService = qualityProfileService; - SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();//.SetValidator(new AllowedValidator()); - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; - } - - private int Create(QualityProfileResource resource) - { - var model = resource.InjectTo(); - model = _qualityProfileService.Add(model); - return model.Id; - } - - private void DeleteProfile(int id) - { - _qualityProfileService.Delete(id); - } - - private void Update(QualityProfileResource resource) - { - var model = _qualityProfileService.Get(resource.Id); - - model.Name = resource.Name; - model.Cutoff = (Quality)resource.Cutoff.Id; - model.Items = resource.Items.InjectTo>(); - _qualityProfileService.Update(model); - } - - private QualityProfileResource GetById(int id) - { - return _qualityProfileService.Get(id).InjectTo(); - } - - private List GetAll() - { - var profiles = _qualityProfileService.All().InjectTo>(); - - return profiles; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs deleted file mode 100644 index dbeff0fa0..000000000 --- a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.Qualities -{ - public class QualityProfileResource : RestResource - { - public String Name { get; set; } - public Quality Cutoff { get; set; } - public List Items { get; set; } - } - - public class QualityProfileItemResource : RestResource - { - public Quality Quality { get; set; } - public bool Allowed { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Queue/QueueModule.cs b/src/NzbDrone.Api/Queue/QueueModule.cs index 148a2210f..1c735050a 100644 --- a/src/NzbDrone.Api/Queue/QueueModule.cs +++ b/src/NzbDrone.Api/Queue/QueueModule.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Queue; @@ -7,25 +9,40 @@ using NzbDrone.Core.Queue; namespace NzbDrone.Api.Queue { public class QueueModule : NzbDroneRestModuleWithSignalR, - IHandle + IHandle, IHandle { private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; - public QueueModule(ICommandExecutor commandExecutor, IQueueService queueService) + public QueueModule(ICommandExecutor commandExecutor, IQueueService queueService, IPendingReleaseService pendingReleaseService) : base(commandExecutor) { _queueService = queueService; + _pendingReleaseService = pendingReleaseService; GetResourceAll = GetQueue; } private List GetQueue() { - return ToListResource(_queueService.GetQueue); + return ToListResource(GetQueueItems); + } + + private IEnumerable GetQueueItems() + { + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + + return queue.Concat(pending); } public void Handle(UpdateQueueEvent message) { BroadcastResourceChange(ModelAction.Sync); } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index 1c5806549..d80bf60ff 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -15,7 +15,6 @@ using NzbDrone.Api.Mapping; using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.DataAugmentation.Scene; -using Omu.ValueInjecter; namespace NzbDrone.Api.Series { @@ -27,7 +26,6 @@ namespace NzbDrone.Api.Series IHandle { - private readonly ICommandExecutor _commandExecutor; private readonly ISeriesService _seriesService; private readonly ISeriesStatisticsService _seriesStatisticsService; private readonly ISceneMappingService _sceneMappingService; @@ -47,7 +45,6 @@ namespace NzbDrone.Api.Series ) : base(commandExecutor) { - _commandExecutor = commandExecutor; _seriesService = seriesService; _seriesStatisticsService = seriesStatisticsService; _sceneMappingService = sceneMappingService; @@ -60,7 +57,7 @@ namespace NzbDrone.Api.Series UpdateResource = UpdateSeries; DeleteResource = DeleteSeries; - SharedValidator.RuleFor(s => s.QualityProfileId).ValidId(); + SharedValidator.RuleFor(s => s.ProfileId).ValidId(); SharedValidator.RuleFor(s => s.Path) .Cascade(CascadeMode.StopOnFirstFailure) diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs index b6b980b03..39ee26e0a 100644 --- a/src/NzbDrone.Api/Series/SeriesResource.cs +++ b/src/NzbDrone.Api/Series/SeriesResource.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Api.Series { //Todo: Sorters should be done completely on the client //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? - //Todo: We should get the entire QualityProfile instead of ID and Name separately + //Todo: We should get the entire Profile instead of ID and Name separately //View Only public String Title { get; set; } @@ -32,7 +32,7 @@ namespace NzbDrone.Api.Series public Int32 EpisodeCount { get; set; } public Int32 EpisodeFileCount { get; set; } public SeriesStatusType Status { get; set; } - public String QualityProfileName { get; set; } + public String ProfileName { get; set; } public String Overview { get; set; } public DateTime? NextAiring { get; set; } public DateTime? PreviousAiring { get; set; } @@ -46,7 +46,7 @@ namespace NzbDrone.Api.Series //View & Edit public String Path { get; set; } - public Int32 QualityProfileId { get; set; } + public Int32 ProfileId { get; set; } //Editing Only public Boolean SeasonFolder { get; set; } @@ -65,5 +65,21 @@ namespace NzbDrone.Api.Series public String RootFolderPath { get; set; } public String Certification { get; set; } public List Genres { get; set; } + + //Used to support legacy consumers + public Int32 QualityProfileId + { + get + { + return ProfileId; + } + set + { + if (value > 0 && ProfileId == 0) + { + ProfileId = value; + } + } + } } } diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs index 32aee7226..8cf59717b 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -1,6 +1,7 @@ using FizzWare.NBuilder; using NUnit.Framework; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Core.Qualities; @@ -15,19 +16,19 @@ namespace NzbDrone.Core.Test.Datastore [SetUp] public void Setup() { - var qualityProfile = new NzbDrone.Core.Qualities.QualityProfile + var profile = new Profile { Name = "Test", Cutoff = Quality.WEBDL720p, - Items = NzbDrone.Core.Test.Qualities.QualityFixture.GetDefaultQualities() + Items = Qualities.QualityFixture.GetDefaultQualities() }; - qualityProfile = Db.Insert(qualityProfile); + profile = Db.Insert(profile); var series = Builder.CreateListOfSize(1) .All() - .With(v => v.QualityProfileId = qualityProfile.Id) + .With(v => v.ProfileId = profile.Id) .BuildListOfNew(); Db.InsertMany(series); @@ -50,7 +51,7 @@ namespace NzbDrone.Core.Test.Datastore } [Test] - public void should_lazy_load_qualityprofile_if_not_joined() + public void should_lazy_load_profile_if_not_joined() { var db = Mocker.Resolve(); var DataMapper = db.GetDataMapper(); @@ -62,7 +63,7 @@ namespace NzbDrone.Core.Test.Datastore foreach (var episode in episodes) { Assert.IsNotNull(episode.Series); - Assert.IsFalse(episode.Series.QualityProfile.IsLoaded); + Assert.IsFalse(episode.Series.Profile.IsLoaded); } } @@ -84,20 +85,20 @@ namespace NzbDrone.Core.Test.Datastore } [Test] - public void should_explicit_load_qualityprofile_if_joined() + public void should_explicit_load_profile_if_joined() { var db = Mocker.Resolve(); var DataMapper = db.GetDataMapper(); var episodes = DataMapper.Query() .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id) - .Join(Marr.Data.QGen.JoinType.Inner, v => v.QualityProfile, (l, r) => l.QualityProfileId == r.Id) + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Profile, (l, r) => l.ProfileId == r.Id) .ToList(); foreach (var episode in episodes) { Assert.IsNotNull(episode.Series); - Assert.IsTrue(episode.Series.QualityProfile.IsLoaded); + Assert.IsTrue(episode.Series.Profile.IsLoaded); } } diff --git a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs index 17b45da97..2db04935f 100644 --- a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using System.Linq; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests { @@ -119,5 +120,23 @@ namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests newColumns.Values.Should().HaveSameCount(columns.Values); newIndexes.Should().Contain(i=>i.Column == "AirTime"); } + + [Test] + public void should_create_indexes_with_the_same_uniqueness() + { + var columns = _subject.GetColumns("Series"); + var indexes = _subject.GetIndexes("Series"); + + var tempIndexes = indexes.JsonClone(); + + tempIndexes[0].Unique = false; + tempIndexes[1].Unique = true; + + _subject.CreateTable("Series_New", columns.Values, tempIndexes); + var newIndexes = _subject.GetIndexes("Series_New"); + + newIndexes.Should().HaveSameCount(tempIndexes); + newIndexes.ShouldAllBeEquivalentTo(tempIndexes, options => options.Excluding(o => o.IndexName).Excluding(o => o.Table)); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/DuplicateFixture.cs b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/DuplicateFixture.cs index d193fb1ec..c7e8e2ad3 100644 --- a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/DuplicateFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/DuplicateFixture.cs @@ -25,12 +25,12 @@ namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests { var series = Builder.CreateListOfSize(10) .Random(3) - .With(c => c.QualityProfileId = 100) + .With(c => c.ProfileId = 100) .BuildListOfNew(); Db.InsertMany(series); - var duplicates = _subject.GetDuplicates("series", "QualityProfileId").ToList(); + var duplicates = _subject.GetDuplicates("series", "ProfileId").ToList(); duplicates.Should().HaveCount(1); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index 861d39d32..d7ae1643c 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; @@ -13,35 +14,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_current_episode_is_less_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.DVD, true)).Should().BeTrue(); } [Test] public void should_return_false_if_current_episode_is_equal_to_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true)).Should().BeFalse(); } [Test] public void should_return_false_if_current_episode_is_greater_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } [Test] public void should_return_true_when_new_episode_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, false), new QualityModel(Quality.HDTV720p, true)).Should().BeTrue(); } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true), new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 0fc46ab1f..f26cdf7ea 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Download.Clients.Sabnzbd; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; @@ -42,7 +43,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; _fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode @@ -62,9 +63,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _upgradableQuality = new QualityModel(Quality.SDTV, false); _notupgradableQuality = new QualityModel(Quality.HDTV1080p, true); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_notupgradableQuality); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 2)).Returns(_notupgradableQuality); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 3)).Returns(null); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_notupgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 2)).Returns(_notupgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 3)).Returns(null); Mocker.GetMock() .Setup(c => c.GetDownloadClients()) @@ -73,12 +74,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void WithFirstReportUpgradable() { - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_upgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_upgradableQuality); } private void WithSecondReportUpgradable() { - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 2)).Returns(_upgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 2)).Returns(_upgradableQuality); } private void GivenSabnzbdDownloadClient() @@ -132,11 +133,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing() { - _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, false); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, false); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_upgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_upgradableQuality); _upgradeHistory.IsSatisfiedBy(_parseResultSingle, null).Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs index 10ffdc806..37cde0cd2 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs @@ -1,9 +1,12 @@ using FluentAssertions; +using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -11,28 +14,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public class LanguageSpecificationFixture : CoreTest { - private RemoteEpisode parseResult; + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _remoteEpisode = new RemoteEpisode + { + ParsedEpisodeInfo = new ParsedEpisodeInfo + { + Language = Language.English + }, + Series = new Series + { + Profile = new LazyLoaded(new Profile + { + Language = Language.English + }) + } + }; + } private void WithEnglishRelease() { - parseResult = new RemoteEpisode - { - ParsedEpisodeInfo = new ParsedEpisodeInfo - { - Language = Language.English - } - }; + _remoteEpisode.ParsedEpisodeInfo.Language = Language.English; } private void WithGermanRelease() { - parseResult = new RemoteEpisode - { - ParsedEpisodeInfo = new ParsedEpisodeInfo - { - Language = Language.German - } - }; + _remoteEpisode.ParsedEpisodeInfo.Language = Language.German; } [Test] @@ -40,7 +50,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { WithEnglishRelease(); - Mocker.Resolve().IsSatisfiedBy(parseResult, null).Should().BeTrue(); + Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); } [Test] @@ -48,7 +58,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { WithGermanRelease(); - Mocker.Resolve().IsSatisfiedBy(parseResult, null).Should().BeFalse(); + Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Should().BeFalse(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index ff0457b43..be385ae90 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; @@ -27,7 +28,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { _series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _episode = Builder.CreateNew() diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index b91614795..1f8119b68 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Tv; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.DecisionEngine; @@ -38,7 +40,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests remoteEpisode.Release.Size = size; remoteEpisode.Series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); return remoteEpisode; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index d8d46f3c0..02f017d7a 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -5,6 +5,7 @@ using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; @@ -35,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { var fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = (LazyLoaded)new QualityProfile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p }) .Build(); remoteEpisode = new RemoteEpisode @@ -49,7 +50,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_allow_if_quality_is_defined_in_profile(Quality qualityType) { remoteEpisode.ParsedEpisodeInfo.Quality.Quality = qualityType; - remoteEpisode.Series.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p); + remoteEpisode.Series.Profile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p); Subject.IsSatisfiedBy(remoteEpisode, null).Should().BeTrue(); } @@ -58,7 +59,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_not_allow_if_quality_is_not_defined_in_profile(Quality qualityType) { remoteEpisode.ParsedEpisodeInfo.Quality.Quality = qualityType; - remoteEpisode.Series.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p); + remoteEpisode.Series.Profile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p); Subject.IsSatisfiedBy(remoteEpisode, null).Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index e91771776..b8596840f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -2,6 +2,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; @@ -43,9 +44,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(true); - var qualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }; + var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }; - Subject.IsUpgradable(qualityProfile, new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) + Subject.IsUpgradable(profile, new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) .Should().Be(expected); } @@ -54,9 +55,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(false); - var qualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }; + var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }; - Subject.IsUpgradable(qualityProfile, new QualityModel(Quality.DVD, true), new QualityModel(Quality.DVD, false)) + Subject.IsUpgradable(profile, new QualityModel(Quality.DVD, true), new QualityModel(Quality.DVD, false)) .Should().BeFalse(); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs new file mode 100644 index 000000000..29ce17057 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Marr.Data; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.DecisionEngine.Specifications.RssSync; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.History; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync +{ + [TestFixture] + public class DelaySpecificationFixture : CoreTest + { + private Profile _profile; + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _profile = Builder.CreateNew() + .Build(); + + var series = Builder.CreateNew() + .With(s => s.Profile = _profile) + .Build(); + + _remoteEpisode = Builder.CreateNew() + .With(r => r.Series = series) + .Build(); + + _profile.Items = new List(); + _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }); + _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.WEBDL720p }); + _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p }); + + _profile.Cutoff = Quality.WEBDL720p; + + _profile.GrabDelayMode = GrabDelayMode.Always; + + _remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo(); + _remoteEpisode.Release = new ReleaseInfo(); + + _remoteEpisode.Episodes = Builder.CreateListOfSize(1).Build().ToList(); + } + + private void GivenExistingFile(QualityModel quality) + { + _remoteEpisode.Episodes[0].EpisodeFile = new LazyLoaded(new EpisodeFile + { + Quality = quality + }); + } + + [Test] + public void should_be_true_when_search() + { + Subject.IsSatisfiedBy(new RemoteEpisode(), new SingleEpisodeSearchCriteria()).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_profile_does_not_have_a_delay() + { + _profile.GrabDelay = 0; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_quality_is_last_allowed_in_profile() + { + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray720p); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_release_is_older_than_delay() + { + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddHours(-10); + + _profile.GrabDelay = 1; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_false_when_release_is_younger_than_delay() + { + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.SDTV); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + _profile.GrabDelay = 12; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeFalse(); + } + + [Test] + public void should_be_true_when_release_is_proper_for_existing_episode() + { + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p, true); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + GivenExistingFile(new QualityModel(Quality.HDTV720p)); + + _profile.GrabDelay = 12; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_false_when_release_is_proper_and_no_existing_episode() + { + + } + + [Test] + public void should_be_true_when_release_meets_cutoff_and_mode_is_cutoff() + { + _profile.GrabDelayMode = GrabDelayMode.Cutoff; + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + _profile.GrabDelay = 12; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_release_exceeds_cutoff_and_mode_is_cutoff() + { + _profile.GrabDelayMode = GrabDelayMode.Cutoff; + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + _profile.GrabDelay = 12; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_false_when_release_is_below_cutoff_and_mode_is_cutoff() + { + _profile.GrabDelayMode = GrabDelayMode.Cutoff; + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + _profile.GrabDelay = 12; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeFalse(); + } + + [Test] + public void should_be_false_when_release_is_proper_for_existing_episode_of_different_quality() + { + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p, true); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + GivenExistingFile(new QualityModel(Quality.SDTV)); + + _profile.GrabDelay = 12; + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeFalse(); + } + + [Test] + public void should_be_false_when_release_is_first_detected_and_mode_is_first() + { + _profile.GrabDelayMode = GrabDelayMode.First; + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + _profile.GrabDelay = 12; + + Mocker.GetMock() + .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny())) + .Returns(new List()); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeFalse(); + } + + [Test] + public void should_be_false_when_release_is_not_first_but_oldest_has_not_expired_and_type_is_first() + { + _profile.GrabDelayMode = GrabDelayMode.First; + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + + _profile.GrabDelay = 12; + + var pendingRemoteEpisode = _remoteEpisode.JsonClone(); + + Mocker.GetMock() + .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny())) + .Returns(new List { _remoteEpisode.JsonClone() }); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeFalse(); + } + + [Test] + public void should_be_true_when_existing_pending_release_expired_and_mode_is_first() + { + _profile.GrabDelayMode = GrabDelayMode.First; + + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + _profile.GrabDelay = 12; + + var pendingRemoteEpisode = _remoteEpisode.JsonClone(); + pendingRemoteEpisode.Release.PublishDate = DateTime.UtcNow.AddHours(-15); + + Mocker.GetMock() + .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny())) + .Returns(new List { pendingRemoteEpisode }); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_one_existing_pending_release_is_expired_and_mode_is_first() + { + _profile.GrabDelayMode = GrabDelayMode.First; + + _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow; + _profile.GrabDelay = 12; + + var pendingRemoteEpisode1 = _remoteEpisode.JsonClone(); + pendingRemoteEpisode1.Release.PublishDate = DateTime.UtcNow.AddHours(-15); + + var pendingRemoteEpisode2 = _remoteEpisode.JsonClone(); + pendingRemoteEpisode2.Release.PublishDate = DateTime.UtcNow.AddHours(5); + + Mocker.GetMock() + .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny())) + .Returns(new List { pendingRemoteEpisode1, pendingRemoteEpisode2 }); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index b95925ced..f377b80d5 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; @@ -37,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync var doubleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } }; var fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p }) .Build(); _parseResultMulti = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index c9e03d256..e21e10d12 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; @@ -38,7 +39,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var doubleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } }; var fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index ed80e2c3c..529c1e355 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -4,19 +4,19 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; -using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; -using NzbDrone.Core.DecisionEngine; namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests { [TestFixture] - public class DownloadApprovedFixture : CoreTest + public class DownloadApprovedFixture : CoreTest { [SetUp] public void SetUp() @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release.PublishDate = DateTime.UtcNow; remoteEpisode.Series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); return remoteEpisode; @@ -62,7 +62,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisode)); - Subject.DownloadApproved(decisions); + Subject.ProcessDecisions(decisions); Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once()); } @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteEpisode)); decisions.Add(new DownloadDecision(remoteEpisode)); - Subject.DownloadApproved(decisions); + Subject.ProcessDecisions(decisions); Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once()); } @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteEpisode1)); decisions.Add(new DownloadDecision(remoteEpisode2)); - Subject.DownloadApproved(decisions); + Subject.ProcessDecisions(decisions); Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once()); } @@ -110,7 +110,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var decisions = new List(); decisions.Add(new DownloadDecision(remoteEpisode)); - Subject.DownloadApproved(decisions).Should().HaveCount(1); + Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(1); } [Test] @@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteEpisode1)); decisions.Add(new DownloadDecision(remoteEpisode2)); - Subject.DownloadApproved(decisions).Should().HaveCount(2); + Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(2); } [Test] @@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteEpisode2)); decisions.Add(new DownloadDecision(remoteEpisode3)); - Subject.DownloadApproved(decisions).Should().HaveCount(2); + Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(2); } [Test] @@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteEpisode)); Mocker.GetMock().Setup(s => s.DownloadReport(It.IsAny())).Throws(new Exception()); - Subject.DownloadApproved(decisions).Should().BeEmpty(); + Subject.ProcessDecisions(decisions).Grabbed.Should().BeEmpty(); ExceptionVerification.ExpectedWarns(1); } @@ -177,8 +177,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_return_an_empty_list_when_none_are_appproved() { var decisions = new List(); - decisions.Add(new DownloadDecision(null, "Failure!")); - decisions.Add(new DownloadDecision(null, "Failure!")); + decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); + decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); Subject.GetQualifiedReports(decisions).Should().BeEmpty(); } diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/ProcessFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/ProcessFixture.cs new file mode 100644 index 000000000..a913b62b1 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/ProcessFixture.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FizzWare.NBuilder; +using FluentAssertions; +using Marr.Data; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests +{ + [TestFixture] + public class ProcessFixture : CoreTest + { + private DownloadDecision _temporarilyRejected; + private Series _series; + private Episode _episode; + private Profile _profile; + private ReleaseInfo _release; + private ParsedEpisodeInfo _parsedEpisodeInfo; + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .Build(); + + _episode = Builder.CreateNew() + .Build(); + + _profile = new Profile + { + Name = "Test", + Cutoff = Quality.HDTV720p, + GrabDelay = 1, + Items = new List + { + new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, + new ProfileQualityItem { Allowed = true, Quality = Quality.WEBDL720p }, + new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p } + }, + }; + + _series.Profile = new LazyLoaded(_profile); + + _release = Builder.CreateNew().Build(); + + _parsedEpisodeInfo = Builder.CreateNew().Build(); + _parsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p); + + _remoteEpisode = new RemoteEpisode(); + _remoteEpisode.Episodes = new List{ _episode }; + _remoteEpisode.Series = _series; + _remoteEpisode.ParsedEpisodeInfo = _parsedEpisodeInfo; + _remoteEpisode.Release = _release; + + _temporarilyRejected = new DownloadDecision(_remoteEpisode, new Rejection("Temp Rejected", RejectionType.Temporary)); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(new List()); + + Mocker.GetMock() + .Setup(s => s.GetSeries(It.IsAny())) + .Returns(_series); + + Mocker.GetMock() + .Setup(s => s.GetEpisodes(It.IsAny(), _series, true, null)) + .Returns(new List {_episode}); + + Mocker.GetMock() + .Setup(s => s.PrioritizeDecisions(It.IsAny>())) + .Returns((List d) => d); + } + + private void GivenHeldRelease(String title, String indexer, DateTime publishDate) + { + var release = _release.JsonClone(); + release.Indexer = indexer; + release.PublishDate = publishDate; + + + var heldReleases = Builder.CreateListOfSize(1) + .All() + .With(h => h.SeriesId = _series.Id) + .With(h => h.Title = title) + .With(h => h.Release = release) + .Build(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(heldReleases); + } + + [Test] + public void should_add() + { + Subject.Add(_temporarilyRejected); + + VerifyInsert(); + } + + [Test] + public void should_not_add_if_it_is_the_same_release_from_the_same_indexer() + { + GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate); + + Subject.Add(_temporarilyRejected); + + VerifyNoInsert(); + } + + [Test] + public void should_add_if_title_is_different() + { + GivenHeldRelease(_release.Title + "-RP", _release.Indexer, _release.PublishDate); + + Subject.Add(_temporarilyRejected); + + VerifyInsert(); + } + + [Test] + public void should_add_if_indexer_is_different() + { + GivenHeldRelease(_release.Title, "AnotherIndexer", _release.PublishDate); + + Subject.Add(_temporarilyRejected); + + VerifyInsert(); + } + [Test] + public void should_add_if_publish_date_is_different() + { + GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate.AddHours(1)); + + Subject.Add(_temporarilyRejected); + + VerifyInsert(); + } + + private void VerifyInsert() + { + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Once()); + } + + private void VerifyNoInsert() + { + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Never()); + } + } +} diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 717f9f915..c7ea148ef 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; @@ -10,14 +11,14 @@ namespace NzbDrone.Core.Test.HistoryTests { public class HistoryServiceFixture : CoreTest { - private QualityProfile _profile; - private QualityProfile _profileCustom; + private Profile _profile; + private Profile _profileCustom; [SetUp] public void Setup() { - _profile = new QualityProfile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities() }; - _profileCustom = new QualityProfile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities(Quality.DVD) }; + _profile = new Profile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities() }; + _profileCustom = new Profile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities(Quality.DVD) }; } [Test] diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs new file mode 100644 index 000000000..350471a1d --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs @@ -0,0 +1,42 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupOrphanedBlacklistFixture : DbTest + { + [Test] + public void should_delete_orphaned_blacklist_items() + { + var blacklist = Builder.CreateNew() + .BuildNew(); + + Db.Insert(blacklist); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_unorphaned_blacklist_items() + { + var series = Builder.CreateNew().BuildNew(); + + Db.Insert(series); + + var blacklist = Builder.CreateNew() + .With(b => b.SeriesId = series.Id) + .BuildNew(); + + Db.Insert(blacklist); + + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs new file mode 100644 index 000000000..34965efae --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs @@ -0,0 +1,42 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupOrphanedPendingReleasesFixture : DbTest + { + [Test] + public void should_delete_orphaned_pending_items() + { + var pendingRelease = Builder.CreateNew() + .BuildNew(); + + Db.Insert(pendingRelease); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_unorphaned_pending_items() + { + var series = Builder.CreateNew().BuildNew(); + + Db.Insert(series); + + var pendingRelease = Builder.CreateNew() + .With(h => h.SeriesId = series.Id) + .BuildNew(); + + Db.Insert(pendingRelease); + + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs index c0e7c0e1c..9570fbfc4 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Test.Framework; using FizzWare.NBuilder; @@ -30,9 +31,9 @@ namespace NzbDrone.Core.Test.IndexerSearchTests .Setup(s => s.GetAvailableProviders()) .Returns(new List { indexer.Object }); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.GetSearchDecision(It.IsAny>(), It.IsAny())) - .Returns(new List()); + .Returns(new List()); _xemSeries = Builder.CreateNew() .With(v => v.UseSceneNumbering = true) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 0dea398ea..ccaf18d11 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _videoFiles = new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; _series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _quality = new QualityModel(Quality.DVD); diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index a0bff8424..a217610af 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -22,8 +23,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications public void Setup() { _series = Builder.CreateNew() - .With(s => s.SeriesType = SeriesTypes.Standard) - .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(s => s.SeriesType = SeriesTypes.Standard) + .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _localEpisode = new LocalEpisode diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 904a0e5c7..251e02c31 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -29,7 +30,7 @@ namespace NzbDrone.Core.Test.MediaFiles _approvedDecisions = new List(); var series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); var episodes = Builder.CreateListOfSize(5) diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index a36695aa6..b04fdc1b6 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -111,6 +111,7 @@ + @@ -123,6 +124,7 @@ + @@ -141,6 +143,8 @@ + + @@ -209,7 +213,7 @@ - + @@ -242,7 +246,7 @@ - + @@ -260,11 +264,10 @@ - - + diff --git a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs similarity index 52% rename from src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs rename to src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs index 10b8ece92..3c9003da7 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; -namespace NzbDrone.Core.Test.Qualities +namespace NzbDrone.Core.Test.Profiles { [TestFixture] - public class QualityProfileRepositoryFixture : DbTest + public class ProfileRepositoryFixture : DbTest { [Test] public void should_be_able_to_read_and_write() { - var profile = new QualityProfile + var profile = new Profile { - Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), + Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), Cutoff = Quality.Bluray1080p, Name = "TestProfile" }; @@ -23,8 +23,8 @@ namespace NzbDrone.Core.Test.Qualities StoredModel.Name.Should().Be(profile.Name); StoredModel.Cutoff.Should().Be(profile.Cutoff); - - StoredModel.Items.Should().Equal(profile.Items, (a,b) => a.Quality == b.Quality && a.Allowed == b.Allowed); + + StoredModel.Items.Should().Equal(profile.Items, (a, b) => a.Quality == b.Quality && a.Allowed == b.Allowed); } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs similarity index 61% rename from src/NzbDrone.Core.Test/Qualities/QualityProfileServiceFixture.cs rename to src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs index 9390925c1..7e33072af 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs @@ -3,23 +3,24 @@ using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Test.Qualities +namespace NzbDrone.Core.Test.Profiles { [TestFixture] - public class QualityProfileServiceFixture : CoreTest + public class ProfileServiceFixture : CoreTest { [Test] public void Init_should_add_two_profiles() { Subject.Handle(new ApplicationStartedEvent()); - Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(4)); + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(4)); } [Test] @@ -27,14 +28,14 @@ namespace NzbDrone.Core.Test.Qualities //We don't want to keep adding them back if a user deleted them on purpose. public void Init_should_skip_if_any_profiles_already_exist() { - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.All()) - .Returns(Builder.CreateListOfSize(2).Build().ToList()); + .Returns(Builder.CreateListOfSize(2).Build().ToList()); Subject.Handle(new ApplicationStartedEvent()); - Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Never()); } @@ -43,15 +44,15 @@ namespace NzbDrone.Core.Test.Qualities { var seriesList = Builder.CreateListOfSize(3) .Random(1) - .With(c => c.QualityProfileId = 2) + .With(c => c.ProfileId = 2) .Build().ToList(); Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); - Assert.Throws(() => Subject.Delete(2)); + Assert.Throws(() => Subject.Delete(2)); - Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); } @@ -61,7 +62,7 @@ namespace NzbDrone.Core.Test.Qualities { var seriesList = Builder.CreateListOfSize(3) .All() - .With(c => c.QualityProfileId = 2) + .With(c => c.ProfileId = 2) .Build().ToList(); @@ -69,7 +70,7 @@ namespace NzbDrone.Core.Test.Qualities Subject.Delete(1); - Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); + Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index ddab8f19b..673ca0ca9 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -44,7 +45,7 @@ namespace NzbDrone.Core.Test.Qualities i.Should().Be(expected); } - public static List GetDefaultQualities(params Quality[] allowed) + public static List GetDefaultQualities(params Quality[] allowed) { var qualities = new List { @@ -66,7 +67,7 @@ namespace NzbDrone.Core.Test.Qualities var items = qualities .Except(allowed) .Concat(allowed) - .Select(v => new QualityProfileItem { Quality = v, Allowed = allowed.Contains(v) }).ToList(); + .Select(v => new ProfileQualityItem { Quality = v, Allowed = allowed.Contains(v) }).ToList(); return items; } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index 187455abb..78262f0a5 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; @@ -13,20 +14,20 @@ namespace NzbDrone.Core.Test.Qualities { public QualityModelComparer Subject { get; set; } - private void GivenDefaultQualityProfile() + private void GivenDefaultProfile() { - Subject = new QualityModelComparer(new QualityProfile { Items = QualityFixture.GetDefaultQualities() }); + Subject = new QualityModelComparer(new Profile { Items = QualityFixture.GetDefaultQualities() }); } - private void GivenCustomQualityProfile() + private void GivenCustomProfile() { - Subject = new QualityModelComparer(new QualityProfile { Items = QualityFixture.GetDefaultQualities(Quality.Bluray720p, Quality.DVD) }); + Subject = new QualityModelComparer(new Profile { Items = QualityFixture.GetDefaultQualities(Quality.Bluray720p, Quality.DVD) }); } [Test] public void Icomparer_greater_test() { - GivenDefaultQualityProfile(); + GivenDefaultProfile(); var first = new QualityModel(Quality.DVD, true); var second = new QualityModel(Quality.Bluray1080p, true); @@ -39,7 +40,7 @@ namespace NzbDrone.Core.Test.Qualities [Test] public void Icomparer_greater_proper() { - GivenDefaultQualityProfile(); + GivenDefaultProfile(); var first = new QualityModel(Quality.Bluray1080p, false); var second = new QualityModel(Quality.Bluray1080p, true); @@ -52,7 +53,7 @@ namespace NzbDrone.Core.Test.Qualities [Test] public void Icomparer_lesser() { - GivenDefaultQualityProfile(); + GivenDefaultProfile(); var first = new QualityModel(Quality.DVD, true); var second = new QualityModel(Quality.Bluray1080p, true); @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Test.Qualities [Test] public void Icomparer_lesser_proper() { - GivenDefaultQualityProfile(); + GivenDefaultProfile(); var first = new QualityModel(Quality.DVD, false); var second = new QualityModel(Quality.DVD, true); @@ -78,7 +79,7 @@ namespace NzbDrone.Core.Test.Qualities [Test] public void Icomparer_greater_custom_order() { - GivenCustomQualityProfile(); + GivenCustomProfile(); var first = new QualityModel(Quality.DVD, true); var second = new QualityModel(Quality.Bluray720p, true); diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/EpisodeProviderTest.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/EpisodeProviderTest.cs deleted file mode 100644 index 6ed79d4bc..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/EpisodeProviderTest.cs +++ /dev/null @@ -1,1519 +0,0 @@ -//TODO: Alrighty then... We should delete this or uncomment some of these tests... - -/* - - -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Download; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Model; -using NzbDrone.Core.Providers; -using NzbDrone.Core.Providers.Core; -using NzbDrone.Core.Test.Framework; - -using TvdbLib.Data; - -namespace NzbDrone.Core.Test.TvTests.EpisodeProviderTests -{ - [TestFixture] - - public class EpisodeProviderTest : ObjectDbTest - { - [Test] - public void GetEpisodes_exists() - { - - - var fakeSeries = Builder.CreateNew().Build(); - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - - - var episode = Mocker.Resolve().GetEpisode(1); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series, e => e.EpisodeFile).EqualTo(fakeEpisodes.First()); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - } - - [Test] - public void GetEpisodes_by_season_episode_exists() - { - - - var fakeSeries = Builder.CreateNew() - .With(s => s.Id = 1) - .Build(); - var fakeEpisodes = Builder.CreateNew() - .With(e => e.SeriesId = 1) - .With(e => e.EpisodeNumber = 1) - .And(e => e.SeasonNumber = 2).Build(); - - Db.Insert(fakeSeries); - Db.Insert(fakeEpisodes); - - - var episode = Mocker.Resolve().GetEpisode(fakeSeries.Id, 2, 1); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series).EqualTo(fakeEpisodes); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - } - - [Test] - public void GetEpisodes_by_season_episode_doesnt_exists() - { - - - - var episode = Mocker.Resolve().GetEpisode(1, 1, 1); - - - episode.Should().BeNull(); - } - - [Test] - public void GetEpisode_with_EpisodeFile() - { - - - var fakeSeries = Builder.CreateNew().Build(); - var fakeFile = Builder.CreateNew().With(f => f.Id).With(c => c.Quality = Quality.SDTV).Build(); - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).TheFirst(1).With(e => e.EpisodeFile = new EpisodeFile { Id = 1 }).With(e => e.EpisodeFile = fakeFile).Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - Db.Insert(fakeFile); - - - var episode = Mocker.Resolve().GetEpisode(1); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series, e => e.EpisodeFile).EqualTo(fakeEpisodes.First()); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - episode.EpisodeFile.Should().NotBeNull(); - } - - [Test] - [ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "Sequence contains no elements")] - public void GetEpisodes_invalid_series() - { - - - Mocker.Resolve(); - - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).Build(); - - - Db.InsertMany(fakeEpisodes); - - - - Mocker.Resolve().GetEpisode(1); - } - - [Test] - public void GetEpisodesBySeason_success() - { - - - var fakeSeries = Builder.CreateNew() - .With(s => s.Id = 12) - .Build(); - - var episodes = Builder.CreateListOfSize(10) - .All().With(c => c.SeriesId = 12).And(c => c.SeasonNumber = 2) - .TheFirst(5).With(c => c.SeasonNumber = 1) - .Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(episodes); - - - var seasonEposodes = Mocker.Resolve().GetEpisodesBySeason(12, 2); - - - Db.Fetch().Should().HaveCount(10); - seasonEposodes.Should().HaveCount(5); - } - - [Test] - public void RefreshEpisodeInfo_emptyRepo() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 10; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(episodeCount). - All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var actualCount = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList().Count; - Mocker.GetMock().VerifyAll(); - actualCount.Should().Be(episodeCount); - } - - [Test] - public void RefreshEpisodeInfo_should_set_older_than_1900_to_null() - { - //Arrange - const int seriesId = 71663; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(10). - All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")).And(e => e.FirstAired = DateTime.Now) - .TheFirst(7).With(e => e.FirstAired = new DateTime(1800, 1, 1)) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var storedEpisodes = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - storedEpisodes.Should().HaveCount(10); - storedEpisodes.Where(e => e.AirDate == null).Should().HaveCount(7); - storedEpisodes.Where(e => e.AirDate != null).Should().HaveCount(3); - } - - [Test] - public void RefreshEpisodeInfo_should_set_older_than_1900_to_null_for_existing_episodes() - { - //Arrange - const int seriesId = 71663; - - var fakeEpisode = Builder.CreateNew() - .With(e => e.TvDbEpisodeId = 12345) - .With(e => e.AirDate = DateTime.Today) - .Build(); - - var fakeTvDbEpisodes = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(1) - .All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")).And(e => e.FirstAired = DateTime.Now) - .TheFirst(1).With(e => e.FirstAired = new DateTime(1800, 1, 1)) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - Db.Insert(fakeSeries); - Db.Insert(fakeEpisode); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeTvDbEpisodes); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var storedEpisodes = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - storedEpisodes.Should().HaveCount(1); - storedEpisodes.Where(e => e.AirDate == null).Should().HaveCount(1); - } - - [Test] - public void RefreshEpisodeInfo_ignore_episode_zero() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 10; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(episodeCount). - All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")) - .TheFirst(1) - .With(e => e.EpisodeNumber = 0) - .With(e => e.SeasonNumber = 15) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - Mocker.GetMock().VerifyAll(); - result.Should().HaveCount(episodeCount); - result.Where(e => e.EpisodeNumber == 0 && e.SeasonNumber == 15).Single().Ignored.Should().BeTrue(); - } - - [Test] - public void RefreshEpisodeInfo_should_skip_future_episodes_with_no_title() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 10; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = new List(Builder.CreateListOfSize(episodeCount). - All() - .With(a => c.FirstAired = DateTime.Now.AddDays(-2)) - .With(e => e.EpisodeName = "Something") - .TheFirst(3) - .With(e => e.EpisodeName = "") - .With(e => e.FirstAired = DateTime.Now.AddDays(10)) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - result.Should().HaveCount(episodeCount - 3); - result.Should().OnlyContain(c => !string.IsNullOrWhiteSpace(c.Title) || c.AirDate < DateTime.Now); - } - - [Test] - public void RefreshEpisodeInfo_should_skip_older_than_1900_year_episodes_with_no_title() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 10; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = new List(Builder.CreateListOfSize(episodeCount). - All() - .With(a => c.FirstAired = DateTime.Now.AddDays(-2)) - .With(e => e.EpisodeName = "Something") - .TheFirst(3) - .With(e => e.EpisodeName = "") - .With(e => e.FirstAired = new DateTime(1889, 1, 1)) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - result.Should().HaveCount(episodeCount - 3); - result.Should().OnlyContain(c => !string.IsNullOrWhiteSpace(c.Title) || c.AirDate < DateTime.Now); - } - - [Test] - public void RefreshEpisodeInfo_should_add_future_episodes_with_title() - { - const int seriesId = 71663; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = new List(Builder.CreateListOfSize(10). - All() - .With(a => a.FirstAired = DateTime.Now.AddDays(10)) - .With(e => e.EpisodeName = "Something") - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - result.Should().HaveSameCount(fakeEpisodes.Episodes); - } - - [Test] - public void RefreshEpisodeInfo_should_add_old_episodes_with_no_title() - { - const int seriesId = 71663; - - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = new List(Builder.CreateListOfSize(10). - All() - .With(a => a.FirstAired = DateTime.Now.AddDays(-10)) - .With(e => e.EpisodeName = string.Empty) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - result.Should().HaveSameCount(fakeEpisodes.Episodes); - } - - [Test] - public void RefreshEpisodeInfo_ignore_season_zero() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 10; - - var fakeEpisodes = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(episodeCount). - All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")) - .With(e => e.SeasonNumber = 0) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeEpisodes); - - Mocker.GetMock() - .Setup(s => s.IsIgnored(seriesId, 0)) - .Returns(true); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - Mocker.GetMock().VerifyAll(); - result.Should().HaveCount(episodeCount); - result.Where(e => e.Ignored).Should().HaveCount(episodeCount); - } - - [Test] - public void new_episodes_only_calls_Insert() - { - const int seriesId = 71663; - var tvdbSeries = Builder.CreateNew() - .With(c => c.Episodes = new List(Builder.CreateListOfSize(5).Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - var currentEpisodes = new List(); - - Mocker.GetMock(MockBehavior.Strict) - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(currentEpisodes); - - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - Mocker.GetMock().Verify(c => c.InsertMany(It.Is>(l => l.Count() == 5)), Times.Once()); - Mocker.GetMock().Verify(c => c.Update(It.IsAny>()), Times.Never()); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void existing_episodes_only_calls_Update() - { - const int seriesId = 71663; - var tvdbSeries = Builder.CreateNew() - .With(c => c.Episodes = new List(Builder.CreateListOfSize(5).Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - var currentEpisodes = new List(); - foreach (var tvDbEpisode in tvdbSeries.Episodes) - { - currentEpisodes.Add(new Episode { TvDbEpisodeId = tvDbEpisode.Id, Series = fakeSeries }); - } - - Mocker.GetMock(MockBehavior.Strict) - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(currentEpisodes); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - Mocker.GetMock().Verify(c => c.InsertMany(It.Is>(l => l.Count() == 0)), Times.Once()); - Mocker.GetMock().Verify(c => c.UpdateMany(It.Is>(l => l.Count() == 5)), Times.Once()); - Mocker.VerifyAllMocks(); - } - - [Test] - public void should_try_to_get_existing_episode_using_tvdbid_first() - { - const int seriesId = 71663; - var fakeTvDbResult = Builder.CreateNew() - .With(c => c.Id = seriesId) - .With(c => c.Episodes = new List( - Builder.CreateListOfSize(1) - .All().With(g => g.Id = 99) - .Build()) - ) - .Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - var fakeEpisodeList = new List { new Episode { TvDbEpisodeId = 99, SeasonNumber = 10, EpisodeNumber = 10, Series = fakeSeries } }; - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(fakeEpisodeList); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(fakeTvDbResult); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - Mocker.VerifyAllMocks(); - Mocker.GetMock().Verify(c => c.UpdateMany(fakeEpisodeList), Times.Once()); - } - - [Test] - public void should_try_to_get_existing_episode_using_tvdbid_first_then_season_episode() - { - const int seriesId = 71663; - var tvdbSeries = Builder.CreateNew() - .With(c => c.Id = seriesId) - .With(c => c.Episodes = new List{ - Builder.CreateNew() - .With(g => g.Id = 99) - .With(g => g.SeasonNumber = 4) - .With(g => g.EpisodeNumber = 15) - .With(g=>g.SeriesId = seriesId) - .Build() - }) - .Build(); - - var localEpisode = Builder.CreateNew() - .With(c => c.SeriesId = seriesId) - .With(c => c.SeasonNumber = 4) - .With(c => c.EpisodeNumber = 15) - .Build(); - - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - Mocker.GetMock(MockBehavior.Strict) - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(new List { localEpisode }); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - Mocker.VerifyAllMocks(); - Mocker.GetMock().Verify(c => c.UpdateMany(new List { localEpisode }), Times.Once()); - } - - [Test] - public void existing_episodes_keep_their_episodeId_file_id() - { - const int seriesId = 71663; - var tvdbSeries = Builder.CreateNew() - .With(c => c.Episodes = new List(Builder.CreateListOfSize(5).Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - var currentEpisodes = new List(); - foreach (var tvDbEpisode in tvdbSeries.Episodes) - { - currentEpisodes.Add(new Episode - { - TvDbEpisodeId = tvDbEpisode.Id, - Id = 99, - EpisodeFile = new EpisodeFile { Id = 69 }, - Ignored = true, - Series = fakeSeries, - EpisodeNumber = tvDbEpisode.EpisodeNumber, - SeasonNumber = tvDbEpisode.SeasonNumber - }); - } - - Mocker.GetMock(MockBehavior.Strict) - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - var updatedEpisodes = new List(); - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(currentEpisodes); - - Mocker.GetMock() - .Setup(c => c.UpdateMany(It.IsAny>())) - .Callback>(ep => updatedEpisodes = ep.ToList()); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - updatedEpisodes.Should().HaveSameCount(tvdbSeries.Episodes); - updatedEpisodes.Should().OnlyContain(c => c.Id == 99); - updatedEpisodes.Should().OnlyContain(c => c.EpisodeFileId == 69); - updatedEpisodes.Should().OnlyContain(c => c.Ignored == true); - } - - [Test] - public void existing_episodes_remote_their_episodeId_file_id_when_episode_number_doesnt_match_tvdbid() - { - const int seriesId = 71663; - var tvdbSeries = Builder.CreateNew() - .With(c => c.Episodes = new List(Builder.CreateListOfSize(5).Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - var currentEpisodes = new List(); - foreach (var tvDbEpisode in tvdbSeries.Episodes) - { - currentEpisodes.Add(new Episode - { - TvDbEpisodeId = tvDbEpisode.Id, - Id = 99, - EpisodeFile = new EpisodeFile { Id = 69 }, - Ignored = true, - Series = fakeSeries, - EpisodeNumber = tvDbEpisode.EpisodeNumber + 1, - SeasonNumber = tvDbEpisode.SeasonNumber - }); - } - - Mocker.GetMock(MockBehavior.Strict) - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - var updatedEpisodes = new List(); - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(currentEpisodes); - - Mocker.GetMock() - .Setup(c => c.UpdateMany(It.IsAny>())) - .Callback>(ep => updatedEpisodes = ep.ToList()); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - updatedEpisodes.Should().OnlyContain(c => c.EpisodeFileId == 0); - } - - [Test] - public void existing_episodes_remote_their_episodeId_file_id_when_season_number_doesnt_match_tvdbid() - { - const int seriesId = 71663; - var tvdbSeries = Builder.CreateNew() - .With(c => c.Episodes = new List(Builder.CreateListOfSize(5).Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - var currentEpisodes = new List(); - foreach (var tvDbEpisode in tvdbSeries.Episodes) - { - currentEpisodes.Add(new Episode - { - TvDbEpisodeId = tvDbEpisode.Id, - Id = 99, - EpisodeFile = new EpisodeFile { Id = 69 }, - Ignored = true, - Series = fakeSeries, - EpisodeNumber = tvDbEpisode.EpisodeNumber, - SeasonNumber = tvDbEpisode.SeasonNumber + 1 - }); - } - - Mocker.GetMock(MockBehavior.Strict) - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - var updatedEpisodes = new List(); - - Mocker.GetMock() - .Setup(d => d.Fetch(It.IsAny(), It.IsAny())) - .Returns(currentEpisodes); - - Mocker.GetMock() - .Setup(c => c.UpdateMany(It.IsAny>())) - .Callback>(ep => updatedEpisodes = ep.ToList()); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - updatedEpisodes.Should().OnlyContain(c => c.EpisodeFileId == 0); - } - - [Test] - public void RefreshEpisodeInfo_should_ignore_new_episode_for_ignored_season() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 2; - - var fakeEpisode = Builder.CreateNew() - .With(e => e.SeasonNumber = 5) - .With(e => e.EpisodeNumber = 1) - .With(e => e.TvDbEpisodeId = 11) - .With(e => e.SeriesId = seriesId) - .With(e => e.Ignored = true) - .Build(); - - var tvdbSeries = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(episodeCount). - All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")) - .With(e => e.SeasonNumber = 5) - .TheFirst(1) - .With(e => e.EpisodeNumber = 1) - .With(e => e.Id = 11) - .TheNext(1) - .With(e => e.EpisodeNumber = 2) - .With(e => e.Id = 22) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - Db.Insert(fakeEpisode); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - Mocker.GetMock() - .Setup(s => s.IsIgnored(seriesId, It.IsAny())) - .Returns(true); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - Mocker.GetMock().VerifyAll(); - result.Should().HaveCount(episodeCount); - result.Where(e => e.Ignored).Should().HaveCount(episodeCount); - } - - [Test] - [Explicit] - public void Add_daily_show_episodes() - { - - Mocker.Resolve(); - - Mocker.GetMock() - .Setup(e => e.DefaultQualityProfile).Returns(1); - - Db.Insert(Builder.CreateNew().Build()); - - var seriesRepo = Mocker.Resolve(); - - const int tvDbSeriesId = 71256; - - var seriesProvider = Mocker.Resolve(); - - seriesProvider.AddSeries("Test Series", "c:\\test\\", tvDbSeriesId, 1, null); - - - - var episodeProvider = Mocker.Resolve(); - episodeProvider.RefreshEpisodeInfo(seriesRepo.Get(tvDbSeriesId)); - - - var episodes = episodeProvider.GetEpisodeBySeries(tvDbSeriesId); - episodes.Should().NotBeEmpty(); - } - - [Test] - public void GetEpisode_by_Season_Episode_none_existing() - { - - - - var episode = Mocker.Resolve().GetEpisode(1, 1, 1); - - - episode.Should().BeNull(); - } - - [Test] - public void GetEpisode_by_Season_Episode_with_EpisodeFile() - { - - - var fakeSeries = Builder.CreateNew().Build(); - var fakeFile = Builder.CreateNew().With(f => f.Id).With(c => c.Quality = Quality.SDTV).Build(); - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).TheFirst(1).With(c => c.EpisodeFile = new EpisodeFile { Id = 1 }).With(e => e.EpisodeFile = fakeFile).Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - Db.Insert(fakeFile); - - - var episode = Mocker.Resolve().GetEpisode(1, 1, 1); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series, e => e.EpisodeFile).EqualTo(fakeEpisodes.First()); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - episode.EpisodeFile.Should().NotBeNull(); - } - - [Test] - public void GetEpisode_by_Season_Episode_without_EpisodeFile() - { - - - var fakeSeries = Builder.CreateNew().Build(); - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).TheFirst(1).Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - - - var episode = Mocker.Resolve().GetEpisode(1, 1, 1); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series).EqualTo(fakeEpisodes.First()); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - episode.EpisodeFile.Should().BeNull(); - } - - [Test] - public void GetEpisode_by_AirDate_with_EpisodeFile() - { - - - var fakeSeries = Builder.CreateNew().Build(); - var fakeFile = Builder.CreateNew().With(f => f.Id).With(c => c.Quality = Quality.SDTV).Build(); - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).TheFirst(1).With(e => e.EpisodeFile = new EpisodeFile { Id = 1 }).With(e => e.EpisodeFile = fakeFile).Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - Db.Insert(fakeFile); - - - var episode = Mocker.Resolve().GetEpisode(1, fakeEpisodes[0].AirDate.Value); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series, e => e.EpisodeFile).EqualTo(fakeEpisodes.First()); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - episode.EpisodeFile.Should().NotBeNull(); - } - - [Test] - public void GetEpisode_by_AirDate_without_EpisodeFile() - { - - - var fakeSeries = Builder.CreateNew().Build(); - var fakeEpisodes = Builder.CreateListOfSize(5) - .All().With(e => e.SeriesId = 1).TheFirst(1).With(e => e.EpisodeFile = new EpisodeFile { Id = 1 }).Build(); - - Db.InsertMany(fakeEpisodes); - Db.Insert(fakeSeries); - - - var episode = Mocker.Resolve().GetEpisode(1, fakeEpisodes[0].AirDate.Value); - - - episode.ShouldHave().AllPropertiesBut(e => e.Series).EqualTo(fakeEpisodes.First()); - episode.Series.ShouldHave().AllPropertiesBut(s => s.EpisodeCount, s => s.EpisodeFileCount, s => s.SeasonCount, s => s.NextAiring).EqualTo(fakeSeries); - episode.EpisodeFile.Should().BeNull(); - } - - [Test] - public void MarkEpisodeAsFetched() - { - - var fakeEpisodes = Builder.CreateListOfSize(2) - .All().With(e => e.GrabDate = null) - .Build(); - - var parseResult = new EpisodeParseResult() { Episodes = fakeEpisodes }; - - Mocker.Resolve().Handle(new EpisodeGrabbedEvent(parseResult)); - - Mocker.GetMock().Verify(c=>c.Update(fakeEpisodes[0]),Times.Once()); - Mocker.GetMock().Verify(c=>c.Update(fakeEpisodes[1]),Times.Once()); - } - - [Test] - public void AddEpisode_episode_is_ignored_when_full_season_is_ignored() - { - - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.Ignored = true) - .Build().ToList(); - - episodes.ForEach(c => Db.Insert(c)); - - var newEpisode = Builder.CreateNew() - .With(e => e.SeriesId = 10) - .With(e => e.SeasonNumber = 1) - .With(e => e.EpisodeNumber = 8) - .With(e => e.SeasonNumber = 1) - .With(e => e.Ignored = false) - .Build(); - - Mocker.GetMock() - .Setup(s => s.IsIgnored(newEpisode.SeriesId, newEpisode.SeasonNumber)) - .Returns(true); - - - Mocker.Resolve().AddEpisode(newEpisode); - - - var episodesInDb = Db.Fetch(@"SELECT * FROM Episodes"); - - episodesInDb.Should().HaveCount(5); - episodesInDb.Should().OnlyContain(e => e.Ignored); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void AddEpisode_episode_is_not_ignored_when_full_season_is_not_ignored() - { - - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.Ignored = false) - .Build().ToList(); - - episodes.ForEach(c => Db.Insert(c)); - - var newEpisode = Builder.CreateNew() - .With(e => e.SeriesId = 10) - .With(e => e.SeasonNumber = 1) - .With(e => e.EpisodeNumber = 8) - .With(e => e.SeasonNumber = 1) - .With(e => e.Ignored = false) - .Build(); - - - Mocker.Resolve().AddEpisode(newEpisode); - - - var episodesInDb = Db.Fetch(@"SELECT * FROM Episodes"); - - episodesInDb.Should().HaveCount(5); - episodesInDb.Should().OnlyContain(e => e.Ignored == false); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void AddEpisode_episode_is_not_ignored_when_not_full_season_is_not_ignored() - { - - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .And(c => c.SeasonNumber = 1) - .And(c => c.Ignored = true) - .TheFirst(2) - .With(c => c.Ignored = false) - .Build().ToList(); - - episodes.ForEach(c => Db.Insert(c)); - - var newEpisode = Builder.CreateNew() - .With(e => e.SeriesId = 10) - .With(e => e.SeasonNumber = 1) - .With(e => e.EpisodeNumber = 8) - .With(e => e.SeasonNumber = 1) - .With(e => e.Ignored = false) - .Build(); - - - Mocker.Resolve().AddEpisode(newEpisode); - - - var episodesInDb = Db.Fetch(@"SELECT * FROM Episodes"); - - episodesInDb.Should().HaveCount(5); - episodesInDb.Where(e => e.EpisodeNumber == 8 && !e.Ignored).Should().HaveCount(1); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void IgnoreEpisode_Ignore() - { - - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.Ignored = false) - .Build().ToList(); - - episodes.ForEach(c => Db.Insert(c)); - - - Mocker.Resolve().SetEpisodeIgnore(1, true); - - - var episodesInDb = Db.Fetch(@"SELECT * FROM Episodes"); - - episodesInDb.Should().HaveCount(4); - episodesInDb.Where(e => e.Ignored).Should().HaveCount(1); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void IgnoreEpisode_RemoveIgnore() - { - - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.Ignored = true) - .Build().ToList(); - - episodes.ForEach(c => Db.Insert(c)); - - - Mocker.Resolve().SetEpisodeIgnore(1, false); - - - var episodesInDb = Db.Fetch(@"SELECT * FROM Episodes"); - - episodesInDb.Should().HaveCount(4); - episodesInDb.Where(e => !e.Ignored).Should().HaveCount(1); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void EpisodesWithoutFiles_no_specials() - { - - - var series = Builder.CreateNew() - .With(s => s.Id = 10) - .Build(); - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.AirDate = DateTime.Today.AddDays(-4)) - .With(c => c.Ignored = true) - .TheFirst(2) - .Section(1, 2) - .With(c => c.Ignored = false) - .Build().ToList(); - - var specials = Builder.CreateListOfSize(2) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 0) - .With(c => c.AirDate = DateTime.Today.AddDays(-4)) - .With(c => c.Ignored = false) - .TheFirst(1).With(c => c.Ignored = true) - .Build().ToList(); - - Db.Insert(series); - Db.InsertMany(episodes); - Db.InsertMany(specials); - - - var missingFiles = Mocker.Resolve().EpisodesWithoutFiles(false); - - - missingFiles.Should().HaveCount(1); - missingFiles.Where(e => e.EpisodeFileId == 0).Should().HaveCount(1); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void EpisodesWithoutFiles_with_specials() - { - - - var series = Builder.CreateNew() - .With(s => s.Id = 10) - .Build(); - - var episodes = Builder.CreateListOfSize(4) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.AirDate = DateTime.Today.AddDays(-4)) - .With(c => c.Ignored = true) - .TheFirst(2) - .Section(1, 2) - .With(c => c.Ignored = false) - .Build().ToList(); - - var specials = Builder.CreateListOfSize(2) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 0) - .With(c => c.AirDate = DateTime.Today.AddDays(-4)) - .With(c => c.Ignored = false) - .TheFirst(1) - .With(c => c.Ignored = true) - .Build().ToList(); - - Db.Insert(series); - Db.InsertMany(episodes); - Db.InsertMany(specials); - - - var missingFiles = Mocker.Resolve().EpisodesWithoutFiles(true); - - - missingFiles.Should().HaveCount(2); - missingFiles.Where(e => e.EpisodeFileId == 0).Should().HaveCount(2); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void EpisodesWithFiles_success() - { - - - var series = Builder.CreateNew() - .With(s => s.Id = 10) - .Build(); - - var episodeFile = Builder.CreateNew() - .With(c => c.Id = 1) - .With(c => c.Quality = Quality.SDTV) - .Build(); - - var episodes = Builder.CreateListOfSize(2) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.AirDate = DateTime.Today.AddDays(-4)) - .With(c => c.Ignored = true) - .With(c => c.EpisodeFile = episodeFile) - .Build().ToList(); - - Db.Insert(series); - Db.Insert(episodeFile); - Db.InsertMany(episodes); - - - var withFiles = Mocker.Resolve().EpisodesWithFiles(); - - - withFiles.Should().HaveCount(2); - withFiles.Where(e => e.EpisodeFileId == 0).Should().HaveCount(0); - withFiles.Where(e => e.EpisodeFile == null).Should().HaveCount(0); - - foreach (var withFile in withFiles) - { - withFile.EpisodeFile.Should().NotBeNull(); - withFile.Series.Title.Should().NotBeNullOrEmpty(); - } - - Mocker.VerifyAllMocks(); - } - - [Test] - public void EpisodesWithFiles_no_files() - { - - - var series = Builder.CreateNew() - .With(s => s.Id = 10) - .Build(); - - var episodes = Builder.CreateListOfSize(2) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.AirDate = DateTime.Today.AddDays(-4)) - .With(c => c.Ignored = true) - .Build().ToList(); - - Db.Insert(series); - Db.InsertMany(episodes); - - - var withFiles = Mocker.Resolve().EpisodesWithFiles(); - - - withFiles.Should().HaveCount(0); - - Mocker.VerifyAllMocks(); - } - - [Test] - public void GetEpisodesByFileId_multi_episodes() - { - - - var series = Builder.CreateNew() - .With(s => s.Id = 10) - .Build(); - - var fakeEpisodes = Builder.CreateListOfSize(2) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.EpisodeFile = new EpisodeFile { Id = 12345 }) - .Build(); - - Db.Insert(series); - Db.InsertMany(fakeEpisodes); - - - var episodes = Mocker.Resolve().GetEpisodesByFileId(12345); - - - episodes.Should().HaveCount(2); - Mocker.VerifyAllMocks(); - } - - [Test] - public void GetEpisodesByFileId_single_episode() - { - - - var series = Builder.CreateNew() - .With(s => s.Id = 10) - .Build(); - - var fakeEpisode = Builder.CreateNew() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .With(c => c.EpisodeFile = new EpisodeFile { Id = 12345 }) - .Build(); - - Db.Insert(series); - Db.Insert(fakeEpisode); - - - var episodes = Mocker.Resolve().GetEpisodesByFileId(12345); - - - episodes.Should().HaveCount(1); - episodes.First().ShouldHave().AllPropertiesBut(e => e.Series).EqualTo(fakeEpisode); - Mocker.VerifyAllMocks(); - } - - [Test] - public void IsFirstOrLastEpisodeInSeason_false() - { - - - var fakeEpisodes = Builder.CreateListOfSize(10) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .Build(); - - Db.InsertMany(fakeEpisodes); - - - var result = Mocker.Resolve().IsFirstOrLastEpisodeOfSeason(10, 1, 5); - - - result.Should().BeFalse(); - } - - [Test] - public void IsFirstOrLastEpisodeInSeason_true_first() - { - - - var fakeEpisodes = Builder.CreateListOfSize(10) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .Build(); - - Db.InsertMany(fakeEpisodes); - - - var result = Mocker.Resolve().IsFirstOrLastEpisodeOfSeason(10, 1, 1); - - - result.Should().BeFalse(); - } - - [Test] - public void IsFirstOrLastEpisodeInSeason_true_last() - { - - - var fakeEpisodes = Builder.CreateListOfSize(10) - .All() - .With(c => c.SeriesId = 10) - .With(c => c.SeasonNumber = 1) - .Build(); - - Db.InsertMany(fakeEpisodes); - - - var result = Mocker.Resolve().IsFirstOrLastEpisodeOfSeason(10, 1, 10); - - - result.Should().BeFalse(); - } - - [TestCase("The Office (US) - S01E01 - Episode Title", PostDownloadStatusType.Unpacking, 1)] - [TestCase("The Office (US) - S01E01 - Episode Title", PostDownloadStatusType.Failed, 1)] - [TestCase("The Office (US) - S01E01E02 - Episode Title", PostDownloadStatusType.Unpacking, 2)] - [TestCase("The Office (US) - S01E01E02 - Episode Title", PostDownloadStatusType.Failed, 2)] - [TestCase("The Office (US) - Season 01 - Episode Title", PostDownloadStatusType.Unpacking, 10)] - [TestCase("The Office (US) - Season 01 - Episode Title", PostDownloadStatusType.Failed, 10)] - public void SetPostDownloadStatus(string folderName, PostDownloadStatusType postDownloadStatus, int episodeCount) - { - - - var fakeSeries = Builder.CreateNew() - .With(s => s.Id = 12345) - .With(s => s.CleanTitle = "officeus") - .Build(); - - var fakeEpisodes = Builder.CreateListOfSize(episodeCount) - .All() - .With(c => c.SeriesId = 12345) - .With(c => c.SeasonNumber = 1) - .With(c => c.PostDownloadStatus = PostDownloadStatusType.Unknown) - .Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - - Mocker.GetMock().Setup(s => s.GetByTitle("officeus")).Returns(fakeSeries); - - - Mocker.Resolve().SetPostDownloadStatus(fakeEpisodes.Select(e => e.Id).ToList(), postDownloadStatus); - - - var result = Db.Fetch(); - result.Where(e => e.PostDownloadStatus == postDownloadStatus).Count().Should().Be(episodeCount); - } - - [Test] - public void SetPostDownloadStatus_Invalid_EpisodeId() - { - - - var postDownloadStatus = PostDownloadStatusType.Failed; - - var fakeSeries = Builder.CreateNew() - .With(s => s.Id = 12345) - .With(s => s.CleanTitle = "officeus") - .Build(); - - var fakeEpisodes = Builder.CreateListOfSize(1) - .All() - .With(c => c.SeriesId = 12345) - .With(c => c.SeasonNumber = 1) - .With(c => c.PostDownloadStatus = PostDownloadStatusType.Unknown) - .Build(); - - Db.Insert(fakeSeries); - Db.InsertMany(fakeEpisodes); - - Mocker.GetMock().Setup(s => s.GetByTitle("officeus")).Returns(fakeSeries); - - - Mocker.Resolve().SetPostDownloadStatus(new List { 300 }, postDownloadStatus); - - - var result = Db.Fetch(); - result.Where(e => e.PostDownloadStatus == postDownloadStatus).Count().Should().Be(0); - } - - [Test] - [ExpectedException(typeof(ArgumentException))] - public void SetPostDownloadStatus_should_throw_if_episode_list_is_empty() - { - Mocker.Resolve().SetPostDownloadStatus(new List(), PostDownloadStatusType.Failed); - } - - [Test] - public void RefreshEpisodeInfo_should_ignore_episode_zero_except_if_season_one() - { - //Arrange - const int seriesId = 71663; - const int episodeCount = 5; - - var tvdbSeries = Builder.CreateNew().With( - c => c.Episodes = - new List(Builder.CreateListOfSize(episodeCount). - All() - .With(l => l.Language = new TvdbLanguage(0, "eng", "a")) - .With(e => e.EpisodeNumber = 0) - .TheFirst(1) - .With(e => e.SeasonNumber = 1) - .TheNext(1) - .With(e => e.SeasonNumber = 2) - .TheNext(1) - .With(e => e.SeasonNumber = 3) - .TheNext(1) - .With(e => e.SeasonNumber = 4) - .TheNext(1) - .With(e => e.SeasonNumber = 5) - .Build()) - ).With(c => c.Id = seriesId).Build(); - - var fakeSeries = Builder.CreateNew().With(c => c.Id = seriesId).Build(); - - - - Db.Insert(fakeSeries); - - Mocker.GetMock() - .Setup(c => c.GetSeries(seriesId, true, false)) - .Returns(tvdbSeries); - - - Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); - - - var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); - result.Should().HaveCount(episodeCount); - result.Where(e => e.Ignored).Should().HaveCount(episodeCount - 1); - result.Single(e => e.SeasonNumber == 1).Ignored.Should().BeFalse(); - } - - - } -} -*/ diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs index 5a3f2e1fc..edd9c8369 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -4,6 +4,7 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Core.Qualities; @@ -22,15 +23,15 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [SetUp] public void Setup() { - var qualityProfile = new QualityProfile + var profile = new Profile { Id = 1, Cutoff = Quality.WEBDL480p, - Items = new List + Items = new List { - new QualityProfileItem { Allowed = true, Quality = Quality.SDTV }, - new QualityProfileItem { Allowed = true, Quality = Quality.WEBDL480p }, - new QualityProfileItem { Allowed = true, Quality = Quality.RAWHD } + new ProfileQualityItem { Allowed = true, Quality = Quality.SDTV }, + new ProfileQualityItem { Allowed = true, Quality = Quality.WEBDL480p }, + new ProfileQualityItem { Allowed = true, Quality = Quality.RAWHD } } }; @@ -39,7 +40,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .With(s => s.Runtime = 30) .With(s => s.Monitored = true) .With(s => s.TitleSlug = "Title3") - .With(s => s.Id = qualityProfile.Id) + .With(s => s.Id = profile.Id) .BuildNew(); _unmonitoredSeries = Builder.CreateNew() @@ -47,7 +48,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .With(s => s.Runtime = 30) .With(s => s.Monitored = false) .With(s => s.TitleSlug = "Title2") - .With(s => s.Id = qualityProfile.Id) + .With(s => s.Id = profile.Id) .BuildNew(); _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id; @@ -63,7 +64,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests _qualitiesBelowCutoff = new List { - new QualitiesBelowCutoff(qualityProfile.Id, new[] {Quality.SDTV.Id}) + new QualitiesBelowCutoff(profile.Id, new[] {Quality.SDTV.Id}) }; var qualityMet = new EpisodeFile { Path = "a", Quality = new QualityModel { Quality = Quality.WEBDL480p } }; diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs similarity index 74% rename from src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs rename to src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs index 0685ccc0f..bbd18e7e1 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests [Test] public void should_lazyload_quality_profile() { - var profile = new QualityProfile + var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), @@ -24,15 +24,15 @@ namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests }; - Mocker.Resolve().Insert(profile); + Mocker.Resolve().Insert(profile); var series = Builder.CreateNew().BuildNew(); - series.QualityProfileId = profile.Id; + series.ProfileId = profile.Id; Subject.Insert(series); - StoredModel.QualityProfile.Should().NotBeNull(); + StoredModel.Profile.Should().NotBeNull(); } diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs index 86cbe7320..f0585131d 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests { _series = Builder.CreateListOfSize(5) .All() - .With(s => s.QualityProfileId = 1) + .With(s => s.ProfileId = 1) .With(s => s.Monitored) .With(s => s.SeasonFolder) .With(s => s.Path = @"C:\Test\name".AsOsAgnostic()) diff --git a/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs index 2e40accd4..90be4a920 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs @@ -17,7 +17,6 @@ namespace NzbDrone.Core.Datastore.Extensions query: (db, parent) => db.Query().SingleOrDefault(c => c.Id == childIdSelector(parent)), condition: parent => childIdSelector(parent) > 0 ); - } public static RelationshipBuilder Relationship(this ColumnMapBuilder mapBuilder) @@ -25,16 +24,12 @@ namespace NzbDrone.Core.Datastore.Extensions return mapBuilder.Relationships.AutoMapComplexTypeProperties(); } - - public static RelationshipBuilder HasMany(this RelationshipBuilder relationshipBuilder, Expression>> portalExpression, Func childIdSelector) where TParent : ModelBase where TChild : ModelBase { return relationshipBuilder.For(portalExpression.GetMemberName()) .LazyLoad((db, parent) => db.Query().Where(c => c.Id == childIdSelector(parent)).ToList()); - - } private static string GetMemberName(this Expression> member) diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs index ea6f38fdd..78ffb967d 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -3,6 +3,7 @@ using NzbDrone.Core.Datastore.Migration.Framework; using System.Data; using System.Linq; using NzbDrone.Common.Serializer; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using System.Collections.Generic; using NzbDrone.Core.Datastore.Converters; @@ -41,7 +42,7 @@ namespace NzbDrone.Core.Datastore.Migration var allowed = Json.Deserialize>(allowedJson); - var items = Quality.DefaultQualityDefinitions.OrderBy(v => v.Weight).Select(v => new QualityProfileItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }).ToList(); + var items = Quality.DefaultQualityDefinitions.OrderBy(v => v.Weight).Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }).ToList(); var allowedNewJson = qualityProfileItemConverter.ToDB(items); diff --git a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs new file mode 100644 index 000000000..e665c14a4 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs @@ -0,0 +1,31 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(54)] + public class rename_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Rename.Table("QualityProfiles").To("Profiles"); + + Alter.Table("Profiles").AddColumn("Language").AsInt32().Nullable(); + Alter.Table("Profiles").AddColumn("GrabDelay").AsInt32().Nullable(); + Alter.Table("Profiles").AddColumn("GrabDelayMode").AsInt32().Nullable(); + Execute.Sql("UPDATE Profiles SET Language = 1, GrabDelay = 0, GrabDelayMode = 0"); + + //Rename QualityProfileId in Series + Alter.Table("Series").AddColumn("ProfileId").AsInt32().Nullable(); + Execute.Sql("UPDATE Series SET ProfileId = QualityProfileId"); + + //Add HeldReleases + Create.TableForModel("PendingReleases") + .WithColumn("SeriesId").AsInt32() + .WithColumn("Title").AsString() + .WithColumn("Added").AsDateTime() + .WithColumn("ParsedEpisodeInfo").AsString() + .WithColumn("Release").AsString(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs b/src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs new file mode 100644 index 000000000..349c10ca1 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(55)] + public class drop_old_profile_columns : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + SqLiteAlter.DropColumns("Series", new[] { "QualityProfileId" }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteIndex.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteIndex.cs index cbf18dbd9..59129eaa0 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteIndex.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteIndex.cs @@ -33,7 +33,12 @@ namespace NzbDrone.Core.Datastore.Migration.Framework public string CreateSql(string tableName) { - return string.Format(@"CREATE UNIQUE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName); + if (Unique) + { + return String.Format(@"CREATE UNIQUE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName); + } + + return String.Format(@"CREATE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs index 3bb08dace..fabdefca9 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs @@ -147,14 +147,12 @@ namespace NzbDrone.Core.Datastore.Migration.Framework } } - public void DropTable(string tableName) { var dropCommand = BuildCommand("DROP TABLE {0};", tableName); dropCommand.ExecuteNonQuery(); } - public void RenameTable(string tableName, string newName) { var renameCommand = BuildCommand("ALTER TABLE {0} RENAME TO {1};", tableName, newName); @@ -184,7 +182,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework return Convert.ToInt32(countCommand.ExecuteScalar()); } - public SQLiteTransaction BeginTransaction() { return _connection.BeginTransaction(); @@ -197,7 +194,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework return command; } - public void ExecuteNonQuery(string command, params string[] args) { var sqLiteCommand = new SQLiteCommand(string.Format(command, args)) @@ -226,7 +222,5 @@ namespace NzbDrone.Core.Datastore.Migration.Framework } } - - } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index edb84e624..b0f14fc9d 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; @@ -17,6 +18,8 @@ using NzbDrone.Core.Metadata; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Notifications; using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.RootFolders; using NzbDrone.Core.SeriesStats; @@ -27,7 +30,6 @@ namespace NzbDrone.Core.Datastore { public static class TableMapping { - private static readonly FluentMappings Mapper = new FluentMappings(true); public static void Map() @@ -52,7 +54,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("Series") .Ignore(s => s.RootFolderPath) .Relationship() - .HasOne(s => s.QualityProfile, s => s.QualityProfileId); + .HasOne(s => s.Profile, s => s.ProfileId); Mapper.Entity().RegisterModel("Episodes") .Ignore(e => e.SeriesTitle) @@ -64,14 +66,16 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("EpisodeFiles") .Relationships.AutoMapICollectionOrComplexProperties(); - Mapper.Entity().RegisterModel("QualityProfiles"); + Mapper.Entity().RegisterModel("Profiles"); Mapper.Entity().RegisterModel("QualityDefinitions"); Mapper.Entity().RegisterModel("Logs"); Mapper.Entity().RegisterModel("NamingConfig"); Mapper.Entity().MapResultSet(); Mapper.Entity().RegisterModel("Blacklist"); - Mapper.Entity().RegisterModel("MetadataFiles"); + + Mapper.Entity().RegisterModel("PendingReleases") + .Ignore(e => e.RemoteEpisode); } private static void RegisterMappers() @@ -84,11 +88,13 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(Boolean), new BooleanIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new QualityIntConverter())); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter()); } private static void RegisterProviderSettingConverter() diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/DownloadDecision.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs similarity index 53% rename from src/NzbDrone.Core/DecisionEngine/Specifications/DownloadDecision.cs rename to src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs index 22616a735..dfbca6e84 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/DownloadDecision.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Parser.Model; -namespace NzbDrone.Core.DecisionEngine.Specifications +namespace NzbDrone.Core.DecisionEngine { public class DownloadDecision { public RemoteEpisode RemoteEpisode { get; private set; } - public IEnumerable Rejections { get; private set; } + public IEnumerable Rejections { get; private set; } public bool Approved { @@ -17,13 +17,28 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } - public DownloadDecision(RemoteEpisode episode, params string[] rejections) + public bool TemporarilyRejected + { + get + { + return Rejections.Any() && Rejections.All(r => r.Type == RejectionType.Temporary); + } + } + + public bool Rejected + { + get + { + return Rejections.Any() && Rejections.All(r => r.Type == RejectionType.Permanent); + } + } + + public DownloadDecision(RemoteEpisode episode, params Rejection[] rejections) { RemoteEpisode = episode; Rejections = rejections.ToList(); } - - + public override string ToString() { if (Approved) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 3babd682b..4dd786a90 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -4,7 +4,6 @@ using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Serializer; -using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Parser; @@ -17,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine { List GetRssDecision(List reports); List GetSearchDecision(List reports, SearchCriteriaBase searchCriteriaBase); + DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null); } public class DownloadDecisionMaker : IMakeDownloadDecision @@ -87,7 +87,7 @@ namespace NzbDrone.Core.DecisionEngine } else { - decision = new DownloadDecision(remoteEpisode, "Unknown Series"); + decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown Series")); } } } @@ -110,19 +110,19 @@ namespace NzbDrone.Core.DecisionEngine } } - private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) + public DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) { var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) - .Where(c => !string.IsNullOrWhiteSpace(c)); + .Where(c => c != null); return new DownloadDecision(remoteEpisode, reasons.ToArray()); } - private string EvaluateSpec(IRejectWithReason spec, RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) + private Rejection EvaluateSpec(IRejectWithReason spec, RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) { try { - if (string.IsNullOrWhiteSpace(spec.RejectionReason)) + if (spec.RejectionReason.IsNullOrWhiteSpace()) { throw new InvalidOperationException("[Need Rejection Text]"); } @@ -130,7 +130,7 @@ namespace NzbDrone.Core.DecisionEngine var generalSpecification = spec as IDecisionEngineSpecification; if (generalSpecification != null && !generalSpecification.IsSatisfiedBy(remoteEpisode, searchCriteriaBase)) { - return spec.RejectionReason; + return new Rejection(spec.RejectionReason, generalSpecification.Type); } } catch (Exception e) @@ -138,7 +138,7 @@ namespace NzbDrone.Core.DecisionEngine e.Data.Add("report", remoteEpisode.Release.ToJson()); e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); _logger.ErrorException("Couldn't evaluate decision on " + remoteEpisode.Release.Title, e); - return string.Format("{0}: {1}", spec.GetType().Name, e.Message); + return new Rejection(String.Format("{0}: {1}", spec.GetType().Name, e.Message)); } return null; diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index 9f01356b2..f9fa820b3 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.DecisionEngine return decisions .Where(c => c.RemoteEpisode.Series != null) .GroupBy(c => c.RemoteEpisode.Series.Id, (i, s) => s - .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.QualityProfile)) + .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.Profile)) .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count)) .ThenBy(c => c.RemoteEpisode.Release.Age)) diff --git a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs index 089925c7b..69cbdc894 100644 --- a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs @@ -1,3 +1,4 @@ +using System; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -5,6 +6,8 @@ namespace NzbDrone.Core.DecisionEngine { public interface IDecisionEngineSpecification : IRejectWithReason { - bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); + RejectionType Type { get; } + + Boolean IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/IRejectWithReason.cs b/src/NzbDrone.Core/DecisionEngine/IRejectWithReason.cs index 503071e02..ee9f843d3 100644 --- a/src/NzbDrone.Core/DecisionEngine/IRejectWithReason.cs +++ b/src/NzbDrone.Core/DecisionEngine/IRejectWithReason.cs @@ -4,4 +4,4 @@ namespace NzbDrone.Core.DecisionEngine { string RejectionReason { get; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs index a37f65ef3..0c83be0e3 100644 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs @@ -1,12 +1,13 @@ using NLog; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine { public interface IQualityUpgradableSpecification { - bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); - bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); + bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); + bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality); } @@ -19,7 +20,7 @@ namespace NzbDrone.Core.DecisionEngine _logger = logger; } - public bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) + public bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) { if (newQuality != null) { @@ -39,7 +40,7 @@ namespace NzbDrone.Core.DecisionEngine return true; } - public bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) + public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) { int compare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); diff --git a/src/NzbDrone.Core/DecisionEngine/Rejection.cs b/src/NzbDrone.Core/DecisionEngine/Rejection.cs new file mode 100644 index 000000000..315803b38 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Rejection.cs @@ -0,0 +1,21 @@ +using System; + +namespace NzbDrone.Core.DecisionEngine +{ + public class Rejection + { + public String Reason { get; set; } + public RejectionType Type { get; set; } + + public Rejection(string reason, RejectionType type = RejectionType.Permanent) + { + Reason = reason; + Type = type; + } + + public override string ToString() + { + return String.Format("[{0}] {1}", Type, Reason); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/RejectionType.cs b/src/NzbDrone.Core/DecisionEngine/RejectionType.cs new file mode 100644 index 000000000..f15d810ce --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/RejectionType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.DecisionEngine +{ + public enum RejectionType + { + Permanent = 0, + Temporary = 1 + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 1d776f3e1..0983a9a50 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -21,12 +22,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } - public string RejectionReason + public String RejectionReason { get { return "File size too big or small"; } } - public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public RejectionType Type { get { return RejectionType.Permanent; } } + + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Beginning size check for: {0}", subject); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 6ca2b588a..3e3f28a60 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -27,6 +27,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (!_configService.EnableFailedDownloadHandling) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index 7ede1ed8d..198e26a39 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -24,6 +24,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) @@ -31,7 +33,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); - if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Series.QualityProfile, file.Quality, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Series.Profile, file.Quality, subject.ParsedEpisodeInfo.Quality)) { _logger.Debug("Cutoff already met, rejecting."); return false; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index 1ec1eb76d..b8916846f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -1,6 +1,5 @@ using NLog; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications @@ -18,16 +17,21 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { get { - return "Not English"; + return "Language is not wanted"; } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { + var wantedLanguage = subject.Series.Profile.Value.Language; + _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedEpisodeInfo.Language); - if (subject.ParsedEpisodeInfo.Language != Language.English) + + if (subject.ParsedEpisodeInfo.Language != wantedLanguage) { - _logger.Debug("Report Language: {0} rejected because it is not English", subject.ParsedEpisodeInfo.Language); + _logger.Debug("Report Language: {0} rejected because it is not wanted, wanted {1}", subject.ParsedEpisodeInfo.Language, wantedLanguage); return false; } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index 64f19ad72..fa58e4119 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -28,6 +28,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var queue = _downloadTrackingService.GetQueuedDownloads() @@ -46,7 +48,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable queue) { var matchingSeries = queue.Where(q => q.Series.Id == newEpisode.Series.Id); - var matchingSeriesAndQuality = matchingSeries.Where(q => new QualityModelComparer(q.Series.QualityProfile).Compare(q.ParsedEpisodeInfo.Quality, newEpisode.ParsedEpisodeInfo.Quality) >= 0); + var matchingSeriesAndQuality = matchingSeries.Where(q => new QualityModelComparer(q.Series.Profile).Compare(q.ParsedEpisodeInfo.Quality, newEpisode.ParsedEpisodeInfo.Quality) >= 0); return matchingSeriesAndQuality.Any(q => q.Episodes.Select(e => e.Id).Intersect(newEpisode.Episodes.Select(e => e.Id)).Any()); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs index 7a9e9f65c..6818ab5fa 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs @@ -25,6 +25,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Checking if release contains any restricted terms: {0}", subject); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index c5e193012..a7568c96a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -7,7 +7,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class NotSampleSpecification : IDecisionEngineSpecification { private readonly Logger _logger; + public string RejectionReason { get { return "Sample"; } } + public RejectionType Type { get { return RejectionType.Permanent; } } public NotSampleSpecification(Logger logger) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 758d9b2d6..b40a1acab 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -21,10 +21,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedEpisodeInfo.Quality); - if (!subject.Series.QualityProfile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedEpisodeInfo.Quality.Quality)) + if (!subject.Series.Profile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedEpisodeInfo.Quality.Quality)) { _logger.Debug("Quality {0} rejected by Series' quality profile", subject.ParsedEpisodeInfo.Quality); return false; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index 614794b92..5aa7539d2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -25,6 +25,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var age = subject.Release.Age; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs index c99d24c59..dc964932f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs @@ -30,6 +30,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (!_configService.EnableFailedDownloadHandling) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs new file mode 100644 index 000000000..3d117fc54 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -0,0 +1,116 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync +{ + public class DelaySpecification : IDecisionEngineSpecification + { + private readonly IPendingReleaseService _pendingReleaseService; + private readonly Logger _logger; + + public DelaySpecification(IPendingReleaseService pendingReleaseService, Logger logger) + { + _pendingReleaseService = pendingReleaseService; + _logger = logger; + } + + public string RejectionReason + { + get + { + return "Waiting for better quality release"; + } + } + + public RejectionType Type { get { return RejectionType.Temporary; } } + + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + //How do we want to handle drone being off and the automatic search being triggered? + //TODO: Add a flag to the search to state it is a "scheduled" search + + if (searchCriteria != null) + { + _logger.Debug("Ignore delay for searches"); + return true; + } + + var profile = subject.Series.Profile.Value; + + if (profile.GrabDelay == 0) + { + _logger.Debug("Profile does not delay before download"); + return true; + } + + var comparer = new QualityModelComparer(profile); + + if (subject.ParsedEpisodeInfo.Quality.Proper) + { + foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) + { + if (comparer.Compare(subject.ParsedEpisodeInfo.Quality, file.Quality) > 0) + { + var properCompare = subject.ParsedEpisodeInfo.Quality.Proper.CompareTo(file.Quality.Proper); + + if (subject.ParsedEpisodeInfo.Quality.Quality == file.Quality.Quality && properCompare > 0) + { + _logger.Debug("New quality is a proper for existing quality, skipping delay"); + return true; + } + } + } + } + + //If quality meets or exceeds the best allowed quality in the profile accept it immediately + var bestQualityInProfile = new QualityModel(profile.Items.Last(q => q.Allowed).Quality); + var bestCompare = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile); + + if (bestCompare >= 0) + { + _logger.Debug("Quality is highest in profile, will not delay"); + return true; + } + + if (profile.GrabDelayMode == GrabDelayMode.Cutoff) + { + var cutoff = new QualityModel(profile.Cutoff); + var cutoffCompare = comparer.Compare(subject.ParsedEpisodeInfo.Quality, cutoff); + + if (cutoffCompare >= 0) + { + _logger.Debug("Quality meets or exceeds the cutoff, will not delay"); + return true; + } + } + + if (profile.GrabDelayMode == GrabDelayMode.First) + { + var episodeIds = subject.Episodes.Select(e => e.Id); + + var oldest = _pendingReleaseService.GetPendingRemoteEpisodes(subject.Series.Id) + .Where(r => r.Episodes.Select(e => e.Id).Intersect(episodeIds).Any()) + .OrderByDescending(p => p.Release.AgeHours) + .FirstOrDefault(); + + if (oldest != null && oldest.Release.AgeHours > profile.GrabDelay) + { + return true; + } + } + + if (subject.Release.AgeHours < profile.GrabDelay) + { + _logger.Debug("Age ({0}) is less than delay {1}, delaying", subject.Release.AgeHours, profile.GrabDelay); + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 9dd1fc5c4..d2e49d18c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -34,6 +34,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) @@ -63,11 +65,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync foreach (var episode in subject.Episodes) { - var bestQualityInHistory = _historyService.GetBestQualityInHistory(subject.Series.QualityProfile, episode.Id); + var bestQualityInHistory = _historyService.GetBestQualityInHistory(subject.Series.Profile, episode.Id); if (bestQualityInHistory != null) { _logger.Debug("Comparing history quality with report. History is {0}", bestQualityInHistory); - if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.QualityProfile, bestQualityInHistory, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.Profile, bestQualityInHistory, subject.ParsedEpisodeInfo.Quality)) return false; } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs index a0709622f..62f21bceb 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs @@ -22,6 +22,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index 0346ab28a..a5ce14a84 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -28,6 +28,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs index 4d089ce68..576deaa62 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs @@ -23,6 +23,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return "Episode doesn't match"; } } + + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs index 6df94a6fc..506d0790e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs @@ -23,6 +23,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index 211870221..2ce482905 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -21,6 +21,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs index 6e6f146f8..abf15f4b4 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs @@ -21,6 +21,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs index 3960a4e4d..c24dd8821 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs @@ -22,6 +22,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 833f4b9a5..d71bc9225 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -24,13 +24,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + public RejectionType Type { get { return RejectionType.Permanent; } } + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) { _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); - if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.QualityProfile, file.Quality, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.Profile, file.Quality, subject.ParsedEpisodeInfo.Quality)) { return false; } diff --git a/src/NzbDrone.Core/Download/DownloadApprovedReports.cs b/src/NzbDrone.Core/Download/DownloadApprovedReports.cs deleted file mode 100644 index c5dd7ef58..000000000 --- a/src/NzbDrone.Core/Download/DownloadApprovedReports.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Download -{ - public interface IDownloadApprovedReports - { - List DownloadApproved(List decisions); - } - - public class DownloadApprovedReports : IDownloadApprovedReports - { - private readonly IDownloadService _downloadService; - private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision; - private readonly Logger _logger; - - public DownloadApprovedReports(IDownloadService downloadService, IPrioritizeDownloadDecision prioritizeDownloadDecision, Logger logger) - { - _downloadService = downloadService; - _prioritizeDownloadDecision = prioritizeDownloadDecision; - _logger = logger; - } - - public List DownloadApproved(List decisions) - { - var qualifiedReports = GetQualifiedReports(decisions); - var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); - var downloadedReports = new List(); - - foreach (var report in prioritizedDecisions) - { - var remoteEpisode = report.RemoteEpisode; - - try - { - if (downloadedReports.SelectMany(r => r.RemoteEpisode.Episodes) - .Select(e => e.Id) - .ToList() - .Intersect(remoteEpisode.Episodes.Select(e => e.Id)) - .Any()) - { - continue; - } - - _downloadService.DownloadReport(remoteEpisode); - downloadedReports.Add(report); - } - catch (Exception e) - { - _logger.WarnException("Couldn't add report to download queue. " + remoteEpisode, e); - } - } - - return downloadedReports; - } - - public List GetQualifiedReports(IEnumerable decisions) - { - return decisions.Where(c => c.Approved && c.RemoteEpisode.Episodes.Any()).ToList(); - } - } -} diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 8b9365c9c..3ef9ca2b2 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -1,8 +1,5 @@ using NzbDrone.Core.Parser.Model; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace NzbDrone.Core.Download { diff --git a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs new file mode 100644 index 000000000..c99e5ac6a --- /dev/null +++ b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download.Pending +{ + public class PendingRelease : ModelBase + { + public Int32 SeriesId { get; set; } + public String Title { get; set; } + public DateTime Added { get; set; } + public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } + public ReleaseInfo Release { get; set; } + + //Not persisted + public RemoteEpisode RemoteEpisode { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs new file mode 100644 index 000000000..26cfb1d39 --- /dev/null +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Download.Pending +{ + public interface IPendingReleaseRepository : IBasicRepository + { + void DeleteBySeriesId(Int32 seriesId); + List AllBySeriesId(Int32 seriesId); + } + + public class PendingReleaseRepository : BasicRepository, IPendingReleaseRepository + { + public PendingReleaseRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public void DeleteBySeriesId(Int32 seriesId) + { + Delete(r => r.SeriesId == seriesId); + } + + public List AllBySeriesId(Int32 seriesId) + { + return Query.Where(p => p.SeriesId == seriesId); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs new file mode 100644 index 000000000..d60e84e58 --- /dev/null +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Download.Pending +{ + public interface IPendingReleaseService + { + void Add(DownloadDecision decision); + void RemoveGrabbed(List grabbed); + void RemoveRejected(List rejected); + List GetPending(); + List GetPendingRemoteEpisodes(Int32 seriesId); + List GetPendingQueue(); + } + + public class PendingReleaseService : IPendingReleaseService, IHandle + { + private readonly IPendingReleaseRepository _repository; + private readonly ISeriesService _seriesService; + private readonly IParsingService _parsingService; + private readonly IDownloadService _downloadService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public PendingReleaseService(IPendingReleaseRepository repository, + ISeriesService seriesService, + IParsingService parsingService, + IDownloadService downloadService, + IEventAggregator eventAggregator, + Logger logger) + { + _repository = repository; + _seriesService = seriesService; + _parsingService = parsingService; + _downloadService = downloadService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public void Add(DownloadDecision decision) + { + var alreadyPending = GetPendingReleases(); + + var episodeIds = decision.RemoteEpisode.Episodes.Select(e => e.Id); + + var existingReports = alreadyPending.Where(r => r.RemoteEpisode.Episodes.Select(e => e.Id) + .Intersect(episodeIds) + .Any()); + + if (existingReports.Any(MatchingReleasePredicate(decision))) + { + _logger.Debug("This release is already pending, not adding again"); + return; + } + + _logger.Debug("Adding release to pending releases"); + Insert(decision); + } + + public void RemoveGrabbed(List grabbed) + { + _logger.Debug("Removing grabbed releases from pending"); + var alreadyPending = GetPendingReleases(); + + foreach (var decision in grabbed) + { + var decisionLocal = decision; + var episodeIds = decisionLocal.RemoteEpisode.Episodes.Select(e => e.Id); + + + var existingReports = alreadyPending.Where(r => r.RemoteEpisode.Episodes.Select(e => e.Id) + .Intersect(episodeIds) + .Any()); + + foreach (var existingReport in existingReports) + { + _logger.Debug("Removing previously pending release, as it was grabbed."); + Delete(existingReport); + } + } + } + + public void RemoveRejected(List rejected) + { + _logger.Debug("Removing failed releases from pending"); + var pending = GetPendingReleases(); + + foreach (var rejectedRelease in rejected) + { + var matching = pending.SingleOrDefault(MatchingReleasePredicate(rejectedRelease)); + + if (matching != null) + { + _logger.Debug("Removing previously pending release, as it has now been rejected."); + Delete(matching); + } + } + } + + public List GetPending() + { + return _repository.All().Select(p => p.Release).ToList(); + } + + public List GetPendingRemoteEpisodes(int seriesId) + { + return _repository.AllBySeriesId(seriesId).Select(GetRemoteEpisode).ToList(); + } + + public List GetPendingQueue() + { + var queued = new List(); + + foreach (var pendingRelease in GetPendingReleases()) + { + foreach (var episode in pendingRelease.RemoteEpisode.Episodes) + { + var queue = new Queue.Queue + { + Id = episode.Id ^ (pendingRelease.Id << 16), + Series = pendingRelease.RemoteEpisode.Series, + Episode = episode, + Quality = pendingRelease.RemoteEpisode.ParsedEpisodeInfo.Quality, + Title = pendingRelease.Title, + Size = pendingRelease.RemoteEpisode.Release.Size, + Sizeleft = pendingRelease.RemoteEpisode.Release.Size, + Timeleft = + pendingRelease.Release.PublishDate.AddHours( + pendingRelease.RemoteEpisode.Series.Profile.Value.GrabDelay) + .Subtract(DateTime.UtcNow), + Status = "Pending", + RemoteEpisode = pendingRelease.RemoteEpisode + }; + queued.Add(queue); + } + } + + return queued; + } + + private List GetPendingReleases() + { + var result = new List(); + + foreach (var release in _repository.All()) + { + var remoteEpisode = GetRemoteEpisode(release); + + if (remoteEpisode == null) continue; + + release.RemoteEpisode = remoteEpisode; + + result.Add(release); + } + + return result; + } + + private RemoteEpisode GetRemoteEpisode(PendingRelease release) + { + var series = _seriesService.GetSeries(release.SeriesId); + + //Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up) + if (series == null) return null; + + var episodes = _parsingService.GetEpisodes(release.ParsedEpisodeInfo, series, true); + + return new RemoteEpisode + { + Series = series, + Episodes = episodes, + ParsedEpisodeInfo = release.ParsedEpisodeInfo, + Release = release.Release + }; + } + + private void Insert(DownloadDecision decision) + { + _repository.Insert(new PendingRelease + { + SeriesId = decision.RemoteEpisode.Series.Id, + ParsedEpisodeInfo = decision.RemoteEpisode.ParsedEpisodeInfo, + Release = decision.RemoteEpisode.Release, + Title = decision.RemoteEpisode.Release.Title, + Added = DateTime.UtcNow + }); + + _eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent()); + } + + private void Delete(PendingRelease pendingRelease) + { + _repository.Delete(pendingRelease); + _eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent()); + } + + private Func MatchingReleasePredicate(DownloadDecision decision) + { + return p => p.Title == decision.RemoteEpisode.Release.Title && + p.Release.PublishDate == decision.RemoteEpisode.Release.PublishDate && + p.Release.Indexer == decision.RemoteEpisode.Release.Indexer; + } + + public void Handle(SeriesDeletedEvent message) + { + _repository.DeleteBySeriesId(message.Series.Id); + } + } +} diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleasesUpdatedEvent.cs b/src/NzbDrone.Core/Download/Pending/PendingReleasesUpdatedEvent.cs new file mode 100644 index 000000000..fd8c881f1 --- /dev/null +++ b/src/NzbDrone.Core/Download/Pending/PendingReleasesUpdatedEvent.cs @@ -0,0 +1,8 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Download.Pending +{ + public class PendingReleasesUpdatedEvent : IEvent + { + } +} diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs new file mode 100644 index 000000000..4dab30611 --- /dev/null +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public interface IProcessDownloadDecisions + { + ProcessedDecisions ProcessDecisions(List decisions); + } + + public class ProcessDownloadDecisions : IProcessDownloadDecisions + { + private readonly IDownloadService _downloadService; + private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly Logger _logger; + + public ProcessDownloadDecisions(IDownloadService downloadService, + IPrioritizeDownloadDecision prioritizeDownloadDecision, + IPendingReleaseService pendingReleaseService, + Logger logger) + { + _downloadService = downloadService; + _prioritizeDownloadDecision = prioritizeDownloadDecision; + _pendingReleaseService = pendingReleaseService; + _logger = logger; + } + + public ProcessedDecisions ProcessDecisions(List decisions) + { + var qualifiedReports = GetQualifiedReports(decisions); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); + var downloadedReports = new List(); + var pendingReports = new List(); + + foreach (var report in prioritizedDecisions) + { + var remoteEpisode = report.RemoteEpisode; + + if (DownloadingOrPending(downloadedReports, pendingReports, remoteEpisode)) + { + continue; + } + + if (report.TemporarilyRejected) + { + _pendingReleaseService.Add(report); + pendingReports.Add(report); + continue; + } + + try + { + _downloadService.DownloadReport(remoteEpisode); + downloadedReports.Add(report); + } + catch (Exception e) + { + //TODO: support for store & forward + _logger.WarnException("Couldn't add report to download queue. " + remoteEpisode, e); + } + } + + return new ProcessedDecisions(downloadedReports, pendingReports); + } + + internal List GetQualifiedReports(IEnumerable decisions) + { + //Process both approved and temporarily rejected + return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteEpisode.Episodes.Any()).ToList(); + } + + private bool DownloadingOrPending(List downloading, List pending, RemoteEpisode remoteEpisode) + { + var episodeIds = remoteEpisode.Episodes.Select(e => e.Id).ToList(); + + if (downloading.SelectMany(r => r.RemoteEpisode.Episodes) + .Select(e => e.Id) + .ToList() + .Intersect(episodeIds) + .Any()) + { + return true; + } + + if (pending.SelectMany(r => r.RemoteEpisode.Episodes) + .Select(e => e.Id) + .ToList() + .Intersect(episodeIds) + .Any()) + { + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/Download/ProcessedDecisions.cs b/src/NzbDrone.Core/Download/ProcessedDecisions.cs new file mode 100644 index 000000000..c274b931a --- /dev/null +++ b/src/NzbDrone.Core/Download/ProcessedDecisions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NzbDrone.Core.DecisionEngine; + +namespace NzbDrone.Core.Download +{ + public class ProcessedDecisions + { + public List Grabbed { get; set; } + public List Pending { get; set; } + + public ProcessedDecisions(List grabbed, List pending) + { + Grabbed = grabbed; + Pending = pending; + } + } +} diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index e2d359016..ca0fbf3bc 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -17,7 +18,7 @@ namespace NzbDrone.Core.History List All(); void Purge(); void Trim(); - QualityModel GetBestQualityInHistory(QualityProfile qualityProfile, int episodeId); + QualityModel GetBestQualityInHistory(Profile profile, int episodeId); PagingSpec Paged(PagingSpec pagingSpec); List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); @@ -95,9 +96,9 @@ namespace NzbDrone.Core.History _historyRepository.Trim(); } - public QualityModel GetBestQualityInHistory(QualityProfile qualityProfile, int episodeId) + public QualityModel GetBestQualityInHistory(Profile profile, int episodeId) { - var comparer = new QualityModelComparer(qualityProfile); + var comparer = new QualityModelComparer(profile); return _historyRepository.GetBestQualityInHistory(episodeId) .OrderByDescending(q => q, comparer) .FirstOrDefault(); diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs new file mode 100644 index 000000000..83a5e9fd0 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs @@ -0,0 +1,31 @@ +using NLog; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupOrphanedPendingReleases : IHousekeepingTask + { + private readonly IDatabase _database; + private readonly Logger _logger; + + public CleanupOrphanedPendingReleases(IDatabase database, Logger logger) + { + _database = database; + _logger = logger; + } + + public void Clean() + { + _logger.Debug("Running orphaned pending releases cleanup"); + + var mapper = _database.GetDataMapper(); + + mapper.ExecuteNonQuery(@"DELETE FROM PendingReleases + WHERE Id IN ( + SELECT PendingReleases.Id FROM PendingReleases + LEFT OUTER JOIN Series + ON PendingReleases.SeriesId = Series.Id + WHERE Series.Id IS NULL)"); + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index 4bca174db..6a59c3f12 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -21,19 +21,19 @@ namespace NzbDrone.Core.IndexerSearch public class MissingEpisodeSearchService : IEpisodeSearchService, IExecute, IExecute { private readonly ISearchForNzb _nzbSearchService; - private readonly IDownloadApprovedReports _downloadApprovedReports; + private readonly IProcessDownloadDecisions _processDownloadDecisions; private readonly IEpisodeService _episodeService; private readonly IQueueService _queueService; private readonly Logger _logger; public MissingEpisodeSearchService(ISearchForNzb nzbSearchService, - IDownloadApprovedReports downloadApprovedReports, + IProcessDownloadDecisions processDownloadDecisions, IEpisodeService episodeService, IQueueService queueService, Logger logger) { _nzbSearchService = nzbSearchService; - _downloadApprovedReports = downloadApprovedReports; + _processDownloadDecisions = processDownloadDecisions; _episodeService = episodeService; _queueService = queueService; _logger = logger; @@ -52,9 +52,10 @@ namespace NzbDrone.Core.IndexerSearch foreach (var episode in missing) { + //TODO: Add a flag to the search to state it is a "scheduled" search var decisions = _nzbSearchService.EpisodeSearch(episode); - var downloaded = _downloadApprovedReports.DownloadApproved(decisions); - downloadedCount += downloaded.Count; + var processed = _processDownloadDecisions.ProcessDecisions(decisions); + downloadedCount += processed.Grabbed.Count; } _logger.ProgressInfo("Completed search for {0} episodes. {1} reports downloaded.", missing.Count, downloadedCount); @@ -65,9 +66,9 @@ namespace NzbDrone.Core.IndexerSearch foreach (var episodeId in message.EpisodeIds) { var decisions = _nzbSearchService.EpisodeSearch(episodeId); - var downloaded = _downloadApprovedReports.DownloadApproved(decisions); + var processed = _processDownloadDecisions.ProcessDecisions(decisions); - _logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count); + _logger.ProgressInfo("Episode search completed. {0} reports downloaded.", processed.Grabbed.Count); } } @@ -97,8 +98,8 @@ namespace NzbDrone.Core.IndexerSearch { rateGate.WaitToProceed(); var decisions = _nzbSearchService.EpisodeSearch(episode); - var downloaded = _downloadApprovedReports.DownloadApproved(decisions); - downloadedCount += downloaded.Count; + var processed = _processDownloadDecisions.ProcessDecisions(decisions); + downloadedCount += processed.Grabbed.Count; } } diff --git a/src/NzbDrone.Core/IndexerSearch/SeasonSearchService.cs b/src/NzbDrone.Core/IndexerSearch/SeasonSearchService.cs index 4c164ceef..85e849e01 100644 --- a/src/NzbDrone.Core/IndexerSearch/SeasonSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/SeasonSearchService.cs @@ -8,24 +8,24 @@ namespace NzbDrone.Core.IndexerSearch public class SeasonSearchService : IExecute { private readonly ISearchForNzb _nzbSearchService; - private readonly IDownloadApprovedReports _downloadApprovedReports; + private readonly IProcessDownloadDecisions _processDownloadDecisions; private readonly Logger _logger; public SeasonSearchService(ISearchForNzb nzbSearchService, - IDownloadApprovedReports downloadApprovedReports, + IProcessDownloadDecisions processDownloadDecisions, Logger logger) { _nzbSearchService = nzbSearchService; - _downloadApprovedReports = downloadApprovedReports; + _processDownloadDecisions = processDownloadDecisions; _logger = logger; } public void Execute(SeasonSearchCommand message) { var decisions = _nzbSearchService.SeasonSearch(message.SeriesId, message.SeasonNumber); - var downloaded = _downloadApprovedReports.DownloadApproved(decisions); + var processed = _processDownloadDecisions.ProcessDecisions(decisions); - _logger.ProgressInfo("Season search completed. {0} reports downloaded.", downloaded.Count); + _logger.ProgressInfo("Season search completed. {0} reports downloaded.", processed.Grabbed.Count); } } } diff --git a/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs index e80480ef0..d92675b78 100644 --- a/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs @@ -10,17 +10,17 @@ namespace NzbDrone.Core.IndexerSearch { private readonly ISeriesService _seriesService; private readonly ISearchForNzb _nzbSearchService; - private readonly IDownloadApprovedReports _downloadApprovedReports; + private readonly IProcessDownloadDecisions _processDownloadDecisions; private readonly Logger _logger; public SeriesSearchService(ISeriesService seriesService, ISearchForNzb nzbSearchService, - IDownloadApprovedReports downloadApprovedReports, + IProcessDownloadDecisions processDownloadDecisions, Logger logger) { _seriesService = seriesService; _nzbSearchService = nzbSearchService; - _downloadApprovedReports = downloadApprovedReports; + _processDownloadDecisions = processDownloadDecisions; _logger = logger; } @@ -39,7 +39,7 @@ namespace NzbDrone.Core.IndexerSearch } var decisions = _nzbSearchService.SeasonSearch(message.SeriesId, season.SeasonNumber); - downloadedCount += _downloadApprovedReports.DownloadApproved(decisions).Count; + downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count; } _logger.ProgressInfo("Series search completed. {0} reports downloaded.", downloadedCount); diff --git a/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs b/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs new file mode 100644 index 000000000..af4fef0ff --- /dev/null +++ b/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs @@ -0,0 +1,8 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Indexers +{ + public class RssSyncCompleteEvent : IEvent + { + } +} diff --git a/src/NzbDrone.Core/Indexers/RssSyncService.cs b/src/NzbDrone.Core/Indexers/RssSyncService.cs index 540b4e52e..9c9ed7f1f 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncService.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncService.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Indexers { @@ -20,20 +21,26 @@ namespace NzbDrone.Core.Indexers { private readonly IFetchAndParseRss _rssFetcherAndParser; private readonly IMakeDownloadDecision _downloadDecisionMaker; - private readonly IDownloadApprovedReports _downloadApprovedReports; + private readonly IProcessDownloadDecisions _processDownloadDecisions; private readonly IEpisodeSearchService _episodeSearchService; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public RssSyncService(IFetchAndParseRss rssFetcherAndParser, IMakeDownloadDecision downloadDecisionMaker, - IDownloadApprovedReports downloadApprovedReports, + IProcessDownloadDecisions processDownloadDecisions, IEpisodeSearchService episodeSearchService, + IPendingReleaseService pendingReleaseService, + IEventAggregator eventAggregator, Logger logger) { _rssFetcherAndParser = rssFetcherAndParser; _downloadDecisionMaker = downloadDecisionMaker; - _downloadApprovedReports = downloadApprovedReports; + _processDownloadDecisions = processDownloadDecisions; _episodeSearchService = episodeSearchService; + _pendingReleaseService = pendingReleaseService; + _eventAggregator = eventAggregator; _logger = logger; } @@ -42,24 +49,35 @@ namespace NzbDrone.Core.Indexers { _logger.ProgressInfo("Starting RSS Sync"); - var reports = _rssFetcherAndParser.Fetch(); + var reports = _rssFetcherAndParser.Fetch().Concat(_pendingReleaseService.GetPending()).ToList(); var decisions = _downloadDecisionMaker.GetRssDecision(reports); - var downloaded = _downloadApprovedReports.DownloadApproved(decisions); + var processed = _processDownloadDecisions.ProcessDecisions(decisions); + _pendingReleaseService.RemoveGrabbed(processed.Grabbed); + _pendingReleaseService.RemoveRejected(decisions.Where(d => d.Rejected).ToList()); - _logger.ProgressInfo("RSS Sync Completed. Reports found: {0}, Reports downloaded: {1}", reports.Count, downloaded.Count()); + var message = String.Format("RSS Sync Completed. Reports found: {0}, Reports grabbed: {1}", reports.Count, processed.Grabbed.Count); - return downloaded; + if (processed.Pending.Any()) + { + message += ", Reports pending: " + processed.Pending.Count; + } + + _logger.ProgressInfo(message); + + return processed.Grabbed.Concat(processed.Pending).ToList(); } public void Execute(RssSyncCommand message) { - var downloaded = Sync(); + var processed = Sync(); if (message.LastExecutionTime.HasValue && DateTime.UtcNow.Subtract(message.LastExecutionTime.Value).TotalHours > 3) { _logger.Info("RSS Sync hasn't run since: {0}. Searching for any missing episodes since then.", message.LastExecutionTime.Value); - _episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1), downloaded.SelectMany(d => d.RemoteEpisode.Episodes).Select(e => e.Id)); + _episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1), processed.SelectMany(d => d.RemoteEpisode.Episodes).Select(e => e.Id)); } + + _eventAggregator.PublishEvent(new RssSyncCompleteEvent()); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 598093ff7..e0e6c7263 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { var qualifiedImports = decisions.Where(c => c.Approved) .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s - .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.QualityProfile)) + .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.Profile)) .ThenByDescending(c => c.LocalEpisode.Size)) .SelectMany(c => c) .ToList(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 3c96c66d5..3836c1ef0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser; @@ -61,7 +60,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (parsedEpisode != null) { - if (quality != null && new QualityModelComparer(parsedEpisode.Series.QualityProfile).Compare(quality, parsedEpisode.Quality) > 0) + if (quality != null && new QualityModelComparer(parsedEpisode.Series.Profile).Compare(quality, parsedEpisode.Quality) > 0) { _logger.Debug("Using quality from folder: {0}", quality); parsedEpisode.Quality = quality; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index e8dfbeda9..3f91e7296 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -2,7 +2,6 @@ using NLog; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { @@ -19,7 +18,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications public bool IsSatisfiedBy(LocalEpisode localEpisode) { - var qualityComparer = new QualityModelComparer(localEpisode.Series.QualityProfile); + var qualityComparer = new QualityModelComparer(localEpisode.Series.Profile); if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && qualityComparer.Compare(e.EpisodeFile.Value.Quality, localEpisode.Quality) > 0)) { _logger.Debug("This file isn't an upgrade for all episodes. Skipping {0}", localEpisode.Path); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index f85d0f043..0d87b71f6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -205,6 +205,8 @@ + + @@ -222,15 +224,18 @@ + + + - + @@ -282,9 +287,14 @@ + + + + + - + @@ -311,6 +321,7 @@ + @@ -344,6 +355,7 @@ + @@ -458,6 +470,12 @@ + + + + + + @@ -562,7 +580,6 @@ - @@ -570,9 +587,7 @@ - - @@ -679,9 +694,6 @@ Code - - Code - Code @@ -705,7 +717,6 @@ - @@ -724,6 +735,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Language.cs b/src/NzbDrone.Core/Parser/Language.cs index 564de13f2..a56766162 100644 --- a/src/NzbDrone.Core/Parser/Language.cs +++ b/src/NzbDrone.Core/Parser/Language.cs @@ -2,17 +2,17 @@ { public enum Language { - English = 0, - French = 1, - Spanish = 2, - German = 3, - Italian = 4, - Danish = 5, - Dutch = 6, - Japanese = 7, - Cantonese = 8, - Mandarin = 9, - Korean = 10, + Unknown = 0, + English = 1, + French = 2, + Spanish = 3, + German = 4, + Italian = 5, + Danish = 6, + Dutch = 7, + Japanese = 8, + Cantonese = 9, + Mandarin = 10, Russian = 11, Polish = 12, Vietnamese = 13, @@ -22,6 +22,7 @@ Turkish = 17, Portuguese = 18, Flemish = 19, - Greek = 20 + Greek = 20, + Korean = 21 } } diff --git a/src/NzbDrone.Core/Profiles/GrabDelayMode.cs b/src/NzbDrone.Core/Profiles/GrabDelayMode.cs new file mode 100644 index 000000000..146e68894 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/GrabDelayMode.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Profiles +{ + public enum GrabDelayMode + { + First = 0, + Cutoff = 1, + Always = 2 + } +} diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs new file mode 100644 index 000000000..914f49815 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Profile.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Profiles +{ + public class Profile : ModelBase + { + public String Name { get; set; } + public Quality Cutoff { get; set; } + public List Items { get; set; } + public Language Language { get; set; } + public Int32 GrabDelay { get; set; } + public GrabDelayMode GrabDelayMode { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/ProfileInUseException.cs b/src/NzbDrone.Core/Profiles/ProfileInUseException.cs new file mode 100644 index 000000000..d55523d9a --- /dev/null +++ b/src/NzbDrone.Core/Profiles/ProfileInUseException.cs @@ -0,0 +1,13 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Profiles +{ + public class ProfileInUseException : NzbDroneException + { + public ProfileInUseException(int profileId) + : base("Profile [{0}] is in use.", profileId) + { + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs new file mode 100644 index 000000000..35c9ce360 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Profiles +{ + public class ProfileQualityItem : IEmbeddedDocument + { + public Quality Quality { get; set; } + public bool Allowed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Profiles/ProfileRepository.cs b/src/NzbDrone.Core/Profiles/ProfileRepository.cs new file mode 100644 index 000000000..424875617 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/ProfileRepository.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Profiles +{ + public interface IProfileRepository : IBasicRepository + { + + } + + public class ProfileRepository : BasicRepository, IProfileRepository + { + public ProfileRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/ProfileService.cs new file mode 100644 index 000000000..29fe14634 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/ProfileService.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Profiles +{ + public interface IProfileService + { + Profile Add(Profile profile); + void Update(Profile profile); + void Delete(int id); + List All(); + Profile Get(int id); + } + + public class ProfileService : IProfileService, IHandle + { + private readonly IProfileRepository _profileRepository; + private readonly ISeriesService _seriesService; + private readonly Logger _logger; + + public ProfileService(IProfileRepository profileRepository, ISeriesService seriesService, Logger logger) + { + _profileRepository = profileRepository; + _seriesService = seriesService; + _logger = logger; + } + + public Profile Add(Profile profile) + { + return _profileRepository.Insert(profile); + } + + public void Update(Profile profile) + { + _profileRepository.Update(profile); + } + + public void Delete(int id) + { + if (_seriesService.GetAllSeries().Any(c => c.ProfileId == id)) + { + throw new ProfileInUseException(id); + } + + _profileRepository.Delete(id); + } + + public List All() + { + return _profileRepository.All().ToList(); + } + + public Profile Get(int id) + { + return _profileRepository.Get(id); + } + + private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed) + { + var items = Quality.DefaultQualityDefinitions + .OrderBy(v => v.Weight) + .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) + .ToList(); + + var profile = new Profile { Name = name, Cutoff = cutoff, Items = items, Language = Language.English }; + + return Add(profile); + } + + public void Handle(ApplicationStartedEvent message) + { + if (All().Any()) return; + + _logger.Info("Setting up default quality profiles"); + + AddDefaultProfile("SD", Quality.SDTV, + Quality.SDTV, + Quality.WEBDL480p, + Quality.DVD); + + AddDefaultProfile("HD-720p", Quality.HDTV720p, + Quality.HDTV720p, + Quality.WEBDL720p, + Quality.Bluray720p); + + AddDefaultProfile("HD-1080p", Quality.HDTV1080p, + Quality.HDTV1080p, + Quality.WEBDL1080p, + Quality.Bluray1080p); + + AddDefaultProfile("HD - All", Quality.HDTV720p, + Quality.HDTV720p, + Quality.HDTV1080p, + Quality.WEBDL720p, + Quality.WEBDL1080p, + Quality.Bluray720p, + Quality.Bluray1080p); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index bb66dcd7f..ed392e229 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NLog; +using System.Collections.Generic; using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Profiles; namespace NzbDrone.Core.Qualities { public class QualityModelComparer : IComparer, IComparer { - private readonly QualityProfile _qualityProfile; + private readonly Profile _profile; - public QualityModelComparer(QualityProfile qualityProfile) + public QualityModelComparer(Profile profile) { - Ensure.That(qualityProfile, () => qualityProfile).IsNotNull(); - Ensure.That(qualityProfile.Items, () => qualityProfile.Items).HasItems(); + Ensure.That(profile, () => profile).IsNotNull(); + Ensure.That(profile.Items, () => profile.Items).HasItems(); - _qualityProfile = qualityProfile; + _profile = profile; } public int Compare(Quality left, Quality right) { - int leftIndex = _qualityProfile.Items.FindIndex(v => v.Quality == left); - int rightIndex = _qualityProfile.Items.FindIndex(v => v.Quality == right); + int leftIndex = _profile.Items.FindIndex(v => v.Quality == left); + int rightIndex = _profile.Items.FindIndex(v => v.Quality == right); return leftIndex.CompareTo(rightIndex); } diff --git a/src/NzbDrone.Core/Qualities/QualityProfile.cs b/src/NzbDrone.Core/Qualities/QualityProfile.cs deleted file mode 100644 index b578d1962..000000000 --- a/src/NzbDrone.Core/Qualities/QualityProfile.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Qualities -{ - public class QualityProfile : ModelBase - { - public string Name { get; set; } - public Quality Cutoff { get; set; } - public List Items { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualityProfileInUseException.cs b/src/NzbDrone.Core/Qualities/QualityProfileInUseException.cs deleted file mode 100644 index 24c50e7c6..000000000 --- a/src/NzbDrone.Core/Qualities/QualityProfileInUseException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.Qualities -{ - public class QualityProfileInUseException : NzbDroneException - { - public QualityProfileInUseException(int profileId) - : base("QualityProfile [{0}] is in use.", profileId) - { - - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualityProfileItem.cs b/src/NzbDrone.Core/Qualities/QualityProfileItem.cs deleted file mode 100644 index 9d7d839d2..000000000 --- a/src/NzbDrone.Core/Qualities/QualityProfileItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Qualities -{ - public class QualityProfileItem : IEmbeddedDocument - { - public Quality Quality { get; set; } - public bool Allowed { get; set; } - } -} diff --git a/src/NzbDrone.Core/Qualities/QualityProfileRepository.cs b/src/NzbDrone.Core/Qualities/QualityProfileRepository.cs deleted file mode 100644 index 408cc009e..000000000 --- a/src/NzbDrone.Core/Qualities/QualityProfileRepository.cs +++ /dev/null @@ -1,19 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - - -namespace NzbDrone.Core.Qualities -{ - public interface IQualityProfileRepository : IBasicRepository - { - - } - - public class QualityProfileRepository : BasicRepository, IQualityProfileRepository - { - public QualityProfileRepository(IDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - } -} diff --git a/src/NzbDrone.Core/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Qualities/QualityProfileService.cs deleted file mode 100644 index 13b18afad..000000000 --- a/src/NzbDrone.Core/Qualities/QualityProfileService.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; - - -namespace NzbDrone.Core.Qualities -{ - public interface IQualityProfileService - { - QualityProfile Add(QualityProfile profile); - void Update(QualityProfile profile); - void Delete(int id); - List All(); - QualityProfile Get(int id); - } - - public class QualityProfileService : IQualityProfileService, IHandle - { - private readonly IQualityProfileRepository _qualityProfileRepository; - private readonly ISeriesService _seriesService; - private readonly Logger _logger; - - public QualityProfileService(IQualityProfileRepository qualityProfileRepository, ISeriesService seriesService, Logger logger) - { - _qualityProfileRepository = qualityProfileRepository; - _seriesService = seriesService; - _logger = logger; - } - - public QualityProfile Add(QualityProfile profile) - { - return _qualityProfileRepository.Insert(profile); - } - - public void Update(QualityProfile profile) - { - _qualityProfileRepository.Update(profile); - } - - public void Delete(int id) - { - if (_seriesService.GetAllSeries().Any(c => c.QualityProfileId == id)) - { - throw new QualityProfileInUseException(id); - } - - _qualityProfileRepository.Delete(id); - } - - public List All() - { - return _qualityProfileRepository.All().ToList(); - } - - public QualityProfile Get(int id) - { - return _qualityProfileRepository.Get(id); - } - - private QualityProfile AddDefaultQualityProfile(string name, Quality cutoff, params Quality[] allowed) - { - var items = Quality.DefaultQualityDefinitions - .OrderBy(v => v.Weight) - .Select(v => new QualityProfileItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) - .ToList(); - - var qualityProfile = new QualityProfile { Name = name, Cutoff = cutoff, Items = items }; - - return Add(qualityProfile); - } - - public void Handle(ApplicationStartedEvent message) - { - if (All().Any()) return; - - _logger.Info("Setting up default quality profiles"); - - AddDefaultQualityProfile("SD", Quality.SDTV, - Quality.SDTV, - Quality.WEBDL480p, - Quality.DVD); - - AddDefaultQualityProfile("HD-720p", Quality.HDTV720p, - Quality.HDTV720p, - Quality.WEBDL720p, - Quality.Bluray720p); - - AddDefaultQualityProfile("HD-1080p", Quality.HDTV1080p, - Quality.HDTV1080p, - Quality.WEBDL1080p, - Quality.Bluray1080p); - - AddDefaultQualityProfile("HD - All", Quality.HDTV720p, - Quality.HDTV720p, - Quality.HDTV1080p, - Quality.WEBDL720p, - Quality.WEBDL1080p, - Quality.Bluray720p, - Quality.Bluray1080p); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 20b57db4a..f7dac3139 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Collections.Generic; using NLog; using NzbDrone.Core.Download; @@ -39,19 +38,21 @@ namespace NzbDrone.Core.Queue { foreach (var episode in queueItem.DownloadItem.RemoteEpisode.Episodes) { - var queue = new Queue(); - queue.Id = queueItem.DownloadItem.DownloadClientId.GetHashCode() + episode.Id; - queue.Series = queueItem.DownloadItem.RemoteEpisode.Series; - queue.Episode = episode; - queue.Quality = queueItem.DownloadItem.RemoteEpisode.ParsedEpisodeInfo.Quality; - queue.Title = queueItem.DownloadItem.Title; - queue.Size = queueItem.DownloadItem.TotalSize; - queue.Sizeleft = queueItem.DownloadItem.RemainingSize; - queue.Timeleft = queueItem.DownloadItem.RemainingTime; - queue.Status = queueItem.DownloadItem.Status.ToString(); - queue.RemoteEpisode = queueItem.DownloadItem.RemoteEpisode; + var queue = new Queue + { + Id = episode.Id ^ (queueItem.DownloadItem.DownloadClientId.GetHashCode().GetHashCode() << 16), + Series = queueItem.DownloadItem.RemoteEpisode.Series, + Episode = episode, + Quality = queueItem.DownloadItem.RemoteEpisode.ParsedEpisodeInfo.Quality, + Title = queueItem.DownloadItem.Title, + Size = queueItem.DownloadItem.TotalSize, + Sizeleft = queueItem.DownloadItem.RemainingSize, + Timeleft = queueItem.DownloadItem.RemainingTime, + Status = queueItem.DownloadItem.Status.ToString(), + RemoteEpisode = queueItem.DownloadItem.RemoteEpisode + }; - if (queueItem.HasError) + if (queueItem.HasError) { queue.ErrorMessage = queueItem.StatusMessage; } diff --git a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs index f88dfc02e..6747aa87e 100644 --- a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs @@ -2,6 +2,7 @@ using System.Linq; using NLog; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Tv @@ -14,30 +15,30 @@ namespace NzbDrone.Core.Tv public class EpisodeCutoffService : IEpisodeCutoffService { private readonly IEpisodeRepository _episodeRepository; - private readonly IQualityProfileService _qualityProfileService; + private readonly IProfileService _profileService; private readonly Logger _logger; - public EpisodeCutoffService(IEpisodeRepository episodeRepository, IQualityProfileService qualityProfileService, Logger logger) + public EpisodeCutoffService(IEpisodeRepository episodeRepository, IProfileService profileService, Logger logger) { _episodeRepository = episodeRepository; - _qualityProfileService = qualityProfileService; + _profileService = profileService; _logger = logger; } public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec) { var qualitiesBelowCutoff = new List(); - var qualityProfiles = _qualityProfileService.All(); + var profiles = _profileService.All(); //Get all items less than the cutoff - foreach (var qualityProfile in qualityProfiles) + foreach (var profile in profiles) { - var cutoffIndex = qualityProfile.Items.FindIndex(v => v.Quality == qualityProfile.Cutoff); - var belowCutoff = qualityProfile.Items.Take(cutoffIndex).ToList(); + var cutoffIndex = profile.Items.FindIndex(v => v.Quality == profile.Cutoff); + var belowCutoff = profile.Items.Take(cutoffIndex).ToList(); if (belowCutoff.Any()) { - qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(qualityProfile.Id, belowCutoff.Select(i => i.Quality.Id))); + qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id))); } } diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 5ebb8c71d..65f92bf8b 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -224,7 +224,7 @@ namespace NzbDrone.Core.Tv { foreach (var belowCutoff in profile.QualityIds) { - clauses.Add(String.Format("([t1].[QualityProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); + clauses.Add(String.Format("([t1].[ProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); } } diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 2d81a305b..7baf9fb80 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Marr.Data; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Qualities; +using NzbDrone.Core.Profiles; using NzbDrone.Common; @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Tv public string Overview { get; set; } public String AirTime { get; set; } public bool Monitored { get; set; } - public int QualityProfileId { get; set; } + public int ProfileId { get; set; } public bool SeasonFolder { get; set; } public DateTime? LastInfoSync { get; set; } public int Runtime { get; set; } @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Tv public string RootFolderPath { get; set; } public DateTime? FirstAired { get; set; } - public LazyLoaded QualityProfile { get; set; } + public LazyLoaded Profile { get; set; } public List Seasons { get; set; } diff --git a/src/NzbDrone.Core/Validation/LangaugeValidator.cs b/src/NzbDrone.Core/Validation/LangaugeValidator.cs new file mode 100644 index 000000000..678245f36 --- /dev/null +++ b/src/NzbDrone.Core/Validation/LangaugeValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation.Validators; + +namespace NzbDrone.Core.Validation +{ + public class LangaugeValidator : PropertyValidator + { + public LangaugeValidator() + : base("Unknown Language") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return false; + + if ((int) context.PropertyValue == 0) return false; + + return true; + } + } +} diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index 9bcc7e18a..39a3826d3 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; +using NzbDrone.Core.Parser; namespace NzbDrone.Core.Validation { @@ -31,5 +32,10 @@ namespace NzbDrone.Core.Validation { return ruleBuilder.SetValidator(new InclusiveBetweenValidator(0, 65535)); } + + public static IRuleBuilderOptions ValidLanguage(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new LangaugeValidator()); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs b/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs index 610d6f8bf..39f267679 100644 --- a/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs +++ b/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Integration.Test { var newSeries = Series.Lookup("archer").First(); - newSeries.QualityProfileId = 1; + newSeries.ProfileId = 1; newSeries.Path = @"C:\Test\Archer".AsOsAgnostic(); newSeries = Series.Post(newSeries); diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 837beaf72..eb1ca6f15 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -104,7 +104,6 @@ - diff --git a/src/NzbDrone.Integration.Test/QualityProfileIntegrationTest.cs b/src/NzbDrone.Integration.Test/QualityProfileIntegrationTest.cs deleted file mode 100644 index 2fff89a73..000000000 --- a/src/NzbDrone.Integration.Test/QualityProfileIntegrationTest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using NUnit.Framework; - -namespace NzbDrone.Integration.Test -{ - [TestFixture] - public class QualityProfileIntegrationTest : IntegrationTest - { - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/SeriesEditorIntegrationTest.cs b/src/NzbDrone.Integration.Test/SeriesEditorIntegrationTest.cs index c0634ea0c..3667a2247 100644 --- a/src/NzbDrone.Integration.Test/SeriesEditorIntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/SeriesEditorIntegrationTest.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Integration.Test { var newSeries = Series.Lookup(title).First(); - newSeries.QualityProfileId = 1; + newSeries.ProfileId = 1; newSeries.Path = String.Format(@"C:\Test\{0}", title).AsOsAgnostic(); Series.Post(newSeries); @@ -33,13 +33,13 @@ namespace NzbDrone.Integration.Test foreach (var s in series) { - s.QualityProfileId = 2; + s.ProfileId = 2; } var result = Series.Editor(series); result.Should().HaveCount(2); - result.TrueForAll(s => s.QualityProfileId == 2).Should().BeTrue(); + result.TrueForAll(s => s.ProfileId == 2).Should().BeTrue(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/SeriesIntegrationTest.cs b/src/NzbDrone.Integration.Test/SeriesIntegrationTest.cs index eef706a78..9700b898f 100644 --- a/src/NzbDrone.Integration.Test/SeriesIntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/SeriesIntegrationTest.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Integration.Test { var series = Series.Lookup("archer").First(); - series.QualityProfileId = 1; + series.ProfileId = 1; series.Path = @"C:\Test\Archer".AsOsAgnostic(); series = Series.Post(series); @@ -50,7 +50,7 @@ namespace NzbDrone.Integration.Test { var series = Series.Lookup("90210").First(); - series.QualityProfileId = 1; + series.ProfileId = 1; series.Path = @"C:\Test\90210".AsOsAgnostic(); series = Series.Post(series); diff --git a/src/UI/AddSeries/AddSeriesLayout.js b/src/UI/AddSeries/AddSeriesLayout.js index b51245d3a..52e29e070 100644 --- a/src/UI/AddSeries/AddSeriesLayout.js +++ b/src/UI/AddSeries/AddSeriesLayout.js @@ -7,7 +7,7 @@ define( 'AddSeries/RootFolders/RootFolderLayout', 'AddSeries/Existing/AddExistingSeriesCollectionView', 'AddSeries/AddSeriesView', - 'Quality/QualityProfileCollection', + 'Profile/ProfileCollection', 'AddSeries/RootFolders/RootFolderCollection', 'Series/SeriesCollection' ], function (vent, @@ -16,7 +16,7 @@ define( RootFolderLayout, ExistingSeriesCollectionView, AddSeriesView, - QualityProfileCollection, + ProfileCollection, RootFolderCollection) { return Marionette.Layout.extend({ @@ -36,7 +36,7 @@ define( }, initialize: function () { - QualityProfileCollection.fetch(); + ProfileCollection.fetch(); RootFolderCollection.fetch() .done(function () { RootFolderCollection.synced = true; diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index cb9e80612..685182b5d 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -6,7 +6,7 @@ define( 'AppLayout', 'backbone', 'marionette', - 'Quality/QualityProfileCollection', + 'Profile/ProfileCollection', 'AddSeries/RootFolders/RootFolderCollection', 'AddSeries/RootFolders/RootFolderLayout', 'Series/SeriesCollection', @@ -19,7 +19,7 @@ define( AppLayout, Backbone, Marionette, - QualityProfiles, + Profiles, RootFolders, RootFolderLayout, SeriesCollection, @@ -32,7 +32,7 @@ define( template: 'AddSeries/SearchResultViewTemplate', ui: { - qualityProfile : '.x-quality-profile', + profile : '.x-profile', rootFolder : '.x-root-folder', seasonFolder : '.x-season-folder', seriesType : '.x-series-type', @@ -42,11 +42,11 @@ define( }, events: { - 'click .x-add' : '_addSeries', - 'change .x-quality-profile' : '_qualityProfileChanged', - 'change .x-root-folder' : '_rootFolderChanged', - 'change .x-season-folder' : '_seasonFolderChanged', - 'change .x-series-type' : '_seriesTypeChanged' + 'click .x-add' : '_addSeries', + 'change .x-profile' : '_profileChanged', + 'change .x-root-folder' : '_rootFolderChanged', + 'change .x-season-folder' : '_seasonFolderChanged', + 'change .x-series-type' : '_seriesTypeChanged' }, initialize: function () { @@ -65,13 +65,13 @@ define( onRender: function () { - var defaultQuality = Config.getValue(Config.Keys.DefaultQualityProfileId); + var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true); var defaultSeriesType = Config.getValueBoolean(Config.Keys.DefaultSeriesType, true); - if (QualityProfiles.get(defaultQuality)) { - this.ui.qualityProfile.val(defaultQuality); + if (Profiles.get(defaultProfile)) { + this.ui.profile.val(defaultProfile); } if (RootFolders.get(defaultRoot)) { @@ -101,7 +101,7 @@ define( this.templateHelpers.existing = existingSeries[0].toJSON(); } - this.templateHelpers.qualityProfiles = QualityProfiles.toJSON(); + this.templateHelpers.profiles = Profiles.toJSON(); if (!this.model.get('isExisting')) { this.templateHelpers.rootFolders = RootFolders.toJSON(); @@ -109,8 +109,8 @@ define( }, _onConfigUpdated: function (options) { - if (options.key === Config.Keys.DefaultQualityProfileId) { - this.ui.qualityProfile.val(options.value); + if (options.key === Config.Keys.DefaultProfileId) { + this.ui.profile.val(options.value); } else if (options.key === Config.Keys.DefaultRootFolderId) { @@ -126,8 +126,8 @@ define( } }, - _qualityProfileChanged: function () { - Config.setValue(Config.Keys.DefaultQualityProfileId, this.ui.qualityProfile.val()); + _profileChanged: function () { + Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val()); }, _seasonFolderChanged: function () { @@ -160,14 +160,14 @@ define( var icon = this.ui.addButton.find('icon'); icon.removeClass('icon-plus').addClass('icon-spin icon-spinner disabled'); - var quality = this.ui.qualityProfile.val(); + var profile = this.ui.profile.val(); var rootFolderPath = this.ui.rootFolder.children(':selected').text(); var startingSeason = this.ui.startingSeason.val(); var seriesType = this.ui.seriesType.val(); var seasonFolder = this.ui.seasonFolder.prop('checked'); this.model.set({ - qualityProfileId: quality, + profileId: profile, rootFolderPath: rootFolderPath, seasonFolder: seasonFolder, seriesType: seriesType diff --git a/src/UI/AddSeries/SearchResultViewTemplate.html b/src/UI/AddSeries/SearchResultViewTemplate.html index bb56b167d..b60ebcb62 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.html +++ b/src/UI/AddSeries/SearchResultViewTemplate.html @@ -40,8 +40,8 @@
- - {{> QualityProfileSelectionPartial qualityProfiles}} + + {{> ProfileSelectionPartial profiles}}
diff --git a/src/UI/Cells/Edit/QualityCellEditor.js b/src/UI/Cells/Edit/QualityCellEditor.js index e676df24a..475f0108d 100644 --- a/src/UI/Cells/Edit/QualityCellEditor.js +++ b/src/UI/Cells/Edit/QualityCellEditor.js @@ -4,8 +4,8 @@ define( 'backgrid', 'marionette', 'underscore', - 'Settings/Quality/Profile/QualityProfileSchemaCollection', - ], function (Backgrid, Marionette, _, QualityProfileSchemaCollection) { + 'Settings/Profile/ProfileSchemaCollection' + ], function (Backgrid, Marionette, _, ProfileSchemaCollection) { return Backgrid.CellEditor.extend({ className: 'quality-cell-editor', @@ -21,12 +21,12 @@ define( render: function () { var self = this; - var qualityProfileSchemaCollection = new QualityProfileSchemaCollection(); - var promise = qualityProfileSchemaCollection.fetch(); + var profileSchemaCollection = new ProfileSchemaCollection(); + var promise = profileSchemaCollection.fetch(); promise.done(function () { var templateName = self.template; - self.schema = qualityProfileSchemaCollection.first(); + self.schema = profileSchemaCollection.first(); var selected = _.find(self.schema.get('items'), function (model) { return model.quality.id === self.model.get(self.column.get('name')).quality.id; @@ -50,13 +50,13 @@ define( var column = this.column; var selected = parseInt(this.$el.val(), 10); - var qualityProfileItem = _.find(this.schema.get('items'), function(model) { + var profileItem = _.find(this.schema.get('items'), function(model) { return model.quality.id === selected; }); var newQuality = { proper : false, - quality: qualityProfileItem.quality + quality: profileItem.quality }; model.set(column.get('name'), newQuality); diff --git a/src/UI/Cells/QualityProfileCell.js b/src/UI/Cells/ProfileCell.js similarity index 51% rename from src/UI/Cells/QualityProfileCell.js rename to src/UI/Cells/ProfileCell.js index 7cd6efd88..d63cb9701 100644 --- a/src/UI/Cells/QualityProfileCell.js +++ b/src/UI/Cells/ProfileCell.js @@ -2,18 +2,18 @@ define( [ 'backgrid', - 'Quality/QualityProfileCollection', + 'Profile/ProfileCollection', 'underscore' - ], function (Backgrid, QualityProfileCollection,_) { + ], function (Backgrid, ProfileCollection,_) { return Backgrid.Cell.extend({ - className: 'quality-profile-cell', + className: 'profile-cell', render: function () { this.$el.empty(); - var qualityProfileId = this.model.get(this.column.get('name')); + var profileId = this.model.get(this.column.get('name')); - var profile = _.findWhere(QualityProfileCollection.models, { id: qualityProfileId }); + var profile = _.findWhere(ProfileCollection.models, { id: profileId }); if (profile) { this.$el.html(profile.get('name')); diff --git a/src/UI/Config.js b/src/UI/Config.js index 245ac59ae..7a0b03862 100644 --- a/src/UI/Config.js +++ b/src/UI/Config.js @@ -8,7 +8,7 @@ define( ConfigUpdatedEvent: 'ConfigUpdatedEvent' }, Keys : { - DefaultQualityProfileId : 'DefaultQualityProfileId', + DefaultProfileId : 'DefaultProfileId', DefaultRootFolderId : 'DefaultRootFolderId', UseSeasonFolder : 'UseSeasonFolder', DefaultSeriesType : 'DefaultSeriesType', diff --git a/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.html b/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.html index 1263ad98e..87e4b838b 100644 --- a/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.html +++ b/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.html @@ -1,6 +1,6 @@ 
{{#with series}} - {{qualityProfile qualityProfileId}} + {{profile profileId}} {{network}} {{/with}} {{StartTime airDateUtc}} diff --git a/src/UI/Handlebars/Helpers/Quality.js b/src/UI/Handlebars/Helpers/Quality.js index 292c04937..6d9a579cd 100644 --- a/src/UI/Handlebars/Helpers/Quality.js +++ b/src/UI/Handlebars/Helpers/Quality.js @@ -2,15 +2,15 @@ define( [ 'handlebars', - 'Quality/QualityProfileCollection' - ], function (Handlebars, QualityProfileCollection) { + 'Profile/ProfileCollection' + ], function (Handlebars, ProfileCollection) { - Handlebars.registerHelper('qualityProfile', function (profileId) { + Handlebars.registerHelper('profile', function (profileId) { - var profile = QualityProfileCollection.get(profileId); + var profile = ProfileCollection.get(profileId); if (profile) { - return new Handlebars.SafeString('' + profile.get('name') + ''); + return new Handlebars.SafeString('' + profile.get("name") + ''); } return undefined; diff --git a/src/UI/Handlebars/Helpers/String.js b/src/UI/Handlebars/Helpers/String.js new file mode 100644 index 000000000..93b5715fc --- /dev/null +++ b/src/UI/Handlebars/Helpers/String.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'handlebars' + ], function (Handlebars) { + Handlebars.registerHelper('TitleCase', function (input) { + return new Handlebars.SafeString(input.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();})); + }); + }); diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js index 8778df684..69d72c4ed 100644 --- a/src/UI/Handlebars/backbone.marionette.templates.js +++ b/src/UI/Handlebars/backbone.marionette.templates.js @@ -11,7 +11,7 @@ define( 'Handlebars/Helpers/Quality', 'Handlebars/Helpers/System', 'Handlebars/Helpers/EachReverse', - 'Handlebars/Helpers/EachReverse', + 'Handlebars/Helpers/String', 'Handlebars/Handlebars.Debug' ], function (Templates) { return function () { diff --git a/src/UI/History/Queue/QueueStatusCell.js b/src/UI/History/Queue/QueueStatusCell.js index 580d904ce..2fda4b6ea 100644 --- a/src/UI/History/Queue/QueueStatusCell.js +++ b/src/UI/History/Queue/QueueStatusCell.js @@ -31,6 +31,11 @@ define( icon = 'icon-inbox'; title = 'Downloaded'; } + + if (status === 'pending') { + icon = 'icon-time'; + title = 'Pending'; + } if (errorMessage !== '') { if (status === 'completed') { diff --git a/src/UI/History/Queue/TimeleftCell.js b/src/UI/History/Queue/TimeleftCell.js index cea11f12f..043efdf52 100644 --- a/src/UI/History/Queue/TimeleftCell.js +++ b/src/UI/History/Queue/TimeleftCell.js @@ -14,12 +14,21 @@ define( if (this.cellValue) { + //If the release is pending we want to use the timeleft as the time it will be processed at + if (this.cellValue.get('status').toLowerCase() === 'pending') { + this.$el.html('-'); + this.$el.attr('title', 'Will be processed again in: {0}'.format(this.cellValue.get('timeleft'))); + this.$el.attr('data-container', 'body'); + + return this; + } + var timeleft = this.cellValue.get('timeleft'); var totalSize = fileSize(this.cellValue.get('size'), 1, false); var remainingSize = fileSize(this.cellValue.get('sizeleft'), 1, false); if (timeleft === undefined) { - this.$el.html("-"); + this.$el.html('-'); } else { this.$el.html('{0}'.format(timeleft, remainingSize, totalSize)); diff --git a/src/UI/Profile/ProfileCollection.js b/src/UI/Profile/ProfileCollection.js new file mode 100644 index 000000000..2b0844cd1 --- /dev/null +++ b/src/UI/Profile/ProfileCollection.js @@ -0,0 +1,18 @@ +'use strict'; +define( + [ + 'backbone', + 'Profile/ProfileModel' + ], function (Backbone, ProfileModel) { + + var ProfileCollection = Backbone.Collection.extend({ + model: ProfileModel, + url : window.NzbDrone.ApiRoot + '/profile' + }); + + var profiles = new ProfileCollection(); + + profiles.fetch(); + + return profiles; + }); diff --git a/src/UI/Quality/QualityProfileModel.js b/src/UI/Profile/ProfileModel.js similarity index 100% rename from src/UI/Quality/QualityProfileModel.js rename to src/UI/Profile/ProfileModel.js diff --git a/src/UI/Quality/QualityProfileSelectionPartial.html b/src/UI/Profile/ProfileSelectionPartial.html similarity index 53% rename from src/UI/Quality/QualityProfileSelectionPartial.html rename to src/UI/Profile/ProfileSelectionPartial.html index 688d1b276..6d979b88a 100644 --- a/src/UI/Quality/QualityProfileSelectionPartial.html +++ b/src/UI/Profile/ProfileSelectionPartial.html @@ -1,4 +1,4 @@ - {{#each this}} {{/each}} diff --git a/src/UI/Quality/QualityProfileCollection.js b/src/UI/Quality/QualityProfileCollection.js deleted file mode 100644 index f9a33b99e..000000000 --- a/src/UI/Quality/QualityProfileCollection.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Quality/QualityProfileModel' - ], function (Backbone, QualityProfileModel) { - - var QualityProfileCollection = Backbone.Collection.extend({ - model: QualityProfileModel, - url : window.NzbDrone.ApiRoot + '/qualityprofile' - }); - - var profiles = new QualityProfileCollection(); - - profiles.fetch(); - - return profiles; - }); diff --git a/src/UI/Series/Details/InfoViewTemplate.html b/src/UI/Series/Details/InfoViewTemplate.html index 5c962352c..9a5f81071 100644 --- a/src/UI/Series/Details/InfoViewTemplate.html +++ b/src/UI/Series/Details/InfoViewTemplate.html @@ -1,6 +1,6 @@ 
- {{qualityProfile qualityProfileId}} + {{profile profileId}} {{network}} {{runtime}} minutes {{path}} diff --git a/src/UI/Series/Edit/EditSeriesView.js b/src/UI/Series/Edit/EditSeriesView.js index 76801c137..18522f6bd 100644 --- a/src/UI/Series/Edit/EditSeriesView.js +++ b/src/UI/Series/Edit/EditSeriesView.js @@ -3,17 +3,17 @@ define( [ 'vent', 'marionette', - 'Quality/QualityProfileCollection', + 'Profile/ProfileCollection', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView', 'Mixins/AutoComplete' - ], function (vent, Marionette, QualityProfiles, AsModelBoundView, AsValidatedView) { + ], function (vent, Marionette, Profiles, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ template: 'Series/Edit/EditSeriesViewTemplate', ui: { - qualityProfile: '.x-quality-profile', + profile: '.x-profile', path : '.x-path' }, @@ -24,14 +24,14 @@ define( initialize: function () { - this.model.set('qualityProfiles', QualityProfiles); + this.model.set('profiles', Profiles); }, _saveSeries: function () { var self = this; - var qualityProfileId = this.ui.qualityProfile.val(); - this.model.set({ qualityProfileId: qualityProfileId}); + var profileId = this.ui.profile.val(); + this.model.set({ profileId: profileId}); this.model.save().done(function () { self.trigger('saved'); diff --git a/src/UI/Series/Edit/EditSeriesViewTemplate.html b/src/UI/Series/Edit/EditSeriesViewTemplate.html index d619ecbc5..4b8d3f3cd 100644 --- a/src/UI/Series/Edit/EditSeriesViewTemplate.html +++ b/src/UI/Series/Edit/EditSeriesViewTemplate.html @@ -58,11 +58,11 @@
- +
- + {{#each profiles.models}} {{/each}} diff --git a/src/UI/Series/Editor/SeriesEditorFooterView.js b/src/UI/Series/Editor/SeriesEditorFooterView.js index a111be913..d37e4e485 100644 --- a/src/UI/Series/Editor/SeriesEditorFooterView.js +++ b/src/UI/Series/Editor/SeriesEditorFooterView.js @@ -5,7 +5,7 @@ define( 'marionette', 'backgrid', 'vent', - 'Quality/QualityProfileCollection', + 'Profile/ProfileCollection', 'AddSeries/RootFolders/RootFolderCollection', 'Shared/Toolbar/ToolbarLayout', 'AddSeries/RootFolders/RootFolderLayout', @@ -15,7 +15,7 @@ define( Marionette, Backgrid, vent, - QualityProfiles, + Profiles, RootFolders, ToolbarLayout, RootFolderLayout, @@ -26,7 +26,7 @@ define( ui: { monitored : '.x-monitored', - qualityProfile : '.x-quality-profiles', + profile : '.x-profiles', seasonFolder : '.x-season-folder', rootFolder : '.x-root-folder', selectedCount : '.x-selected-count', @@ -43,8 +43,8 @@ define( templateHelpers: function () { return { - qualityProfiles: QualityProfiles, - rootFolders : RootFolders.toJSON() + profiles : Profiles, + rootFolders: RootFolders.toJSON() }; }, @@ -68,7 +68,7 @@ define( var selected = this.editorGrid.getSelectedModels(); var monitored = this.ui.monitored.val(); - var profile = this.ui.qualityProfile.val(); + var profile = this.ui.profile.val(); var seasonFolder = this.ui.seasonFolder.val(); var rootFolder = this.ui.rootFolder.val(); @@ -82,7 +82,7 @@ define( } if (profile !== 'noChange') { - model.set('qualityProfileId', parseInt(profile, 10)); + model.set('profileId', parseInt(profile, 10)); } if (seasonFolder === 'true') { @@ -113,7 +113,7 @@ define( if (selectedCount === 0) { this.ui.monitored.attr('disabled', ''); - this.ui.qualityProfile.attr('disabled', ''); + this.ui.profile.attr('disabled', ''); this.ui.seasonFolder.attr('disabled', ''); this.ui.rootFolder.attr('disabled', ''); this.ui.saveButton.attr('disabled', ''); @@ -122,7 +122,7 @@ define( else { this.ui.monitored.removeAttr('disabled', ''); - this.ui.qualityProfile.removeAttr('disabled', ''); + this.ui.profile.removeAttr('disabled', ''); this.ui.seasonFolder.removeAttr('disabled', ''); this.ui.rootFolder.removeAttr('disabled', ''); this.ui.saveButton.removeAttr('disabled', ''); diff --git a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html index 3ff399d6a..8d30bb969 100644 --- a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html +++ b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html @@ -21,11 +21,11 @@
- + - - {{#each qualityProfiles.models}} + {{#each profiles.models}} {{/each}} diff --git a/src/UI/Series/Editor/SeriesEditorLayout.js b/src/UI/Series/Editor/SeriesEditorLayout.js index 1983444fb..38257a593 100644 --- a/src/UI/Series/Editor/SeriesEditorLayout.js +++ b/src/UI/Series/Editor/SeriesEditorLayout.js @@ -7,7 +7,7 @@ define( 'Series/Index/EmptyView', 'Series/SeriesCollection', 'Cells/SeriesTitleCell', - 'Cells/QualityProfileCell', + 'Cells/ProfileCell', 'Cells/SeriesStatusCell', 'Cells/SeasonFolderCell', 'Shared/Toolbar/ToolbarLayout', @@ -19,7 +19,7 @@ define( EmptyView, SeriesCollection, SeriesTitleCell, - QualityProfileCell, + ProfileCell, SeriesStatusCell, SeasonFolderCell, ToolbarLayout, @@ -33,10 +33,10 @@ define( }, ui: { - monitored : '.x-monitored', - qualityProfiles: '.x-quality-profiles', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count' + monitored : '.x-monitored', + profiles : '.x-profiles', + rootFolder : '.x-root-folder', + selectedCount : '.x-selected-count' }, events: { @@ -47,36 +47,36 @@ define( columns: [ { - name : '', - cell : 'select-row', - headerCell: 'select-all', - sortable : false + name : '', + cell : 'select-row', + headerCell : 'select-all', + sortable : false }, { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell + name : 'statusWeight', + label : '', + cell : SeriesStatusCell }, { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue : 'this' + name : 'title', + label : 'Title', + cell : SeriesTitleCell, + cellValue : 'this' }, { - name : 'qualityProfileId', - label : 'Quality', - cell : QualityProfileCell + name : 'profileId', + label : 'Profile', + cell : ProfileCell }, { - name : 'seasonFolder', - label : 'Season Folder', - cell : SeasonFolderCell + name : 'seasonFolder', + label : 'Season Folder', + cell : SeasonFolderCell }, { - name : 'path', - label : 'Path', - cell : 'string' + name : 'path', + label : 'Path', + cell : 'string' } ], diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html index 3b9ddf3c1..1eb317ace 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html @@ -45,7 +45,7 @@ {{seasonCountHelper}} - {{qualityProfile qualityProfileId}} + {{profile profileId}}
{{> EpisodeProgressPartial }} diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index f0f773d58..d66c79bdf 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -11,7 +11,7 @@ define( 'Cells/RelativeDateCell', 'Cells/SeriesTitleCell', 'Cells/TemplatedCell', - 'Cells/QualityProfileCell', + 'Cells/ProfileCell', 'Cells/EpisodeProgressCell', 'Cells/SeriesActionsCell', 'Cells/SeriesStatusCell', @@ -29,7 +29,7 @@ define( RelativeDateCell, SeriesTitleCell, TemplatedCell, - QualityProfileCell, + ProfileCell, EpisodeProgressCell, SeriesActionsCell, SeriesStatusCell, @@ -65,9 +65,9 @@ define( cell : 'integer' }, { - name : 'qualityProfileId', - label : 'Quality', - cell : QualityProfileCell + name : 'profileId', + label: 'Profile', + cell : ProfileCell }, { name : 'network', @@ -165,7 +165,7 @@ define( }, { title: 'Quality', - name : 'qualityProfileId' + name : 'profileId' }, { title: 'Network', diff --git a/src/UI/Settings/Quality/Profile/AllowedLabeler.js b/src/UI/Settings/Profile/AllowedLabeler.js similarity index 99% rename from src/UI/Settings/Quality/Profile/AllowedLabeler.js rename to src/UI/Settings/Profile/AllowedLabeler.js index dbad67498..667859345 100644 --- a/src/UI/Settings/Quality/Profile/AllowedLabeler.js +++ b/src/UI/Settings/Profile/AllowedLabeler.js @@ -7,6 +7,7 @@ define( Handlebars.registerHelper('allowedLabeler', function () { var ret = ''; var cutoff = this.cutoff; + _.each(this.items, function (item) { if (item.allowed) { if (item.quality.id === cutoff.id) { diff --git a/src/UI/Settings/Quality/Profile/DeleteQualityProfileView.js b/src/UI/Settings/Profile/DeleteProfileView.js similarity index 86% rename from src/UI/Settings/Quality/Profile/DeleteQualityProfileView.js rename to src/UI/Settings/Profile/DeleteProfileView.js index 0aee00971..e685132f5 100644 --- a/src/UI/Settings/Quality/Profile/DeleteQualityProfileView.js +++ b/src/UI/Settings/Profile/DeleteProfileView.js @@ -6,7 +6,7 @@ define( ], function (vent, Marionette) { return Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/DeleteQualityProfileViewTemplate', + template: 'Settings/Profile/DeleteProfileViewTemplate', events: { 'click .x-confirm-delete': '_removeProfile' diff --git a/src/UI/Settings/Quality/Profile/DeleteQualityProfileViewTemplate.html b/src/UI/Settings/Profile/DeleteProfileViewTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Profile/DeleteQualityProfileViewTemplate.html rename to src/UI/Settings/Profile/DeleteProfileViewTemplate.html diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemView.js b/src/UI/Settings/Profile/Edit/EditProfileItemView.js similarity index 61% rename from src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemView.js rename to src/UI/Settings/Profile/Edit/EditProfileItemView.js index dbd101a84..5898f6649 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemView.js +++ b/src/UI/Settings/Profile/Edit/EditProfileItemView.js @@ -4,6 +4,6 @@ define( 'marionette' ], function (Marionette) { return Marionette.ItemView.extend({ - template : 'Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate' + template : 'Settings/Profile/Edit/EditProfileItemViewTemplate' }); }); diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate.html rename to src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.html diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js b/src/UI/Settings/Profile/Edit/EditProfileLayout.js similarity index 83% rename from src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js rename to src/UI/Settings/Profile/Edit/EditProfileLayout.js index 2a5ea7586..e41bbe591 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js +++ b/src/UI/Settings/Profile/Edit/EditProfileLayout.js @@ -6,10 +6,10 @@ define( 'AppLayout', 'marionette', 'backbone', - 'Settings/Quality/Profile/Edit/EditQualityProfileItemView', - 'Settings/Quality/Profile/Edit/QualitySortableCollectionView', - 'Settings/Quality/Profile/Edit/EditQualityProfileView', - 'Settings/Quality/Profile/DeleteQualityProfileView', + 'Settings/Profile/Edit/EditProfileItemView', + 'Settings/Profile/Edit/QualitySortableCollectionView', + 'Settings/Profile/Edit/EditProfileView', + 'Settings/Profile/DeleteProfileView', 'Series/SeriesCollection', 'Config' ], function (_, @@ -17,15 +17,15 @@ define( AppLayout, Marionette, Backbone, - EditQualityProfileItemView, + EditProfileItemView, QualitySortableCollectionView, - EditQualityProfileView, + EditProfileView, DeleteView, SeriesCollection, Config) { return Marionette.Layout.extend({ - template: 'Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate', + template: 'Settings/Profile/Edit/EditProfileLayoutTemplate', regions: { fields : '#x-fields', @@ -37,8 +37,8 @@ define( }, events: { - 'click .x-save' : '_saveQualityProfile', - 'click .x-cancel' : '_cancelQualityProfile', + 'click .x-save' : '_saveProfile', + 'click .x-cancel' : '_cancelProfile', 'click .x-delete' : '_delete' }, @@ -53,7 +53,7 @@ define( }, onShow: function () { - this.fieldsView = new EditQualityProfileView({ model: this.model }); + this.fieldsView = new EditProfileView({ model: this.model }); this._showFieldsView(); this.sortableListView = new QualitySortableCollectionView({ @@ -94,7 +94,7 @@ define( this._showFieldsView(); }, - _saveQualityProfile: function () { + _saveProfile: function () { var self = this; var cutoff = this.fieldsView.getCutoff(); this.model.set('cutoff', cutoff); @@ -109,7 +109,7 @@ define( } }, - _cancelQualityProfile: function () { + _cancelProfile: function () { if (!this.model.has('id')) { vent.trigger(vent.Commands.CloseModalCommand); return; @@ -136,7 +136,7 @@ define( _updateDisableStatus: function () { if (this._isQualityInUse()) { this.ui.deleteButton.addClass('disabled'); - this.ui.deleteButton.attr('title', 'Can\'t delete quality profiles attached to a series.'); + this.ui.deleteButton.attr('title', 'Can\'t delete a profile that is attached to a series.'); } else { this.ui.deleteButton.removeClass('disabled'); @@ -144,7 +144,7 @@ define( }, _isQualityInUse: function () { - return SeriesCollection.where({'qualityProfileId': this.model.id}).length !== 0; + return SeriesCollection.where({'profileId': this.model.id}).length !== 0; } }); }); diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html b/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html rename to src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.html diff --git a/src/UI/Settings/Profile/Edit/EditProfileView.js b/src/UI/Settings/Profile/Edit/EditProfileView.js new file mode 100644 index 000000000..046534358 --- /dev/null +++ b/src/UI/Settings/Profile/Edit/EditProfileView.js @@ -0,0 +1,61 @@ +'use strict'; +define( + [ + 'underscore', + 'marionette', + 'Settings/Profile/Language/LanguageCollection', + 'Config', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (_, Marionette, LanguageCollection, Config, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Profile/Edit/EditProfileViewTemplate', + + ui: { + cutoff : '.x-cutoff', + delay : '.x-delay', + delayMode : '.x-delay-mode' + }, + + events: { + 'change .x-delay': 'toggleDelayMode', + 'keyup .x-delay': 'toggleDelayMode' + }, + + templateHelpers: function () { + return { + languages : LanguageCollection.toJSON() + }; + }, + + onShow: function () { + this.toggleDelayMode(); + }, + + getCutoff: function () { + var self = this; + + return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)}); + }, + + toggleDelayMode: function () { + var delay = parseInt(this.ui.delay.val(), 10); + + if (isNaN(delay)) { + return; + } + + if (delay > 0 && Config.getValueBoolean(Config.Keys.AdvancedSettings)) { + this.ui.delayMode.show(); + } + + else { + this.ui.delayMode.hide(); + } + } + }); + + AsValidatedView.call(view); + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.html b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.html new file mode 100644 index 000000000..1aef89d07 --- /dev/null +++ b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.html @@ -0,0 +1,71 @@ +
+ + +
+ +
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js b/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js new file mode 100644 index 000000000..9c9041de0 --- /dev/null +++ b/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js @@ -0,0 +1,22 @@ +'use strict'; +define( + [ + 'backbone.collectionview', + 'Settings/Profile/Edit/EditProfileItemView' + ], function (BackboneSortableCollectionView, EditProfileItemView) { + return BackboneSortableCollectionView.extend({ + + className: 'qualities', + modelView: EditProfileItemView, + + attributes: { + 'validation-name': 'items' + }, + + events: { + 'click li, td' : '_listItem_onMousedown', + 'dblclick li, td' : '_listItem_onDoubleClick', + 'keydown' : '_onKeydown' + } + }); + }); diff --git a/src/UI/Settings/Profile/Language/LanguageCollection.js b/src/UI/Settings/Profile/Language/LanguageCollection.js new file mode 100644 index 000000000..d190024b1 --- /dev/null +++ b/src/UI/Settings/Profile/Language/LanguageCollection.js @@ -0,0 +1,18 @@ +'use strict'; +define( + [ + 'backbone', + 'Settings/Profile/Language/LanguageModel' + ], function (Backbone, LanguageModel) { + + var LanuageCollection = Backbone.Collection.extend({ + model: LanguageModel, + url : window.NzbDrone.ApiRoot + '/language' + }); + + var languages = new LanuageCollection(); + + languages.fetch(); + + return languages; + }); diff --git a/src/UI/Settings/Profile/Language/LanguageModel.js b/src/UI/Settings/Profile/Language/LanguageModel.js new file mode 100644 index 000000000..ad6b13e3e --- /dev/null +++ b/src/UI/Settings/Profile/Language/LanguageModel.js @@ -0,0 +1,10 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + + }); + }); + diff --git a/src/UI/Settings/Profile/LanguageLabel.js b/src/UI/Settings/Profile/LanguageLabel.js new file mode 100644 index 000000000..c72abda5a --- /dev/null +++ b/src/UI/Settings/Profile/LanguageLabel.js @@ -0,0 +1,20 @@ +'use strict'; +define( + [ + 'underscore', + 'handlebars', + 'Settings/Profile/Language/LanguageCollection' + ], function (_, Handlebars, LanguageCollection) { + Handlebars.registerHelper('languageLabel', function () { + + var wantedLanguage = this.language; + + var language = LanguageCollection.find(function (lang) { + return lang.get('nameLower') === wantedLanguage; + }); + + var result = '' + language.get('name') + ''; + + return new Handlebars.SafeString(result); + }); + }); diff --git a/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html b/src/UI/Settings/Profile/ProfileCollectionTemplate.html similarity index 64% rename from src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html rename to src/UI/Settings/Profile/ProfileCollectionTemplate.html index 88182fda8..11e0047ad 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html +++ b/src/UI/Settings/Profile/ProfileCollectionTemplate.html @@ -1,10 +1,10 @@ 
- Quality Profiles + Profiles
-
    +
    • -
      +
      diff --git a/src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js b/src/UI/Settings/Profile/ProfileCollectionView.js similarity index 69% rename from src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js rename to src/UI/Settings/Profile/ProfileCollectionView.js index b3f5e12af..f4403c144 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js +++ b/src/UI/Settings/Profile/ProfileCollectionView.js @@ -2,16 +2,16 @@ define(['AppLayout', 'marionette', - 'Settings/Quality/Profile/QualityProfileView', - 'Settings/Quality/Profile/Edit/EditQualityProfileLayout', - 'Settings/Quality/Profile/QualityProfileSchemaCollection', + 'Settings/Profile/ProfileView', + 'Settings/Profile/Edit/EditProfileLayout', + 'Settings/Profile/ProfileSchemaCollection', 'underscore' -], function (AppLayout, Marionette, QualityProfileView, EditProfileView, ProfileCollection, _) { +], function (AppLayout, Marionette, ProfileView, EditProfileView, ProfileCollection, _) { return Marionette.CompositeView.extend({ - itemView : QualityProfileView, - itemViewContainer: '.quality-profiles', - template : 'Settings/Quality/Profile/QualityProfileCollectionTemplate', + itemView : ProfileView, + itemViewContainer: '.profiles', + template : 'Settings/Profile/ProfileCollectionTemplate', ui: { 'addCard': '.x-add-card' diff --git a/src/UI/Settings/Profile/ProfileLayout.js b/src/UI/Settings/Profile/ProfileLayout.js new file mode 100644 index 000000000..16822f4f9 --- /dev/null +++ b/src/UI/Settings/Profile/ProfileLayout.js @@ -0,0 +1,27 @@ +'use strict'; + +define( + [ + 'marionette', + 'Profile/ProfileCollection', + 'Settings/Profile/ProfileCollectionView', + 'Settings/Profile/Language/LanguageCollection' + ], function (Marionette, ProfileCollection, ProfileCollectionView, LanguageCollection) { + return Marionette.Layout.extend({ + template: 'Settings/Profile/ProfileLayoutTemplate', + + regions: { + profile : '#profile' + }, + + initialize: function (options) { + this.settings = options.settings; + ProfileCollection.fetch(); + }, + + onShow: function () { + this.profile.show(new ProfileCollectionView({collection: ProfileCollection})); + } + }); + }); + diff --git a/src/UI/Settings/Profile/ProfileLayoutTemplate.html b/src/UI/Settings/Profile/ProfileLayoutTemplate.html new file mode 100644 index 000000000..1e812f24b --- /dev/null +++ b/src/UI/Settings/Profile/ProfileLayoutTemplate.html @@ -0,0 +1,3 @@ +
      +
      +
      diff --git a/src/UI/Settings/Profile/ProfileSchemaCollection.js b/src/UI/Settings/Profile/ProfileSchemaCollection.js new file mode 100644 index 000000000..79101a506 --- /dev/null +++ b/src/UI/Settings/Profile/ProfileSchemaCollection.js @@ -0,0 +1,13 @@ +'use strict'; + +define( + [ + 'backbone', + 'Profile/ProfileModel' + ], function (Backbone, ProfileModel) { + + return Backbone.Collection.extend({ + model: ProfileModel, + url : window.NzbDrone.ApiRoot + '/profile/schema' + }); + }); diff --git a/src/UI/Settings/Quality/Profile/QualityProfileView.js b/src/UI/Settings/Profile/ProfileView.js similarity index 81% rename from src/UI/Settings/Quality/Profile/QualityProfileView.js rename to src/UI/Settings/Profile/ProfileView.js index 45a5ede8e..45d9005cf 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileView.js +++ b/src/UI/Settings/Profile/ProfileView.js @@ -4,14 +4,15 @@ define( [ 'AppLayout', 'marionette', - 'Settings/Quality/Profile/Edit/EditQualityProfileLayout', + 'Settings/Profile/Edit/EditProfileLayout', 'Mixins/AsModelBoundView', - 'Settings/Quality/Profile/AllowedLabeler', + 'Settings/Profile/AllowedLabeler', + 'Settings/Profile/LanguageLabel', 'bootstrap' ], function (AppLayout, Marionette, EditProfileView, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/QualityProfileViewTemplate', + template: 'Settings/Profile/ProfileViewTemplate', tagName : 'li', ui: { diff --git a/src/UI/Settings/Profile/ProfileViewTemplate.html b/src/UI/Settings/Profile/ProfileViewTemplate.html new file mode 100644 index 000000000..dece04aa6 --- /dev/null +++ b/src/UI/Settings/Profile/ProfileViewTemplate.html @@ -0,0 +1,16 @@ +
      +
      +

      +
      + +
      + {{languageLabel}} + + {{#if_gt grabDelay compare="0"}} + + {{/if_gt}} +
      +
        + {{allowedLabeler}} +
      +
      \ No newline at end of file diff --git a/src/UI/Settings/Profile/profile.less b/src/UI/Settings/Profile/profile.less new file mode 100644 index 000000000..185ec469d --- /dev/null +++ b/src/UI/Settings/Profile/profile.less @@ -0,0 +1,31 @@ +@import "../../Content/Bootstrap/mixins"; +@import "../../Content/FontAwesome/font-awesome"; +@import "../../Shared/Styles/clickable.less"; + +.profile-item { + .clickable; + + width: 300px; + height: 158px; + padding: 10px 15px; + + &.add-card { + .center { + margin-top: 10px; + } + } + + .allowed-qualities { + + padding-left: 0px; + + li { + list-style-type : none; + margin: 1px; + } + } + + .language { + margin-bottom: 3px; + } +} diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js index d5a3f9d1a..e08bf41c0 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -25,7 +25,7 @@ define( }, initialize: function (options) { - this.qualityProfileCollection = options.qualityProfiles; + this.profileCollection = options.profiles; this.filesize = fileSize; }, diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js deleted file mode 100644 index 1365de61c..000000000 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -define( - [ - 'underscore', - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView' - ], function (_, Marionette, AsModelBoundView, AsValidatedView) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate', - - ui: { - cutoff : '.x-cutoff' - }, - - getCutoff: function () { - var self = this; - - return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)}); - } - }); - - AsValidatedView.call(view); - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html deleted file mode 100644 index ef0da0ef3..000000000 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html +++ /dev/null @@ -1,23 +0,0 @@ -
      - -
      - -
      -
      -
      - - -
      - -
      - -
      - -
      -
      \ No newline at end of file diff --git a/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js b/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js deleted file mode 100644 index 03693ec29..000000000 --- a/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; -define( - [ - 'backbone.collectionview', - 'Settings/Quality/Profile/Edit/EditQualityProfileItemView' - ], function (BackboneSortableCollectionView, EditQualityProfileItemView) { - return BackboneSortableCollectionView.extend({ - - className: 'qualities', - modelView: EditQualityProfileItemView, - - attributes: { - 'validation-name': 'items' - }, - - events: { - 'click li, td' : '_listItem_onMousedown', - 'dblclick li, td' : '_listItem_onDoubleClick', - 'keydown' : '_onKeydown' - } - }); - }); diff --git a/src/UI/Settings/Quality/Profile/QualityProfileSchemaCollection.js b/src/UI/Settings/Quality/Profile/QualityProfileSchemaCollection.js deleted file mode 100644 index 46cc94027..000000000 --- a/src/UI/Settings/Quality/Profile/QualityProfileSchemaCollection.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -define( - [ - 'backbone', - 'Quality/QualityProfileModel' - ], function (Backbone, QualityProfileModel) { - - return Backbone.Collection.extend({ - model: QualityProfileModel, - url : window.NzbDrone.ApiRoot + '/qualityprofile/schema' - }); - }); diff --git a/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html deleted file mode 100644 index 6dfc5d3ac..000000000 --- a/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html +++ /dev/null @@ -1,9 +0,0 @@ -
      -
      -

      -
      - -
        - {{allowedLabeler}} -
      -
      \ No newline at end of file diff --git a/src/UI/Settings/Quality/QualityLayout.js b/src/UI/Settings/Quality/QualityLayout.js index c80486209..67dcc2f9c 100644 --- a/src/UI/Settings/Quality/QualityLayout.js +++ b/src/UI/Settings/Quality/QualityLayout.js @@ -3,28 +3,23 @@ define( [ 'marionette', - 'Quality/QualityProfileCollection', - 'Settings/Quality/Profile/QualityProfileCollectionView', 'Quality/QualityDefinitionCollection', 'Settings/Quality/Definition/QualityDefinitionCollectionView' - ], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualityDefinitionCollection, QualityDefinitionCollectionView) { + ], function (Marionette, QualityDefinitionCollection, QualityDefinitionCollectionView) { return Marionette.Layout.extend({ template: 'Settings/Quality/QualityLayoutTemplate', regions: { - qualityProfile : '#quality-profile', qualityDefinition : '#quality-definition' }, initialize: function (options) { this.settings = options.settings; - QualityProfileCollection.fetch(); this.qualityDefinitionCollection = new QualityDefinitionCollection(); this.qualityDefinitionCollection.fetch(); }, onShow: function () { - this.qualityProfile.show(new QualityProfileCollectionView({collection: QualityProfileCollection})); this.qualityDefinition.show(new QualityDefinitionCollectionView({collection: this.qualityDefinitionCollection})); } }); diff --git a/src/UI/Settings/Quality/QualityLayoutTemplate.html b/src/UI/Settings/Quality/QualityLayoutTemplate.html index 1684faf98..8db6f83ee 100644 --- a/src/UI/Settings/Quality/QualityLayoutTemplate.html +++ b/src/UI/Settings/Quality/QualityLayoutTemplate.html @@ -1,9 +1,3 @@ 
      -
      -
      - -
      - -
      diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 0376373b4..ad9f69c55 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -2,30 +2,6 @@ @import "../../Content/FontAwesome/font-awesome"; @import "../../Shared/Styles/clickable.less"; -.quality-profile-item { - .clickable; - - width: 300px; - height: 120px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: 10px; - } - } - - .allowed-qualities { - - padding-left: 0px; - - li { - list-style-type : none; - margin: 1px; - } - } -} - ul.qualities { .user-select(none); diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index eddb56ecc..acd7fbc33 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -10,6 +10,7 @@ define( 'Settings/MediaManagement/Naming/NamingModel', 'Settings/MediaManagement/MediaManagementLayout', 'Settings/MediaManagement/MediaManagementSettingsModel', + 'Settings/Profile/ProfileLayout', 'Settings/Quality/QualityLayout', 'Settings/Indexers/IndexerLayout', 'Settings/Indexers/IndexerCollection', @@ -31,6 +32,7 @@ define( NamingModel, MediaManagementLayout, MediaManagementSettingsModel, + ProfileLayout, QualityLayout, IndexerLayout, IndexerCollection, @@ -48,6 +50,7 @@ define( regions: { mediaManagement : '#media-management', + profiles : '#profiles', quality : '#quality', indexers : '#indexers', downloadClient : '#download-client', @@ -59,6 +62,7 @@ define( ui: { mediaManagementTab : '.x-media-management-tab', + profilesTab : '.x-profiles-tab', qualityTab : '.x-quality-tab', indexersTab : '.x-indexers-tab', downloadClientTab : '.x-download-client-tab', @@ -70,6 +74,7 @@ define( events: { 'click .x-media-management-tab' : '_showMediaManagement', + 'click .x-profiles-tab' : '_showProfiles', 'click .x-quality-tab' : '_showQuality', 'click .x-indexers-tab' : '_showIndexers', 'click .x-download-client-tab' : '_showDownloadClient', @@ -109,6 +114,7 @@ define( { self.loading.$el.hide(); self.mediaManagement.show(new MediaManagementLayout({ settings: self.mediaManagementSettings, namingSettings: self.namingSettings })); + self.profiles.show(new ProfileLayout()); self.quality.show(new QualityLayout()); self.indexers.show(new IndexerLayout({ model: self.indexerSettings })); self.downloadClient.show(new DownloadClientLayout({ model: self.downloadClientSettings })); @@ -123,6 +129,9 @@ define( onShow: function () { switch (this.action) { + case 'profiles': + this._showProfiles(); + break; case 'quality': this._showQuality(); break; @@ -158,6 +167,15 @@ define( this._navigate('settings/mediamanagement'); }, + _showProfiles: function (e) { + if (e) { + e.preventDefault(); + } + + this.ui.profilesTab.tab('show'); + this._navigate('settings/profiles'); + }, + _showQuality: function (e) { if (e) { e.preventDefault(); diff --git a/src/UI/Settings/SettingsLayoutTemplate.html b/src/UI/Settings/SettingsLayoutTemplate.html index 84a826ecb..a1deab4da 100644 --- a/src/UI/Settings/SettingsLayoutTemplate.html +++ b/src/UI/Settings/SettingsLayoutTemplate.html @@ -1,5 +1,6 @@ 
      @@ -42,6 +43,7 @@
      +
      \ No newline at end of file diff --git a/src/UI/Settings/UI/UiSettingsModel.js b/src/UI/Settings/UI/UiSettingsModel.js new file mode 100644 index 000000000..4d40b3dae --- /dev/null +++ b/src/UI/Settings/UI/UiSettingsModel.js @@ -0,0 +1,12 @@ +'use strict'; +define( + [ + 'Settings/SettingsModelBase' + ], function (SettingsModelBase) { + return SettingsModelBase.extend({ + + url : window.NzbDrone.ApiRoot + '/config/ui', + successMessage: 'UI settings saved', + errorMessage : 'Failed to save UI settings' + }); + }); diff --git a/src/UI/Settings/UI/UiView.js b/src/UI/Settings/UI/UiView.js new file mode 100644 index 000000000..b251d0cb1 --- /dev/null +++ b/src/UI/Settings/UI/UiView.js @@ -0,0 +1,27 @@ +'use strict'; +define( + [ + 'vent', + 'marionette', + 'Shared/UiSettingsModel', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (vent, Marionette, UiSettingsModel, AsModelBoundView, AsValidatedView) { + var view = Marionette.ItemView.extend({ + template: 'Settings/UI/UiViewTemplate', + + initialize: function () { + this.listenTo(this.model, 'sync', this._reloadUiSettings); + }, + + _reloadUiSettings: function() { + UiSettingsModel.fetch(); + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); + diff --git a/src/UI/Settings/UI/UiViewTemplate.html b/src/UI/Settings/UI/UiViewTemplate.html new file mode 100644 index 000000000..291e8610f --- /dev/null +++ b/src/UI/Settings/UI/UiViewTemplate.html @@ -0,0 +1,95 @@ +
      +
      + Calendar + +
      + + +
      + +
      +
      + +
      + + +
      + +
      + +
      + +
      +
      +
      + +
      + Dates + +
      + + +
      + +
      +
      + +
      + + +
      + +
      +
      + +
      + + +
      + +
      +
      + +
      + + +
      +
      +
      +
      +
      +
      diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less index fdaab3e3d..34f503e7a 100644 --- a/src/UI/Settings/settings.less +++ b/src/UI/Settings/settings.less @@ -109,6 +109,10 @@ li.save-and-add:hover { } .settings-tabs { + li>a { + padding : 10px; + } + @media (min-width: @screen-sm-min) and (max-width: @screen-md-max) { li { a { diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js index aaf51b1e1..7b1e35417 100644 --- a/src/UI/Shared/FormatHelpers.js +++ b/src/UI/Shared/FormatHelpers.js @@ -3,8 +3,9 @@ define( [ 'moment', - 'filesize' - ], function (Moment, Filesize) { + 'filesize', + 'Shared/UiSettingsModel' + ], function (moment, filesize, UiSettings) { return { @@ -15,16 +16,15 @@ define( return ''; } - return Filesize(size, { base: 2, round: 1 }); + return filesize(size, { base: 2, round: 1 }); }, - dateHelper: function (sourceDate) { + relativeDate: function (sourceDate) { if (!sourceDate) { return ''; } - var date = Moment(sourceDate); - + var date = moment(sourceDate); var calendarDate = date.calendar(); //TODO: It would be nice to not have to hack this... @@ -34,12 +34,12 @@ define( return strippedCalendarDate; } - if (date.isAfter(Moment())) { + if (date.isAfter(moment())) { return date.fromNow(true); } - if (date.isBefore(Moment().add('years', -1))) { - return date.format('ll'); + if (date.isBefore(moment().add('years', -1))) { + return date.format(UiSettings.get('shortDateFormat')); } return date.fromNow(); diff --git a/src/UI/Shared/UiSettingsModel.js b/src/UI/Shared/UiSettingsModel.js new file mode 100644 index 000000000..5d9cda590 --- /dev/null +++ b/src/UI/Shared/UiSettingsModel.js @@ -0,0 +1,22 @@ +'use strict'; +define( + [ + 'backbone', + 'api!config/ui' + ], function (Backbone, uiSettings) { + var UiSettings = Backbone.Model.extend({ + + url : window.NzbDrone.ApiRoot + '/config/ui', + + shortDateTime : function () { + return this.get('shortDateFormat') + ' ' + this.get('timeFormat').replace('(', '').replace(')', ''); + }, + + longDateTime : function () { + return this.get('longDateFormat') + ' ' + this.get('timeFormat').replace('(', '').replace(')', ''); + } + }); + + var instance = new UiSettings(uiSettings); + return instance; + }); diff --git a/src/UI/System/Logs/Table/LogTimeCell.js b/src/UI/System/Logs/Table/LogTimeCell.js index dbabc1246..49f082c27 100644 --- a/src/UI/System/Logs/Table/LogTimeCell.js +++ b/src/UI/System/Logs/Table/LogTimeCell.js @@ -2,16 +2,17 @@ define( [ 'Cells/NzbDroneCell', - 'moment' - ], function (NzbDroneCell, Moment) { + 'moment', + 'Shared/UiSettingsModel' + ], function (NzbDroneCell, moment, UiSettings) { return NzbDroneCell.extend({ className: 'log-time-cell', render: function () { - var date = Moment(this._getValue()); - this.$el.html('{0}'.format(date.format('LT'), date.format('LLLL'))); + var date = moment(this._getValue()); + this.$el.html('{0}'.format(date.format(UiSettings.get('timeFormat')), date.format(UiSettings.longDateFormat()))); return this; } diff --git a/src/UI/app.js b/src/UI/app.js index a0878855c..cdec34b7c 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -199,7 +199,6 @@ require.config({ headerCell: 'NzbDrone', sortType : 'toggle' }; - }); } }, @@ -247,7 +246,19 @@ define( 'Instrumentation/StringFormat', 'LifeCycle', 'Hotkeys/Hotkeys' - ], function ($, Backbone, Marionette, RouteBinder, SignalRBroadcaster, NavbarView, AppLayout, SeriesController, Router, ModalController, ControlPanelController, serverStatusModel, Tooltip) { + ], function ($, + Backbone, + Marionette, + RouteBinder, + SignalRBroadcaster, + NavbarView, + AppLayout, + SeriesController, + Router, + ModalController, + ControlPanelController, + serverStatusModel, + Tooltip) { new SeriesController(); new ModalController(); From cd66f8f1989d737fec273cf1d21c82cb0d6f4aea Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 4 Aug 2014 23:31:12 -0700 Subject: [PATCH 041/105] Week column header help is info not warning --- src/UI/Settings/UI/UiViewTemplate.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/Settings/UI/UiViewTemplate.html b/src/UI/Settings/UI/UiViewTemplate.html index 291e8610f..6e8cb4fca 100644 --- a/src/UI/Settings/UI/UiViewTemplate.html +++ b/src/UI/Settings/UI/UiViewTemplate.html @@ -17,7 +17,7 @@
      - +
      From 0a8dcaaf168d8bad765b445a7707fae52c9433ef Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Aug 2014 06:33:05 -0700 Subject: [PATCH 042/105] Fixed error when trying to display time --- src/UI/System/Logs/Table/LogTimeCell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/System/Logs/Table/LogTimeCell.js b/src/UI/System/Logs/Table/LogTimeCell.js index 49f082c27..25b4134e0 100644 --- a/src/UI/System/Logs/Table/LogTimeCell.js +++ b/src/UI/System/Logs/Table/LogTimeCell.js @@ -12,7 +12,7 @@ define( render: function () { var date = moment(this._getValue()); - this.$el.html('{0}'.format(date.format(UiSettings.get('timeFormat')), date.format(UiSettings.longDateFormat()))); + this.$el.html('{0}'.format(date.format(UiSettings.get('timeFormat').replace('(', '').replace(')', '')), date.format(UiSettings.longDateTime()))); return this; } From b0c45aef505ba14956b82a1e057c777880c192a9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Aug 2014 07:30:44 -0700 Subject: [PATCH 043/105] Cleaned up time formatting --- src/UI/Handlebars/Helpers/DateTime.js | 7 +------ src/UI/Shared/FormatHelpers.js | 2 +- src/UI/Shared/UiSettingsModel.js | 12 ++++++++++-- src/UI/System/Logs/Table/LogTimeCell.js | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/UI/Handlebars/Helpers/DateTime.js b/src/UI/Handlebars/Helpers/DateTime.js index 28b8fdf49..ff7f7174f 100644 --- a/src/UI/Handlebars/Helpers/DateTime.js +++ b/src/UI/Handlebars/Helpers/DateTime.js @@ -61,11 +61,6 @@ define( return ''; } - var date = moment(input); - if (date.format('mm') === '00') { - return date.format('ha'); - } - - return date.format('h:mma'); + return moment(input).format(UiSettings.time(false)); }); }); diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js index 7b1e35417..8881b82b2 100644 --- a/src/UI/Shared/FormatHelpers.js +++ b/src/UI/Shared/FormatHelpers.js @@ -45,7 +45,7 @@ define( return date.fromNow(); }, - pad: function(n, width, z) { + pad: function (n, width, z) { z = z || '0'; n = n + ''; return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; diff --git a/src/UI/Shared/UiSettingsModel.js b/src/UI/Shared/UiSettingsModel.js index 5d9cda590..808179019 100644 --- a/src/UI/Shared/UiSettingsModel.js +++ b/src/UI/Shared/UiSettingsModel.js @@ -9,11 +9,19 @@ define( url : window.NzbDrone.ApiRoot + '/config/ui', shortDateTime : function () { - return this.get('shortDateFormat') + ' ' + this.get('timeFormat').replace('(', '').replace(')', ''); + return this.get('shortDateFormat') + ' ' + this.time(true); }, longDateTime : function () { - return this.get('longDateFormat') + ' ' + this.get('timeFormat').replace('(', '').replace(')', ''); + return this.get('longDateFormat') + ' ' + this.time(true); + }, + + time : function (includeMinuteZero) { + if (includeMinuteZero) { + return this.get('timeFormat').replace('(', '').replace(')', ''); + } + + return this.get('timeFormat').replace(/\(\:mm\)/, ''); } }); diff --git a/src/UI/System/Logs/Table/LogTimeCell.js b/src/UI/System/Logs/Table/LogTimeCell.js index 25b4134e0..fd558b404 100644 --- a/src/UI/System/Logs/Table/LogTimeCell.js +++ b/src/UI/System/Logs/Table/LogTimeCell.js @@ -12,7 +12,7 @@ define( render: function () { var date = moment(this._getValue()); - this.$el.html('{0}'.format(date.format(UiSettings.get('timeFormat').replace('(', '').replace(')', '')), date.format(UiSettings.longDateTime()))); + this.$el.html('{0}'.format(date.format(UiSettings.time(true)), date.format(UiSettings.longDateTime()))); return this; } From b427954f5f27768ce9c7f7891021b32bf2043c19 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Mon, 28 Jul 2014 13:16:40 +0200 Subject: [PATCH 044/105] null EmbeddedDocuments are now stored as DBNull instead of json null. --- .../Datastore/MarrDataLazyLoadingFixture.cs | 1 + .../HistoryTests/HistoryRepositoryFixture.cs | 9 +++++++-- .../Housekeepers/CleanupOrphanedBlacklistFixture.cs | 11 +++++++++-- .../CleanupOrphanedEpisodeFilesFixture.cs | 4 ++++ .../CleanupOrphanedHistoryItemsFixture.cs | 5 +++++ .../CleanupOrphanedMetadataFilesFixture.cs | 2 ++ .../CleanupOrphanedPendingReleasesFixture.cs | 11 ++++++++--- .../DownloadedEpisodesImportServiceFixture.cs | 2 +- .../EpisodesRepositoryReadFixture.cs | 5 ++++- .../EpisodesWithFilesFixture.cs | 7 +++++-- .../Datastore/Converters/EmbeddedDocumentConverter.cs | 1 + 11 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs index 8cf59717b..49d67f063 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -36,6 +36,7 @@ namespace NzbDrone.Core.Test.Datastore var episodeFiles = Builder.CreateListOfSize(1) .All() .With(v => v.SeriesId = series[0].Id) + .With(v => v.Quality = new QualityModel()) .BuildListOfNew(); Db.InsertMany(episodeFiles); diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs index e2da6251c..34b6936bb 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs @@ -4,6 +4,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.History; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Test.HistoryTests { @@ -15,7 +16,8 @@ namespace NzbDrone.Core.Test.HistoryTests { var historyItem = Builder.CreateListOfSize(30) .All() - .With(c=>c.Id = 0) + .With(c => c.Id = 0) + .With(c => c.Quality = new QualityModel()) .TheFirst(10).With(c => c.Date = DateTime.Now) .TheNext(20).With(c => c.Date = DateTime.Now.AddDays(-31)) .Build(); @@ -32,7 +34,9 @@ namespace NzbDrone.Core.Test.HistoryTests [Test] public void should_read_write_dictionary() { - var history = Builder.CreateNew().BuildNew(); + var history = Builder.CreateNew() + .With(c => c.Quality = new QualityModel()) + .BuildNew(); history.Data.Add("key1","value1"); history.Data.Add("key2","value2"); @@ -48,6 +52,7 @@ namespace NzbDrone.Core.Test.HistoryTests var history = Builder .CreateListOfSize(5) .All() + .With(c => c.Quality = new QualityModel()) .With(c => c.EventType = HistoryEventType.Unknown) .Random(3) .With(c => c.EventType = HistoryEventType.Grabbed) diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs index 350471a1d..e5c3c077e 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs @@ -3,8 +3,11 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers { @@ -15,6 +18,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_delete_orphaned_blacklist_items() { var blacklist = Builder.CreateNew() + .With(h => h.EpisodeIds = new List()) + .With(h => h.Quality = new QualityModel()) .BuildNew(); Db.Insert(blacklist); @@ -30,8 +35,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Db.Insert(series); var blacklist = Builder.CreateNew() - .With(b => b.SeriesId = series.Id) - .BuildNew(); + .With(h => h.EpisodeIds = new List()) + .With(h => h.Quality = new QualityModel()) + .With(b => b.SeriesId = series.Id) + .BuildNew(); Db.Insert(blacklist); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedEpisodeFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedEpisodeFilesFixture.cs index 0c97d0ae7..b09def40c 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedEpisodeFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedEpisodeFilesFixture.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers { @@ -16,6 +17,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_delete_orphaned_episode_files() { var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) .BuildNew(); Db.Insert(episodeFile); @@ -27,6 +29,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_not_delete_unorphaned_episode_files() { var episodeFiles = Builder.CreateListOfSize(2) + .All() + .With(h => h.Quality = new QualityModel()) .BuildListOfNew(); Db.InsertMany(episodeFiles); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs index 3ebf89dbd..022248abd 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs @@ -2,6 +2,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -39,6 +40,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers GivenEpisode(); var history = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) .With(h => h.EpisodeId = _episode.Id) .BuildNew(); Db.Insert(history); @@ -53,6 +55,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers GivenSeries(); var history = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) .With(h => h.SeriesId = _series.Id) .BuildNew(); Db.Insert(history); @@ -69,6 +72,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var history = Builder.CreateListOfSize(2) .All() + .With(h => h.Quality = new QualityModel()) .With(h => h.EpisodeId = _episode.Id) .TheFirst(1) .With(h => h.SeriesId = _series.Id) @@ -89,6 +93,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var history = Builder.CreateListOfSize(2) .All() + .With(h => h.Quality = new QualityModel()) .With(h => h.SeriesId = _series.Id) .TheFirst(1) .With(h => h.EpisodeId = _episode.Id) diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs index 4ea0046c3..7e4dc060f 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Metadata; using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -68,6 +69,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers .BuildNew(); var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) .BuildNew(); Db.Insert(series); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs index 34965efae..104ba9bfc 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs @@ -3,6 +3,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -15,7 +16,9 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_delete_orphaned_pending_items() { var pendingRelease = Builder.CreateNew() - .BuildNew(); + .With(h => h.ParsedEpisodeInfo = new ParsedEpisodeInfo()) + .With(h => h.Release = new ReleaseInfo()) + .BuildNew(); Db.Insert(pendingRelease); Subject.Clean(); @@ -30,8 +33,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Db.Insert(series); var pendingRelease = Builder.CreateNew() - .With(h => h.SeriesId = series.Id) - .BuildNew(); + .With(h => h.SeriesId = series.Id) + .With(h => h.ParsedEpisodeInfo = new ParsedEpisodeInfo()) + .With(h => h.Release = new ReleaseInfo()) + .BuildNew(); Db.Insert(pendingRelease); diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 3cc90f748..ad3d85bbb 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new DownloadedEpisodesScanCommand()); Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); VerifyNoImport(); diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs index 1a7f06151..07a43b9ca 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs @@ -2,6 +2,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -25,7 +26,9 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_get_episodes_by_file() { - var episodeFile = Builder.CreateNew().BuildNew(); + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .BuildNew(); Db.Insert(episodeFile); diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs index 5dd3b03d2..3925850ee 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { @@ -21,8 +22,9 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests public void Setup() { _episodeFiles = Builder.CreateListOfSize(5) - .BuildListOfNew() - .ToList(); + .All() + .With(c => c.Quality = new QualityModel()) + .BuildListOfNew(); Db.InsertMany(_episodeFiles); @@ -56,6 +58,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { var episodeFile = Builder.CreateNew() .With(f => f.Path = "another path") + .With(c => c.Quality = new QualityModel()) .BuildNew(); Db.Insert(episodeFile); diff --git a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs index 806bbf5f4..222c53b2a 100644 --- a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs @@ -55,6 +55,7 @@ namespace NzbDrone.Core.Datastore.Converters public object ToDB(object clrValue) { if (clrValue == null) return null; + if (clrValue == DBNull.Value) return DBNull.Value; return JsonConvert.SerializeObject(clrValue, SerializerSetting); } From 7b420fc03309fe2a87b7639c0b71ee80ab647393 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 10 Apr 2014 19:58:50 +0200 Subject: [PATCH 045/105] Added MediaInfo to EpisodeFile. --- .../UpdateMediaInfoServiceFixture.cs | 118 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../056_add_mediainfo_to_episodefile.cs | 15 +++ src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 2 + .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../EpisodeImport/ImportDecisionMaker.cs | 7 +- .../MediaFiles/MediaFileRepository.cs | 6 + .../MediaFiles/MediaFileService.cs | 6 + .../MediaFiles/MediaInfo/MediaInfoModel.cs | 3 +- .../MediaInfo/UpdateMediaInfoService.cs | 63 ++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + .../Organizer/FileNameBuilder.cs | 87 +++++++++++++ .../Parser/Model/LocalEpisode.cs | 2 + src/NzbDrone.Core/Parser/ParsingService.cs | 3 + 14 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs new file mode 100644 index 000000000..4b29c0b46 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -0,0 +1,118 @@ +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo +{ + [TestFixture] + public class UpdateMediaInfoServiceFixture : CoreTest + { + private void GivenFileExists() + { + Mocker.GetMock() + .Setup(v => v.FileExists(It.IsAny())) + .Returns(true); + } + + private void GivenSuccessfulScan() + { + Mocker.GetMock() + .Setup(v => v.GetMediaInfo(It.IsAny())) + .Returns(new MediaInfoModel()); + } + + private void GivenFailedScan(String path) + { + Mocker.GetMock() + .Setup(v => v.GetMediaInfo(path)) + .Returns((MediaInfoModel)null); + } + + [Test] + public void should_get_for_existing_episodefile_on_after_series_scan() + { + var episodeFiles = Builder.CreateListOfSize(3) + .All() + .With(v => v.Path = @"C:\series\media.mkv".AsOsAgnostic()) + .TheFirst(1) + .With(v => v.MediaInfo = new MediaInfoModel()) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Handle(new SeriesScannedEvent(new Tv.Series { Id = 1 })); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(@"C:\series\media.mkv".AsOsAgnostic()), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void should_ignore_missing_files() + { + var episodeFiles = Builder.CreateListOfSize(2) + .All() + .With(v => v.Path = @"C:\series\media.mkv".AsOsAgnostic()) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + GivenSuccessfulScan(); + + Subject.Handle(new SeriesScannedEvent(new Tv.Series { Id = 1 })); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(@"C:\series\media.mkv".AsOsAgnostic()), Times.Never()); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Never()); + } + + [Test] + public void should_continue_after_failure() + { + var episodeFiles = Builder.CreateListOfSize(2) + .All() + .With(v => v.Path = @"C:\series\media.mkv".AsOsAgnostic()) + .TheFirst(1) + .With(v => v.Path = @"C:\series\media2.mkv".AsOsAgnostic()) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + GivenFileExists(); + GivenSuccessfulScan(); + GivenFailedScan(@"C:\series\media2.mkv".AsOsAgnostic()); + + Subject.Handle(new SeriesScannedEvent(new Tv.Series { Id = 1 })); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(@"C:\series\media.mkv".AsOsAgnostic()), Times.Exactly(1)); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Exactly(1)); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 9d137cfde..bf8c6b567 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -190,6 +190,7 @@ + diff --git a/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs b/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs new file mode 100644 index 000000000..f8763332b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(56)] + public class add_mediainfo_to_episodefile : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("EpisodeFiles").AddColumn("MediaInfo").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 12dccb7b0..242ee02aa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -2,6 +2,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.MediaFiles { @@ -15,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles public string SceneName { get; set; } public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } public LazyList Episodes { get; set; } public override string ToString() diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index e0e6c7263..5494717e8 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -73,6 +73,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.Path = localEpisode.Path.CleanFilePath(); episodeFile.Size = _diskProvider.GetFileSize(localEpisode.Path); episodeFile.Quality = localEpisode.Quality; + episodeFile.MediaInfo = localEpisode.MediaInfo; episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; episodeFile.ReleaseGroup = localEpisode.ParsedEpisodeInfo.ReleaseGroup; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 3836c1ef0..93382b430 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -23,19 +24,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IParsingService _parsingService; private readonly IMediaFileService _mediaFileService; private readonly IDiskProvider _diskProvider; + private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, IParsingService parsingService, IMediaFileService mediaFileService, IDiskProvider diskProvider, - + IVideoFileInfoReader videoFileInfoReader, Logger logger) { _specifications = specifications; _parsingService = parsingService; _mediaFileService = mediaFileService; _diskProvider = diskProvider; + _videoFileInfoReader = videoFileInfoReader; _logger = logger; } @@ -69,6 +72,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport parsedEpisode.Size = _diskProvider.GetFileSize(file); _logger.Debug("Size: {0}", parsedEpisode.Size); + parsedEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); + decision = GetDecision(parsedEpisode); } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 0a7fe8a70..57f9976cb 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles { List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFilesWithoutMediaInfo(); } @@ -30,5 +31,10 @@ namespace NzbDrone.Core.MediaFiles .AndWhere(c => c.SeasonNumber == seasonNumber) .ToList(); } + + public List GetFilesWithoutMediaInfo() + { + return Query.Where(c => c.MediaInfo == null).ToList(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 3bc146fad..f45753929 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.MediaFiles void Delete(EpisodeFile episodeFile, bool forUpgrade = false); List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, int seriesId); EpisodeFile Get(int id); List Get(IEnumerable ids); @@ -62,6 +63,11 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); } + public List GetFilesWithoutMediaInfo() + { + return _mediaFileRepository.GetFilesWithoutMediaInfo(); + } + public List FilterExistingFiles(List files, int seriesId) { var seriesFiles = GetFilesBySeries(seriesId).Select(f => f.Path).ToList(); diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index c18ec0870..964c62d4d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,8 +1,9 @@ using System; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaFiles.MediaInfo { - public class MediaInfoModel + public class MediaInfoModel : IEmbeddedDocument { public string VideoCodec { get; set; } public int VideoBitrate { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs new file mode 100644 index 000000000..cf3502dea --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -0,0 +1,63 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.MediaInfo +{ + public class UpdateMediaInfoService : IHandle + { + private readonly IDiskProvider _diskProvider; + private readonly IMediaFileService _mediaFileService; + private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly Logger _logger; + + public UpdateMediaInfoService(IDiskProvider diskProvider, + IMediaFileService mediaFileService, + IVideoFileInfoReader videoFileInfoReader, + Logger logger) + { + _diskProvider = diskProvider; + _mediaFileService = mediaFileService; + _videoFileInfoReader = videoFileInfoReader; + _logger = logger; + } + + private void UpdateMediaInfo(List mediaFiles) + { + foreach (var mediaFile in mediaFiles) + { + var path = mediaFile.Path; + + if (!_diskProvider.FileExists(path)) + { + _logger.Debug("Can't update MediaInfo because '{0}' does not exist", path); + continue; + } + + mediaFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(path); + + if (mediaFile.MediaInfo != null) + { + _mediaFileService.Update(mediaFile); + _logger.Debug("Updated MediaInfo for '{0}'", path); + } + } + } + + public void Handle(SeriesScannedEvent message) + { + var mediaFiles = _mediaFileService.GetFilesBySeries(message.Series.Id) + .Where(c => c.MediaInfo == null) + .ToList(); + + UpdateMediaInfo(mediaFiles); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 6ea6013bd..3873058a0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -212,6 +212,7 @@ + @@ -482,6 +483,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7a65d7dfb..7b9920f1a 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -216,6 +216,8 @@ namespace NzbDrone.Core.Organizer tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); + AddMediaInfoTokens(episodeFile, tokenValues); + var filename = ReplaceTokens(pattern, tokenValues).Trim(); filename = FilenameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString() ); @@ -333,6 +335,91 @@ namespace NzbDrone.Core.Organizer return result.Trim(); } + private void AddMediaInfoTokens(EpisodeFile episodeFile, Dictionary tokenValues) + { + if (episodeFile.MediaInfo == null) + return; + + var mediaInfoFull = string.Empty; + + switch (episodeFile.MediaInfo.VideoCodec) + { + case "AVC": + if (Path.GetFileNameWithoutExtension(episodeFile.Path).Contains("x264")) + mediaInfoFull += "x264"; + else if (Path.GetFileNameWithoutExtension(episodeFile.Path).Contains("h264")) + mediaInfoFull += "h264"; + else + mediaInfoFull += "h264"; + break; + + default: + mediaInfoFull += episodeFile.MediaInfo.VideoCodec; + break; + } + + switch (episodeFile.MediaInfo.AudioFormat) + { + case "AC-3": + mediaInfoFull += ".AC3"; + break; + + case "MPEG Audio": + if (episodeFile.MediaInfo.AudioProfile == "Layer 3") + mediaInfoFull += ".MP3"; + else + mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; + break; + + case "DTS": + mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; + break; + + default: + mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; + break; + } + + tokenValues.Add("{MediaInfo Short}", mediaInfoFull); + + var audioLanguagesToken = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + if (!string.IsNullOrEmpty(audioLanguagesToken) && audioLanguagesToken != "EN") + mediaInfoFull += string.Format("[{0}]", audioLanguagesToken); + + var subtitleLanguagesToken = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + if (!string.IsNullOrEmpty(subtitleLanguagesToken)) + mediaInfoFull += string.Format(".[{0}]", subtitleLanguagesToken); + + tokenValues.Add("{MediaInfo Full}", mediaInfoFull); + } + + private string GetLanguagesToken(string mediaInfoLanguages) + { + List tokens = new List(); + foreach (var item in mediaInfoLanguages.Split('/')) + { + if (!string.IsNullOrWhiteSpace(item)) + tokens.Add(item.Trim()); + } + + var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); + + if (cultureInfo != null) + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + } + catch + { + } + } + + return string.Join("+", tokens.Distinct()); + } + private string ReplaceTokens(string pattern, Dictionary tokenValues) { return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenValues)); diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 447055328..eefe76e51 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Collections.Generic; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.Parser.Model { @@ -14,6 +15,7 @@ namespace NzbDrone.Core.Parser.Model public Series Series { get; set; } public List Episodes { get; set; } public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } public Boolean ExistingFile { get; set; } public int SeasonNumber diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 61ede3492..d39a53064 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -27,16 +27,19 @@ namespace NzbDrone.Core.Parser private readonly IEpisodeService _episodeService; private readonly ISeriesService _seriesService; private readonly ISceneMappingService _sceneMappingService; + private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public ParsingService(IEpisodeService episodeService, ISeriesService seriesService, ISceneMappingService sceneMappingService, + IDiskProvider diskProvider, Logger logger) { _episodeService = episodeService; _seriesService = seriesService; _sceneMappingService = sceneMappingService; + _diskProvider = diskProvider; _logger = logger; } From 8a5edfeaf50ea59d0c7902cf333e538a2405855a Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Mon, 28 Jul 2014 23:14:34 +0200 Subject: [PATCH 046/105] Rewrote most of the renamer token handling code to give it a bit more generic architecture. Also added MediaInfo as possible token. --- src/NzbDrone.Api/Config/NamingConfigModule.cs | 8 +- .../MoveEpisodeFileFixture.cs | 2 +- .../MediaFiles/MediaFileServiceTest.cs | 2 +- .../OrganizerTests/FileNameBuilderFixture.cs | 144 ++++-- .../OrganizerTests/GetSeriesFolderFixture.cs | 4 +- .../SeriesServiceTests/AddSeriesFixture.cs | 2 +- .../Download/Clients/Pneumatic/Pneumatic.cs | 2 +- .../UsenetBlackhole/UsenetBlackhole.cs | 6 +- .../MediaFiles/EpisodeFileMovingService.cs | 6 +- .../MediaFiles/RenameEpisodeFileService.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 6 +- .../Organizer/FileNameBuilder.cs | 439 +++++++++++------- .../FilenameBuilderTokenEqualityComparer.cs | 6 +- .../Organizer/FilenameSampleService.cs | 59 ++- .../Organizer/FilenameValidationService.cs | 8 +- src/NzbDrone.Core/Organizer/SampleResult.cs | 2 +- src/NzbDrone.Core/Tv/SeriesService.cs | 2 +- .../Naming/NamingViewTemplate.html | 3 + .../Partials/MediaInfoNamingPartial.html | 11 + .../Partials/SeriesTitleNamingPartial.html | 3 + 20 files changed, 452 insertions(+), 265 deletions(-) create mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.html diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index 03d494aac..b7b2a035f 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -85,19 +85,19 @@ namespace NzbDrone.Api.Config sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null ? "Invalid format" - : singleEpisodeSampleResult.Filename; + : singleEpisodeSampleResult.FileName; sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null ? "Invalid format" - : multiEpisodeSampleResult.Filename; + : multiEpisodeSampleResult.FileName; sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null ? "Invalid format" - : dailyEpisodeSampleResult.Filename; + : dailyEpisodeSampleResult.FileName; sampleResource.AnimeEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult) != null ? "Invalid format" - : animeEpisodeSampleResult.Filename; + : animeEpisodeSampleResult.FileName; sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace() ? "Invalid format" diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs index 00b1302ec..75fe64ca6 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeFileMovingServiceTests .Build(); Mocker.GetMock() - .Setup(s => s.BuildFilename(It.IsAny>(), It.IsAny(), It.IsAny())) + .Setup(s => s.BuildFileName(It.IsAny>(), It.IsAny(), It.IsAny(), null)) .Returns("File Name"); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTest.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTest.cs index ea567a509..d1bd43441 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTest.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTest.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.MediaFiles "Law & Order- Criminal Intent - S10E07 - Icarus [HDTV-720p]")] public void CleanFileName(string name, string expectedName) { - FileNameBuilder.CleanFilename(name).Should().Be(expectedName); + FileNameBuilder.CleanFileName(name).Should().Be(expectedName); } [Test] diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs index e0a06619d..278407505 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs @@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Series Title}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("South Park"); } @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Series_Title}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("South_Park"); } @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Series.Title}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("South.Park"); } @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Series-Title}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("South-Park"); } @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{SERIES TITLE}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("SOUTH PARK"); } @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{sErIES-tItLE}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(_series.Title.Replace(' ', '-')); } @@ -123,16 +123,26 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{series title}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("south park"); } + [Test] + public void should_cleanup_Series_Title() + { + _namingConfig.StandardEpisodeFormat = "{Series.CleanTitle}"; + _series.Title = "South Park (1997)"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.1997"); + } + [Test] public void should_replace_episode_title() { _namingConfig.StandardEpisodeFormat = "{Episode Title}"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("City Sushi"); } @@ -141,7 +151,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{ePisOde-TitLe}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("City-Sushi"); } @@ -151,7 +161,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.SeasonNumber = 1; _namingConfig.StandardEpisodeFormat = "{season}x{episode}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("1x6"); } @@ -161,7 +171,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.SeasonNumber = 1; _namingConfig.StandardEpisodeFormat = "{season:00}x{episode}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("01x6"); } @@ -171,7 +181,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.SeasonNumber = 1; _namingConfig.StandardEpisodeFormat = "{season}x{episode}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("1x6"); } @@ -181,7 +191,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.SeasonNumber = 1; _namingConfig.StandardEpisodeFormat = "{season}x{episode:00}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("1x06"); } @@ -190,7 +200,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Quality Title}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("HDTV-720p"); } @@ -200,7 +210,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Quality Title}"; _episodeFile.Quality.Proper = true; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("HDTV-720p Proper"); } @@ -209,7 +219,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} [{Quality Title}]"; - Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) .Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]"); } @@ -219,7 +229,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.RenameEpisodes = false; _episodeFile.Path = @"C:\Test\TV\30 Rock - S01E01 - Test"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(Path.GetFileNameWithoutExtension(_episodeFile.Path)); } @@ -230,7 +240,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episodeFile.SceneName = "30.Rock.S01E01.xvid-LOL"; _episodeFile.Path = @"C:\Test\TV\30 Rock - S01E01 - Test"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("30.Rock.S01E01.xvid-LOL"); } @@ -253,7 +263,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); - Subject.BuildFilename(new List {episode2, episode}, new Series {Title = "30 Rock"}, _episodeFile) + Subject.BuildFileName(new List {episode2, episode}, new Series {Title = "30 Rock"}, _episodeFile) .Should().Be("30 Rock - S06E06-E07 - Hey, Baby, What's Wrong!"); } @@ -266,7 +276,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.Title = "Hello"; _episode2.Title = "World"; - Subject.BuildFilename(new List {_episode1, _episode2}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1, _episode2}, _series, _episodeFile) .Should().Be("South Park - S15E06-E07 - Hello + World"); } @@ -281,7 +291,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.AirDate = "2012-12-13"; _episode1.Title = "Kristen Stewart"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("The Daily Show with Jon Stewart - 2012-12-13 - Kristen Stewart"); } @@ -296,7 +306,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episode1.AirDate = null; _episode1.Title = "Kristen Stewart"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("The Daily Show with Jon Stewart - Unknown - Kristen Stewart"); } @@ -306,7 +316,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 0; - Subject.BuildFilename(new List {_episode1, _episode2}, _series, _episodeFile) + Subject.BuildFileName(new List {_episode1, _episode2}, _series, _episodeFile) .Should().Be("South Park - S15E06-07 - City Sushi"); } @@ -316,7 +326,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 1; - Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1, _episode2 }, _series, _episodeFile) .Should().Be("South Park - S15E06 - S15E07 - City Sushi"); } @@ -326,7 +336,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 2; - Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1, _episode2 }, _series, _episodeFile) .Should().Be("South Park - S15E06E07 - City Sushi"); } @@ -336,7 +346,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 3; - Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1, _episode2 }, _series, _episodeFile) .Should().Be("South Park - S15E06-E07 - City Sushi"); } @@ -348,7 +358,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Episode Title}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(title); } @@ -357,7 +367,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Release Group}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(_episodeFile.ReleaseGroup); } @@ -370,7 +380,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _episodeFile.SceneName = "30.Rock.S01E01.xvid-LOL"; _episodeFile.Path = @"C:\Test\TV\30 Rock - S01E01 - Test"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("30 Rock - 30.Rock.S01E01.xvid-LOL"); } @@ -387,7 +397,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); - Subject.BuildFilename(new List { episode }, new Series { Title = "30 Rock" }, _episodeFile) + Subject.BuildFileName(new List { episode }, new Series { Title = "30 Rock" }, _episodeFile) .Should().Be("30 Rock - S06E06 - Part 1"); } @@ -404,7 +414,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); - Subject.BuildFilename(new List { episode }, new Series { Title = "30 Rock" }, _episodeFile) + Subject.BuildFileName(new List { episode }, new Series { Title = "30 Rock" }, _episodeFile) .Should().Be("30 Rock - S06E06 - Part 1"); } @@ -419,7 +429,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .With(e => e.EpisodeNumber = 6) .Build(); - Subject.BuildFilename(new List { episode }, new Series { Title = "Chicago P.D." }, _episodeFile) + Subject.BuildFileName(new List { episode }, new Series { Title = "Chicago P.D." }, _episodeFile) .Should().Be("Chicago.P.D.S06E06.Part.1"); } @@ -434,7 +444,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .With(e => e.EpisodeNumber = 6) .Build(); - Subject.BuildFilename(new List { episode }, new Series { Title = "Chicago P.D.." }, _episodeFile) + Subject.BuildFileName(new List { episode }, new Series { Title = "Chicago P.D.." }, _episodeFile) .Should().Be("Chicago.P.D.S06E06.Part.1"); } @@ -443,7 +453,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{absolute:00}.{Episode.Title}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("South.Park.S15E06.City.Sushi"); } @@ -453,7 +463,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _series.SeriesType = SeriesTypes.Anime; _namingConfig.AnimeEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{absolute:00}.{Episode.Title}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("South.Park.S15E06.100.City.Sushi"); } @@ -463,7 +473,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _series.SeriesType = SeriesTypes.Anime; _namingConfig.AnimeEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("South.Park.S15E06.City.Sushi"); } @@ -473,7 +483,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _series.SeriesType = SeriesTypes.Anime; _namingConfig.AnimeEpisodeFormat = "{Series.Title}.{absolute:00}.{Episode.Title}"; - Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be("South.Park.100.City.Sushi"); } @@ -483,7 +493,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _series.SeriesType = SeriesTypes.Anime; _namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}"; - Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1, _episode2 }, _series, _episodeFile) .Should().Be("South Park - 100-101 - City Sushi"); } @@ -496,7 +506,7 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.StandardEpisodeFormat = "{Series Title} - {season:0}x{episode:00} - {Episode Title}"; _namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}"; - Subject.BuildFilename(new List { _episode1, }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1, }, _series, _episodeFile) .Should().Be("South Park - 15x06 - City Sushi"); } @@ -507,8 +517,62 @@ namespace NzbDrone.Core.Test.OrganizerTests _namingConfig.MultiEpisodeStyle = (int)MultiEpisodeStyle.Duplicate; _namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}"; - Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + Subject.BuildFileName(new List { _episode1, _episode2 }, _series, _episodeFile) .Should().Be("South Park - 100 - 101 - City Sushi"); } + + [Test] + public void should_include_affixes_if_value_not_empty() + { + _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}{_Episode.Title_}"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.S15E06_City.Sushi_"); + } + + [Test] + public void should_not_include_affixes_if_value_empty() + { + _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}{_Episode.Title_}"; + + _episode1.Title = ""; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.S15E06"); + } + + [Test] + public void should_format_mediainfo_properly() + { + _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MEDIAINFO.FULL}"; + + _episodeFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() + { + VideoCodec = "AVC", + AudioFormat = "DTS", + AudioLanguages = "English/Spanish", + Subtitles = "English/Spanish/Italian" + }; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.S15E06.City.Sushi.X264.DTS[EN+ES].[EN+ES+IT]"); + } + + [Test] + public void should_exclude_english_in_mediainfo_audio_language() + { + _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MEDIAINFO.FULL}"; + + _episodeFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() + { + VideoCodec = "AVC", + AudioFormat = "DTS", + AudioLanguages = "English", + Subtitles = "English/Spanish/Italian" + }; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.S15E06.City.Sushi.X264.DTS.[EN+ES+IT]"); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetSeriesFolderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetSeriesFolderFixture.cs index abe7bf78b..8757da319 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetSeriesFolderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetSeriesFolderFixture.cs @@ -27,7 +27,9 @@ namespace NzbDrone.Core.Test.OrganizerTests { namingConfig.SeriesFolderFormat = format; - Subject.GetSeriesFolder(seriesTitle).Should().Be(expected); + var series = new NzbDrone.Core.Tv.Series { Title = seriesTitle }; + + Subject.GetSeriesFolder(series).Should().Be(expected); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs index c62504f12..cdc1041e7 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests fakeSeries.RootFolderPath = @"C:\Test\TV"; Mocker.GetMock() - .Setup(s => s.GetSeriesFolder(fakeSeries.Title)) + .Setup(s => s.GetSeriesFolder(fakeSeries, null)) .Returns(fakeSeries.Title); var series = Subject.AddSeries(fakeSeries); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index c0f202af3..6ad451295 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic throw new NotSupportedException("Full season releases are not supported with Pneumatic."); } - title = FileNameBuilder.CleanFilename(title); + title = FileNameBuilder.CleanFileName(title); //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) var filename = Path.Combine(Settings.NzbFolder, title + ".nzb"); diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index bbc077dcd..3dfe01648 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; - title = FileNameBuilder.CleanFilename(title); + title = FileNameBuilder.CleanFileName(title); var filename = Path.Combine(Settings.NzbFolder, title + ".nzb"); @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole { foreach (var folder in _diskProvider.GetDirectories(Settings.WatchFolder)) { - var title = FileNameBuilder.CleanFilename(Path.GetFileName(folder)); + var title = FileNameBuilder.CleanFileName(Path.GetFileName(folder)); var files = _diskProvider.GetFiles(folder, SearchOption.AllDirectories); @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole foreach (var videoFile in _diskScanService.GetVideoFiles(Settings.WatchFolder, false)) { - var title = FileNameBuilder.CleanFilename(Path.GetFileName(videoFile)); + var title = FileNameBuilder.CleanFileName(Path.GetFileName(videoFile)); var historyItem = new DownloadClientItem { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 44bec3fbe..98d801144 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.MediaFiles public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series) { var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id); - var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile); + var newFileName = _buildFileNames.BuildFileName(episodes, series, episodeFile); var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); _logger.Debug("Renaming episode file: {0} to {1}", episodeFile, filePath); @@ -59,7 +59,7 @@ namespace NzbDrone.Core.MediaFiles public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) { - var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); + var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); _logger.Debug("Moving episode file: {0} to {1}", episodeFile, filePath); @@ -69,7 +69,7 @@ namespace NzbDrone.Core.MediaFiles public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) { - var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); + var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); _logger.Debug("Copying episode file: {0} to {1}", episodeFile, filePath); diff --git a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs index 6a81ee4c8..a960cca5f 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs @@ -85,7 +85,7 @@ namespace NzbDrone.Core.MediaFiles } var seasonNumber = episodesInFile.First().SeasonNumber; - var newName = _filenameBuilder.BuildFilename(episodesInFile, series, file); + var newName = _filenameBuilder.BuildFileName(episodesInFile, series, file); var newPath = _filenameBuilder.BuildFilePath(series, seasonNumber, newName, Path.GetExtension(file.Path)); if (!file.Path.PathEquals(newPath)) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3873058a0..88d5299eb 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -634,10 +634,10 @@ - - + + - + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7b9920f1a..e6b27419a 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -14,13 +14,11 @@ namespace NzbDrone.Core.Organizer { public interface IBuildFileNames { - string BuildFilename(IList episodes, Series series, EpisodeFile episodeFile); - string BuildFilename(IList episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig); - string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); + string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFilePath(Series series, Int32 seasonNumber, String fileName, String extension); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); - string GetSeriesFolder(string seriesTitle); - string GetSeriesFolder(string seriesTitle, NamingConfig namingConfig); - string GetSeasonFolder(string seriesTitle, int seasonNumber, NamingConfig namingConfig); + string GetSeriesFolder(Series series, NamingConfig namingConfig = null); + string GetSeasonFolder(Series series, Int32 seasonNumber, NamingConfig namingConfig = null); } public class FileNameBuilder : IBuildFileNames @@ -30,7 +28,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached _patternCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?\{(?:\w+)(?\s|\.|-|_)\w+\})", + private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", @@ -50,12 +48,12 @@ namespace NzbDrone.Core.Organizer public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?\s|\.|-|_)Title\})", + public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?\s|\.|-|_)(Clean)?Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex FilenameCleanupRegex = new Regex(@"\.{2,}", RegexOptions.Compiled); + private static readonly Regex FileNameCleanupRegex = new Regex(@"\.{2,}", RegexOptions.Compiled); - private static readonly char[] EpisodeTitleTrimCharaters = new[] { ' ', '.', '?' }; + private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, @@ -68,18 +66,16 @@ namespace NzbDrone.Core.Organizer _logger = logger; } - public string BuildFilename(IList episodes, Series series, EpisodeFile episodeFile) + public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) { - var nameSpec = _namingConfigService.GetConfig(); + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } - return BuildFilename(episodes, series, episodeFile, nameSpec); - } - - public string BuildFilename(IList episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig) - { if (!namingConfig.RenameEpisodes) { - if (String.IsNullOrWhiteSpace(episodeFile.SceneName)) + if (episodeFile.SceneName.IsNullOrWhiteSpace()) { return Path.GetFileNameWithoutExtension(episodeFile.Path); } @@ -87,42 +83,33 @@ namespace NzbDrone.Core.Organizer return episodeFile.SceneName; } - if (String.IsNullOrWhiteSpace(namingConfig.StandardEpisodeFormat) && series.SeriesType == SeriesTypes.Standard) + if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) { throw new NamingFormatException("Standard episode format cannot be null"); } - if (String.IsNullOrWhiteSpace(namingConfig.DailyEpisodeFormat) && series.SeriesType == SeriesTypes.Daily) + if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) { throw new NamingFormatException("Daily episode format cannot be null"); } - var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber).ToList(); var pattern = namingConfig.StandardEpisodeFormat; + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); + + AddSeriesTokens(tokenHandlers, series); + + AddEpisodeTokens(tokenHandlers, episodes); + + AddEpisodeFileTokens(tokenHandlers, episodeFile); + + AddMediaInfoTokens(tokenHandlers, episodeFile); - var episodeTitles = new List - { - sortedEpisodes.First().Title.TrimEnd(EpisodeTitleTrimCharaters) - }; - - var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); - - tokenValues.Add("{Series Title}", series.Title); - tokenValues.Add("{Original Title}", episodeFile.SceneName); - tokenValues.Add("{Release Group}", episodeFile.ReleaseGroup); - if (series.SeriesType == SeriesTypes.Daily) { pattern = namingConfig.DailyEpisodeFormat; - - if (!String.IsNullOrWhiteSpace(episodes.First().AirDate)) - { - tokenValues.Add("{Air Date}", episodes.First().AirDate.Replace('-', ' ')); - } - - else { - tokenValues.Add("{Air Date}", "Unknown"); - } } if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber > 0)) @@ -137,7 +124,7 @@ namespace NzbDrone.Core.Organizer pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, "{Season Episode}"); var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; - foreach (var episode in sortedEpisodes.Skip(1)) + foreach (var episode in episodes.Skip(1)) { switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) { @@ -158,12 +145,10 @@ namespace NzbDrone.Core.Organizer seasonEpisodePattern += "-" + episodeFormat.EpisodePattern; break; } - - episodeTitles.Add(episode.Title.TrimEnd(EpisodeTitleTrimCharaters)); } - seasonEpisodePattern = ReplaceNumberTokens(seasonEpisodePattern, sortedEpisodes); - tokenValues.Add("{Season Episode}", seasonEpisodePattern); + seasonEpisodePattern = ReplaceNumberTokens(seasonEpisodePattern, episodes); + tokenHandlers["{Season Episode}"] = m => seasonEpisodePattern; } //TODO: Extract to another method @@ -181,7 +166,7 @@ namespace NzbDrone.Core.Organizer pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, "{Absolute Pattern}"); var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; - foreach (var episode in sortedEpisodes.Skip(1)) + foreach (var episode in episodes.Skip(1)) { switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) { @@ -204,24 +189,17 @@ namespace NzbDrone.Core.Organizer absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; break; } - - episodeTitles.Add(episode.Title.TrimEnd(EpisodeTitleTrimCharaters)); } - absoluteEpisodePattern = ReplaceAbsoluteNumberTokens(absoluteEpisodePattern, sortedEpisodes); - tokenValues.Add("{Absolute Pattern}", absoluteEpisodePattern); + absoluteEpisodePattern = ReplaceAbsoluteNumberTokens(absoluteEpisodePattern, episodes); + tokenHandlers["{Absolute Pattern}"] = m => absoluteEpisodePattern; } } - tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); - tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); + var filename = ReplaceTokens(pattern, tokenHandlers).Trim(); + filename = FileNameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString()); - AddMediaInfoTokens(episodeFile, tokenValues); - - var filename = ReplaceTokens(pattern, tokenValues).Trim(); - filename = FilenameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString() ); - - return CleanFilename(filename); + return filename; } public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) @@ -240,10 +218,10 @@ namespace NzbDrone.Core.Organizer else { var nameSpec = _namingConfigService.GetConfig(); - seasonFolder = GetSeasonFolder(series.Title, seasonNumber, nameSpec); + seasonFolder = GetSeasonFolder(series, seasonNumber, nameSpec); } - seasonFolder = CleanFilename(seasonFolder); + seasonFolder = CleanFileName(seasonFolder); path = Path.Combine(path, seasonFolder); } @@ -297,160 +275,260 @@ namespace NzbDrone.Core.Organizer return basicNamingConfig; } - public string GetSeriesFolder(string seriesTitle) + public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) { - var namingConfig = _namingConfigService.GetConfig(); + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } - return GetSeriesFolder(seriesTitle, namingConfig); + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + + return ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers); } - public string GetSeriesFolder(string seriesTitle, NamingConfig namingConfig) + public string GetSeasonFolder(Series series, Int32 seasonNumber, NamingConfig namingConfig = null) { - seriesTitle = CleanFilename(seriesTitle); + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } - var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); - tokenValues.Add("{Series Title}", seriesTitle); + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - return ReplaceTokens(namingConfig.SeriesFolderFormat, tokenValues); + AddSeriesTokens(tokenHandlers, series); + + AddSeasonTokens(tokenHandlers, seasonNumber); + + return ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers); } - public string GetSeasonFolder(string seriesTitle, int seasonNumber, NamingConfig namingConfig) + public static string CleanTitle(string name) { - var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); - tokenValues.Add("{Series Title}", seriesTitle); - - var seasonFolder = ReplaceSeasonTokens(namingConfig.SeasonFolderFormat, seasonNumber); - return ReplaceTokens(seasonFolder, tokenValues); + string[] dropCharacters = { ":", ".", "(", ")" }; + + string result = name; + + for (int i = 0; i < dropCharacters.Length; i++) + { + result = result.Replace(dropCharacters[i], ""); + } + + return result; } - public static string CleanFilename(string name) + public static string CleanFileName(string name) { string result = name; string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; - string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" }; + string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" }; for (int i = 0; i < badCharacters.Length; i++) + { result = result.Replace(badCharacters[i], goodCharacters[i]); + } return result.Trim(); } - private void AddMediaInfoTokens(EpisodeFile episodeFile, Dictionary tokenValues) + private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) { - if (episodeFile.MediaInfo == null) - return; - - var mediaInfoFull = string.Empty; - - switch (episodeFile.MediaInfo.VideoCodec) - { - case "AVC": - if (Path.GetFileNameWithoutExtension(episodeFile.Path).Contains("x264")) - mediaInfoFull += "x264"; - else if (Path.GetFileNameWithoutExtension(episodeFile.Path).Contains("h264")) - mediaInfoFull += "h264"; - else - mediaInfoFull += "h264"; - break; - - default: - mediaInfoFull += episodeFile.MediaInfo.VideoCodec; - break; - } - - switch (episodeFile.MediaInfo.AudioFormat) - { - case "AC-3": - mediaInfoFull += ".AC3"; - break; - - case "MPEG Audio": - if (episodeFile.MediaInfo.AudioProfile == "Layer 3") - mediaInfoFull += ".MP3"; - else - mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; - break; - - case "DTS": - mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; - break; - - default: - mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; - break; - } - - tokenValues.Add("{MediaInfo Short}", mediaInfoFull); - - var audioLanguagesToken = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); - if (!string.IsNullOrEmpty(audioLanguagesToken) && audioLanguagesToken != "EN") - mediaInfoFull += string.Format("[{0}]", audioLanguagesToken); - - var subtitleLanguagesToken = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); - if (!string.IsNullOrEmpty(subtitleLanguagesToken)) - mediaInfoFull += string.Format(".[{0}]", subtitleLanguagesToken); - - tokenValues.Add("{MediaInfo Full}", mediaInfoFull); + tokenHandlers["{Series Title}"] = m => series.Title; + tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); } - private string GetLanguagesToken(string mediaInfoLanguages) + private void AddSeasonTokens(Dictionary> tokenHandlers, Int32 seasonNumber) { - List tokens = new List(); - foreach (var item in mediaInfoLanguages.Split('/')) - { - if (!string.IsNullOrWhiteSpace(item)) - tokens.Add(item.Trim()); - } - - var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); - for (int i = 0; i < tokens.Count; i++) - { - try - { - var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); - - if (cultureInfo != null) - tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); - } - catch - { - } - } - - return string.Join("+", tokens.Distinct()); + tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat ?? "0"); } - private string ReplaceTokens(string pattern, Dictionary tokenValues) + private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) { - return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenValues)); - } - - private string ReplaceToken(Match match, Dictionary tokenValues) - { - var separator = match.Groups["separator"].Value; - var token = match.Groups["token"].Value; - var replacementText = ""; - var patternTokenArray = token.ToCharArray(); - if (!tokenValues.TryGetValue(token, out replacementText)) return null; - - if (patternTokenArray.All(t => !Char.IsLetter(t) || Char.IsLower(t))) + if (!episodes.First().AirDate.IsNullOrWhiteSpace()) { - replacementText = replacementText.ToLowerInvariant(); + tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); + } + else + { + tokenHandlers["{Air Date}"] = m => "Unknown"; } - else if (patternTokenArray.All(t => !Char.IsLetter(t) || Char.IsUpper(t))) + tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes); + } + + private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + tokenHandlers["{Original Title}"]= m => episodeFile.SceneName; + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup; + tokenHandlers["{Quality Title}"] = m => GetQualityTitle(episodeFile.Quality); + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + if (episodeFile.MediaInfo == null) return; + + String mediaInfoVideo; + switch (episodeFile.MediaInfo.VideoCodec) + { + case "AVC": + // TODO: What to do if the original SceneName is hashed? + if (!episodeFile.SceneName.IsNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) + { + mediaInfoVideo = "h264"; + } + else + { + mediaInfoVideo = "x264"; + } + break; + + default: + mediaInfoVideo = episodeFile.MediaInfo.VideoCodec; + break; + } + + String mediaInfoAudio; + switch (episodeFile.MediaInfo.AudioFormat) + { + case "AC-3": + mediaInfoAudio = "AC3"; + break; + + case "MPEG Audio": + if (episodeFile.MediaInfo.AudioProfile == "Layer 3") + { + mediaInfoAudio = "MP3"; + } + else + { + mediaInfoAudio = episodeFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + mediaInfoAudio = episodeFile.MediaInfo.AudioFormat; + break; + + default: + mediaInfoAudio = episodeFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = String.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = String.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = String.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + tokenHandlers["{MediaInfo Video}"] = m => mediaInfoVideo; + tokenHandlers["{MediaInfo Audio}"] = m => mediaInfoAudio; + + tokenHandlers["{MediaInfo Simple}"] = m => String.Format("{0} {1}", mediaInfoVideo, mediaInfoAudio); + + tokenHandlers["{MediaInfo Full}"] = m => String.Format("{0} {1}{2} {3}", mediaInfoVideo, mediaInfoAudio, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private string GetLanguagesToken(String mediaInfoLanguages) + { + List tokens = new List(); + foreach (var item in mediaInfoLanguages.Split('/')) + { + if (!string.IsNullOrWhiteSpace(item)) + tokens.Add(item.Trim()); + } + + var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); + + if (cultureInfo != null) + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + } + catch + { + } + } + + return string.Join("+", tokens.Distinct()); + } + + private string ReplaceTokens(String pattern, Dictionary> tokenHandlers) + { + return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers)); + } + + private string ReplaceToken(Match match, Dictionary> tokenHandlers) + { + var tokenMatch = new TokenMatch + { + RegexMatch = match, + Prefix = match.Groups["prefix"].Value, + Separator = match.Groups["separator"].Value, + Suffix = match.Groups["suffix"].Value, + Token = match.Groups["token"].Value, + CustomFormat = match.Groups["customFormat"].Value + }; + + if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) + { + tokenMatch.CustomFormat = null; + } + + var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => String.Empty); + + var replacementText = tokenHandler(tokenMatch).Trim(); + + if (tokenMatch.Token.All(t => !Char.IsLetter(t) || Char.IsLower(t))) + { + replacementText = replacementText.ToLower(); + } + else if (tokenMatch.Token.All(t => !Char.IsLetter(t) || Char.IsUpper(t))) { replacementText = replacementText.ToUpper(); } - if (!separator.Equals(" ")) + if (!tokenMatch.Separator.IsNullOrWhiteSpace()) { - replacementText = replacementText.Replace(" ", separator); + replacementText = replacementText.Replace(" ", tokenMatch.Separator); + } + + replacementText = CleanFileName(replacementText); + + if (!replacementText.IsNullOrWhiteSpace()) + { + replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; } return replacementText; } + + private sealed class TokenMatch + { + public Match RegexMatch { get; set; } + public String Prefix { get; set; } + public String Separator { get; set; } + public String Suffix { get; set; } + public String Token { get; set; } + public String CustomFormat { get; set; } + } + private string ReplaceNumberTokens(string pattern, List episodes) { var episodeIndex = 0; @@ -531,17 +609,22 @@ namespace NzbDrone.Core.Organizer return null; } - private string GetEpisodeTitle(List episodeTitles) + private String GetEpisodeTitle(List episodes) { - if (episodeTitles.Count == 1) + if (episodes.Count == 1) { - return episodeTitles.First(); + return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); } - return String.Join(" + ", episodeTitles.Select(Parser.Parser.CleanupEpisodeTitle).Distinct()); + var titles = episodes + .Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Select(Parser.Parser.CleanupEpisodeTitle) + .Distinct(); + + return String.Join(" + ", titles); } - private string GetQualityTitle(QualityModel quality) + private String GetQualityTitle(QualityModel quality) { if (quality.Proper) return _qualityDefinitionService.Get(quality.Quality).Title + " Proper"; diff --git a/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs b/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs index 16b225131..055083d81 100644 --- a/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs +++ b/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs @@ -4,13 +4,13 @@ using System.Text.RegularExpressions; namespace NzbDrone.Core.Organizer { - public class FilenameBuilderTokenEqualityComparer : IEqualityComparer + public class FileNameBuilderTokenEqualityComparer : IEqualityComparer { - public static readonly FilenameBuilderTokenEqualityComparer Instance = new FilenameBuilderTokenEqualityComparer(); + public static readonly FileNameBuilderTokenEqualityComparer Instance = new FileNameBuilderTokenEqualityComparer(); private static readonly Regex SimpleTokenRegex = new Regex(@"\s|_|\W", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private FilenameBuilderTokenEqualityComparer() + private FileNameBuilderTokenEqualityComparer() { } diff --git a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs index c4231e533..80ca9083d 100644 --- a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.Organizer { @@ -16,7 +17,7 @@ namespace NzbDrone.Core.Organizer String GetSeasonFolderSample(NamingConfig nameSpec); } - public class FilenameSampleService : IFilenameSampleService + public class FileNameSampleService : IFilenameSampleService { private readonly IBuildFileNames _buildFileNames; private static Series _standardSeries; @@ -31,26 +32,26 @@ namespace NzbDrone.Core.Organizer private static EpisodeFile _dailyEpisodeFile; private static EpisodeFile _animeEpisodeFile; - public FilenameSampleService(IBuildFileNames buildFileNames) + public FileNameSampleService(IBuildFileNames buildFileNames) { _buildFileNames = buildFileNames; _standardSeries = new Series - { - SeriesType = SeriesTypes.Standard, - Title = "Series Title" - }; + { + SeriesType = SeriesTypes.Standard, + Title = "Series Title (2010)" + }; _dailySeries = new Series { SeriesType = SeriesTypes.Daily, - Title = "Series Title" + Title = "Series Title (2010)" }; _animeSeries = new Series { SeriesType = SeriesTypes.Anime, - Title = "Series Title" + Title = "Series Title (2010)" }; _episode1 = new Episode @@ -73,32 +74,52 @@ namespace NzbDrone.Core.Organizer _singleEpisode = new List { _episode1 }; _multiEpisodes = new List { _episode1, _episode2 }; + var mediaInfo = new MediaInfoModel() + { + VideoCodec = "AVC", + AudioFormat = "DTS", + AudioLanguages = "English", + Subtitles = "English/German" + }; + + var mediaInfoAnime = new MediaInfoModel() + { + VideoCodec = "AVC", + AudioFormat = "DTS", + AudioLanguages = "Japanese", + Subtitles = "Japanese/English" + }; + _singleEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv", - ReleaseGroup = "RlsGrp" + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfo }; _multiEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv", - ReleaseGroup = "RlsGrp" + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfo }; _dailyEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv", - ReleaseGroup = "RlsGrp" + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfo }; _animeEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), Path = @"C:\Test\Series.Title.001.HDTV.x264-EVOLVE.mkv", - ReleaseGroup = "RlsGrp" + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfoAnime }; } @@ -106,7 +127,7 @@ namespace NzbDrone.Core.Organizer { var result = new SampleResult { - Filename = BuildSample(_singleEpisode, _standardSeries, _singleEpisodeFile, nameSpec), + FileName = BuildSample(_singleEpisode, _standardSeries, _singleEpisodeFile, nameSpec), Series = _standardSeries, Episodes = _singleEpisode, EpisodeFile = _singleEpisodeFile @@ -119,7 +140,7 @@ namespace NzbDrone.Core.Organizer { var result = new SampleResult { - Filename = BuildSample(_multiEpisodes, _standardSeries, _multiEpisodeFile, nameSpec), + FileName = BuildSample(_multiEpisodes, _standardSeries, _multiEpisodeFile, nameSpec), Series = _standardSeries, Episodes = _multiEpisodes, EpisodeFile = _multiEpisodeFile @@ -132,7 +153,7 @@ namespace NzbDrone.Core.Organizer { var result = new SampleResult { - Filename = BuildSample(_singleEpisode, _dailySeries, _dailyEpisodeFile, nameSpec), + FileName = BuildSample(_singleEpisode, _dailySeries, _dailyEpisodeFile, nameSpec), Series = _dailySeries, Episodes = _singleEpisode, EpisodeFile = _dailyEpisodeFile @@ -145,7 +166,7 @@ namespace NzbDrone.Core.Organizer { var result = new SampleResult { - Filename = BuildSample(_singleEpisode, _animeSeries, _animeEpisodeFile, nameSpec), + FileName = BuildSample(_singleEpisode, _animeSeries, _animeEpisodeFile, nameSpec), Series = _animeSeries, Episodes = _singleEpisode, EpisodeFile = _animeEpisodeFile @@ -156,19 +177,19 @@ namespace NzbDrone.Core.Organizer public string GetSeriesFolderSample(NamingConfig nameSpec) { - return _buildFileNames.GetSeriesFolder(_standardSeries.Title, nameSpec); + return _buildFileNames.GetSeriesFolder(_standardSeries, nameSpec); } public string GetSeasonFolderSample(NamingConfig nameSpec) { - return _buildFileNames.GetSeasonFolder(_standardSeries.Title, _episode1.SeasonNumber, nameSpec); + return _buildFileNames.GetSeasonFolder(_standardSeries, _episode1.SeasonNumber, nameSpec); } private string BuildSample(List episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) { try { - return _buildFileNames.BuildFilename(episodes, series, episodeFile, nameSpec); + return _buildFileNames.BuildFileName(episodes, series, episodeFile, nameSpec); } catch (NamingFormatException) { diff --git a/src/NzbDrone.Core/Organizer/FilenameValidationService.cs b/src/NzbDrone.Core/Organizer/FilenameValidationService.cs index 5c2703cc1..13d91aca6 100644 --- a/src/NzbDrone.Core/Organizer/FilenameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FilenameValidationService.cs @@ -14,14 +14,14 @@ namespace NzbDrone.Core.Organizer ValidationFailure ValidateAnimeFilename(SampleResult sampleResult); } - public class FilenameValidationService : IFilenameValidationService + public class FileNameValidationService : IFilenameValidationService { private const string ERROR_MESSAGE = "Produces invalid file names"; public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.Filename); + var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.Filename); + var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -67,7 +67,7 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.Filename); + var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs index 928438e8c..8966646e0 100644 --- a/src/NzbDrone.Core/Organizer/SampleResult.cs +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Organizer { public class SampleResult { - public string Filename { get; set; } + public String FileName { get; set; } public Series Series { get; set; } public List Episodes { get; set; } public EpisodeFile EpisodeFile { get; set; } diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index f48e900b7..5d765dfd4 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Tv if (String.IsNullOrWhiteSpace(newSeries.Path)) { - var folderName = _fileNameBuilder.GetSeriesFolder(newSeries.Title); + var folderName = _fileNameBuilder.GetSeriesFolder(newSeries); newSeries.Path = Path.Combine(newSeries.RootFolderPath, folderName); } diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index 33c76d757..10280503d 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -48,6 +48,7 @@ {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} {{> QualityTitleNamingPartial}} + {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} {{> OriginalTitleNamingPartial}} {{> SeparatorNamingPartial}} @@ -79,6 +80,7 @@ {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} {{> QualityTitleNamingPartial}} + {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} {{> OriginalTitleNamingPartial}} {{> SeparatorNamingPartial}} @@ -110,6 +112,7 @@ {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} {{> QualityTitleNamingPartial}} + {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} {{> OriginalTitleNamingPartial}} {{> SeparatorNamingPartial}} diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.html new file mode 100644 index 000000000..49203cafc --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.html @@ -0,0 +1,11 @@ +
    • diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html index f2cd5fd81..cc76c95b5 100644 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html +++ b/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html @@ -4,5 +4,8 @@
    • Series Title
    • Series.Title
    • Series_Title
    • +
    • Series CleanTitle
    • +
    • Series.CleanTitle
    • +
    • Series_CleanTitle
    From 4ad28bb6f687a987f73c4220a2d4999e77783bbc Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Aug 2014 13:51:06 -0700 Subject: [PATCH 047/105] Pushover default to Normal --- src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs b/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs index 2bd17b79d..0b0276e85 100644 --- a/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs +++ b/src/NzbDrone.Core/Notifications/Pushover/PushoverSettings.cs @@ -18,6 +18,11 @@ namespace NzbDrone.Core.Notifications.Pushover { private static readonly PushoverSettingsValidator Validator = new PushoverSettingsValidator(); + public PushoverSettings() + { + Priority = 0; + } + [FieldDefinition(0, Label = "API Key", HelpLink = "https://pushover.net/apps/clone/nzbdrone")] public String ApiKey { get; set; } From 1586743df26ce410c2bfa4078ae724f269780eab Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Aug 2014 17:51:40 -0700 Subject: [PATCH 048/105] New: Show calendar title above buttons on small screens (phones) --- src/UI/Calendar/CalendarView.js | 12 ++++++++++++ src/UI/Calendar/calendar.less | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index 1c26d1908..eee12dc9f 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -33,6 +33,18 @@ define( }, _viewRender: function (view) { + + if ($(window).width() < 768) { + this.$('.fc-header-title').show(); + this.$('.calendar-title').remove(); + + var title = this.$('.fc-header-title').text(); + var titleDiv = '

    {0}

    '.format(title); + + this.$('.fc-header').before(titleDiv); + this.$('.fc-header-title').hide(); + } + if (Config.getValue(this.storageKey) !== view.name) { Config.setValue(this.storageKey, view.name); } diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 82525175c..6eeb72fe9 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -185,3 +185,12 @@ cursor : text; } } + +.calendar-title { + text-align : center; + + h2 { + margin-top : 0px; + margin-bottom : 5px; + } +} From 15f9c3a95dfd80c47e7892c836748000de6d5215 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Aug 2014 23:17:44 -0700 Subject: [PATCH 049/105] Removed some extra using statements --- src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index 6b0b7bb01..1818cc0a4 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -2,10 +2,6 @@ using System.Collections.Generic; using System.Linq; using NLog; -using Newtonsoft.Json.Linq; -using NzbDrone.Common; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Tv; @@ -82,8 +78,6 @@ namespace NzbDrone.Core.Notifications.Xbmc { var seriesPath = GetSeriesPath(settings, series); - JObject postJson; - if (seriesPath != null) { _logger.Debug("Updating series {0} (Path: {1}) on XBMC host: {2}", series, seriesPath, settings.Address); From 75236118dd08554a18d50395304f4efc8ea83faa Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Aug 2014 23:41:43 -0700 Subject: [PATCH 050/105] Wait 30 seconds for automation tests to load UI --- src/NzbDrone.Automation.Test/PageModel/PageBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs index b8e56a44d..93c804703 100644 --- a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs +++ b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Automation.Test.PageModel } - public void WaitForNoSpinner(int timeout = 20) + public void WaitForNoSpinner(int timeout = 30) { //give the spinner some time to show up. Thread.Sleep(100); From be0d779448c3aa4176e9881f117cb28200e44f13 Mon Sep 17 00:00:00 2001 From: Frank Riley Date: Wed, 6 Aug 2014 19:34:49 -0700 Subject: [PATCH 051/105] When running under mono, WebClient will sometimes return less data than it should. This causes the FetchFeedService to log errors because the XML received cannot be parsed. Setting the AutomaticDecompression property on the WebRequest fixes this issue. --- src/NzbDrone.Common/Http/HttpProvider.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Common/Http/HttpProvider.cs b/src/NzbDrone.Common/Http/HttpProvider.cs index 6b04cb548..b122b9464 100644 --- a/src/NzbDrone.Common/Http/HttpProvider.cs +++ b/src/NzbDrone.Common/Http/HttpProvider.cs @@ -22,6 +22,16 @@ namespace NzbDrone.Common.Http public class HttpProvider : IHttpProvider { + private class GZipWebClient : WebClient + { + protected override WebRequest GetWebRequest(Uri address) + { + HttpWebRequest request = (HttpWebRequest)base.GetWebRequest(address); + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + return request; + } + } + private readonly Logger _logger; public const string CONTENT_LENGTH_HEADER = "Content-Length"; @@ -49,7 +59,7 @@ namespace NzbDrone.Common.Http { try { - var client = new WebClient { Credentials = identity }; + var client = new GZipWebClient { Credentials = identity }; client.Headers.Add(HttpRequestHeader.UserAgent, _userAgent); return client.DownloadString(url); } @@ -107,7 +117,7 @@ namespace NzbDrone.Common.Http _logger.Debug("Downloading [{0}] to [{1}]", url, fileName); var stopWatch = Stopwatch.StartNew(); - var webClient = new WebClient(); + var webClient = new GZipWebClient(); webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgent); webClient.DownloadFile(url, fileName); stopWatch.Stop(); From c3fee509f60ba6255c92a916197f1e732729a704 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 6 Aug 2014 23:23:13 -0700 Subject: [PATCH 052/105] More info on calendar New: Show more information of grabbed and failed downloads on calendar --- src/UI/Calendar/CalendarView.js | 33 +++++++++++++++++++------ src/UI/Calendar/calendar.less | 5 ++-- src/UI/History/Queue/QueueStatusCell.js | 9 +++++-- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index eee12dc9f..afd900920 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -60,15 +60,24 @@ define( var progress = 100 - (event.downloading.get('sizeleft') / event.downloading.get('size') * 100); var releaseTitle = event.downloading.get('title'); var estimatedCompletionTime = moment(event.downloading.get('estimatedCompletionTime')).fromNow(); + var status = event.downloading.get('status').toLocaleLowerCase(); + var errorMessage = event.downloading.get('errorMessage'); - if (event.downloading.get('status').toLocaleLowerCase() === 'pending') { - this.$(element).find('.fc-event-time') - .after(''); + if (status === 'pending') { + this._addStatusIcon(element, 'icon-time', 'Release will be processed {0}'.format(estimatedCompletionTime)); + } - this.$(element).find('.pending').tooltip({ - title: 'Release will be processed {0}'.format(estimatedCompletionTime), - container: 'body' - }); + else if (errorMessage) { + if (status === 'completed') { + this._addStatusIcon(element, 'icon-nd-import-failed', 'Import failed: {0}'.format(errorMessage)); + } + else { + this._addStatusIcon(element, 'icon-nd-download-failed', 'Download failed: {0}'.format(errorMessage)); + } + } + + else if (status === 'failed') { + this._addStatusIcon(element, 'icon-nd-download-failed', 'Download failed: check download client for more details'); } else { @@ -218,6 +227,16 @@ define( }; return options; + }, + + _addStatusIcon: function (element, icon, tooltip) { + this.$(element).find('.fc-event-time') + .after(''.format(icon)); + + this.$(element).find('.status').tooltip({ + title: tooltip, + container: 'body' + }); } }); }); diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 6eeb72fe9..a383a6126 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -22,7 +22,7 @@ .fc-event { .clickable; - .pending { + .status { margin-right : 4px; } } @@ -173,8 +173,7 @@ } } -.ical -{ +.ical { color: @btn-link-disabled-color; cursor: pointer; } diff --git a/src/UI/History/Queue/QueueStatusCell.js b/src/UI/History/Queue/QueueStatusCell.js index 2fda4b6ea..c85b2238a 100644 --- a/src/UI/History/Queue/QueueStatusCell.js +++ b/src/UI/History/Queue/QueueStatusCell.js @@ -37,14 +37,19 @@ define( title = 'Pending'; } + if (status === 'failed') { + icon = 'icon-nd-download-failed'; + title = 'Download failed: check download client for more details'; + } + if (errorMessage !== '') { if (status === 'completed') { icon = 'icon-nd-import-failed'; - title = "Import failed"; + title = 'Import failed'; } else { icon = 'icon-nd-download-failed'; - title = "Download failed"; + title = 'Download failed'; } this.$el.html(''.format(icon)); From b4698b070ca91cb54a1e5441394040d59f6a6bb3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 7 Aug 2014 08:24:23 -0700 Subject: [PATCH 053/105] Fixed: Update IMDB ID from trakt when series is refreshed --- src/NzbDrone.Core/Queue/QueueService.cs | 4 +--- src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index bf36068de..0f1983ad6 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -14,12 +14,10 @@ namespace NzbDrone.Core.Queue public class QueueService : IQueueService { private readonly IDownloadTrackingService _downloadTrackingService; - private readonly Logger _logger; - public QueueService(IDownloadTrackingService downloadTrackingService, Logger logger) + public QueueService(IDownloadTrackingService downloadTrackingService) { _downloadTrackingService = downloadTrackingService; - _logger = logger; } public List GetQueue() diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 15a694fad..b7b4563b0 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -55,6 +55,7 @@ namespace NzbDrone.Core.Tv series.Title = seriesInfo.Title; series.TitleSlug = seriesInfo.TitleSlug; series.TvRageId = seriesInfo.TvRageId; + series.ImdbId = seriesInfo.ImdbId; series.AirTime = seriesInfo.AirTime; series.Overview = seriesInfo.Overview; series.Status = seriesInfo.Status; From 3991ae5748e85c75b40379de1b4f91e05e731712 Mon Sep 17 00:00:00 2001 From: Frank Riley Date: Fri, 8 Aug 2014 15:14:50 -0700 Subject: [PATCH 054/105] Move grid below show/hide button so that the button does not move when the grid is shown --- src/UI/Series/Details/SeasonLayoutTemplate.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/Series/Details/SeasonLayoutTemplate.html b/src/UI/Series/Details/SeasonLayoutTemplate.html index ff5e8b95a..dae85be8c 100644 --- a/src/UI/Series/Details/SeasonLayoutTemplate.html +++ b/src/UI/Series/Details/SeasonLayoutTemplate.html @@ -31,7 +31,6 @@
-

{{#if showingEpisodes}} @@ -43,4 +42,5 @@ {{/if}}

+
From e752e7f35d3a3d81013ed84ab3bf4b13fab09aab Mon Sep 17 00:00:00 2001 From: Frank Riley Date: Thu, 7 Aug 2014 22:18:22 -0700 Subject: [PATCH 055/105] Fixed several issues in GrowlService: 1) Calls to the growl SDK were not made in a thread safe manner 2) Loading the icon from a URL did not work in mono, and it made the GrowlService calls take exceptionally long 3) The result of the growl SDK calls was not checked 4) A bug in the growl for windows SDK caused the mono version on unix platforms to not work. A fixed version of the SDK is now included in the build (do not use the NuGet version, it's broken on unix). The bug was reported to growl for windows here: https://code.google.com/p/growl-for-windows/issues/detail?id=48 use var for locals made requested changes from code review Gave state class a better name --- src/Libraries/Growl.Connector.dll | Bin 0 -> 50688 bytes src/Libraries/Growl.CoreLibrary.dll | Bin 0 -> 17408 bytes .../Notifications/Growl/GrowlService.cs | 112 +++++++++++++--- src/NzbDrone.Core/NzbDrone.Core.csproj | 27 +++- .../Properties/Resources.Designer.cs | 73 +++++++++++ src/NzbDrone.Core/Properties/Resources.resx | 124 ++++++++++++++++++ src/NzbDrone.Core/Resources/64.png | Bin 0 -> 3582 bytes src/NzbDrone.Core/packages.config | 1 - 8 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 src/Libraries/Growl.Connector.dll create mode 100644 src/Libraries/Growl.CoreLibrary.dll create mode 100644 src/NzbDrone.Core/Properties/Resources.Designer.cs create mode 100644 src/NzbDrone.Core/Properties/Resources.resx create mode 100644 src/NzbDrone.Core/Resources/64.png diff --git a/src/Libraries/Growl.Connector.dll b/src/Libraries/Growl.Connector.dll new file mode 100644 index 0000000000000000000000000000000000000000..ba848ae440a489b7b6c8eae8af9a1f9551269e9a GIT binary patch literal 50688 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 NS{3|X?z4=6e*igmpELjf literal 0 HcmV?d00001 diff --git a/src/Libraries/Growl.CoreLibrary.dll b/src/Libraries/Growl.CoreLibrary.dll new file mode 100644 index 0000000000000000000000000000000000000000..5e67e08f40c03cb8e76163e4d0824db30faa2486 GIT binary patch literal 17408 zcmeHueRLGpk#F_wnVt`gMw$@_FanRvhX)}E3B-r6!3asf*a&1KFvK`8l3LQ3(X`w> z0t5jIu)V?9CQf2<$a4Igwd2H2)(QE*@n*d*_Dje*8}G*CWZ&9xNE|!w#c{k&cKxzR z*4eyYRrg54N1Wum{ITzxZJ6p?b?e?+w{BJ4y4|gg_k4hKBJ$z)+uss>33t9$34C`j zhU(~*e-))Kgr2YdlD7W&>h8gelNh$G0XsdE=uPKxRzA^VChU=1B9lwBZR|=6S$$@G zX=!-2*L7z*(RxiMtL3$>!e~DrRb8T$5M2w3;ZhI22loViJMkkbm$Gu@W&+DE*F%7y zKl%n?UaRh8R{lS6_fRI`N}^vGcqiu0in#b1BZ`3bv#mr`7uUTTo*)Vq)dzvEE5hsZ z=H5K;JC*~WPUZ@^i?0M`TW>pdFNh-BP84{eEAcD3R$;m7Z8K|uk!7VZ{J5{x_!V8N zE}>um)+;WSO_85ot|gj;Ur4AEpflliJ3+a8>G~_)yX=A8=Kp->`Wxpw+I;p@$XEJ* z9(v+~&z_yKW7}7@F8kZ6FTQvEKKtCNn$uIByy{f;%#EXS9=P?EdtW%#pS|bqzkL6z zBO^;cW+Y4Q`PQ9dfAi*7J{kE*{e3CpJ@dZ&sgrO0)vLdbe))#&O+Re#?S5v<&+)~k?r~;yCM6qO?D6A}?;jodMgj)?2*Q?}Yt`auG zs^t3RN>5_yhMt6;jhZbgITd83AAo82lWg7OL8SvI0E8Nj3G!tH3zMa?~tQ`~A4<)+}y2j?HH5R=u{;trmUhqE<_)Sf}Vj zbgn1ofIF=A!*6}HhLM~L)NNZ~>gsfq! z5ojrlBqVDcHfpyS$%UZR-l(P=i1M_IqT_IdPsM!J z5+=snW=pvY_wji>oe$4dmD1;Vs$qMAz;x_>3PTOrOvQF+u{Q^!@*i3Xf*?S*{J*vZNg@&NX?`$kH$6zRq%EI-SBX= za}}GeVgF@eMq&FI6ibYNgP;mUC9lD4E{nx8=Q~fC?>tMs^9*aQg3qr2XAO_>K8%5N zjwuU4He45(WPb;JI@bZ&r;@91t5uU~#2v%)+X62{f>)L*}D27VuQ~V!jtD z{4xKP;YFxg4bt2xrSsJkUM|CRP0YBJ>2CF$lFEkDUJb{I6y(7E5t|fNwW}*5YEEff zNr-UT(5NJUB?~0mQMA?o1gy1y;q#H0k7I_bN>OXbjUTWsEEGbCqAO_f0(YC>n6PAp ztKaFEJ3NNtzKl6TRXW%cczFUz_=yoNUZwN;-1BU)g+9wDG}Y~gN4RP>>1-F%8+>#O zJXk>PGl@Bc>-H(hRb+t&L-5ta|1yOo(n#f zL^P-&obiqosw(Jj9!+m^$K;y1Nvd{rsS{k61&irPC?i}~u3Mml>w>zq1@~Itl+yD{ zRT3U&gyH!JKSqh}-Xj?JCh(M=ubgYgG;BIR#ATsj?;{YbkzBv|}u-8@uDl3BXZ;fFcK3Wp6ZU)iX z4iE`ix8PP0n%@`;S?@)$G-U0-tuAnICvNkhJ4P#f2f4S9l?DQH?%`XA)0=SJS*$5< zjGmTb*z&OY!B_yh-hHU^v(30hFP$i>56x;L9%2Hik-=&_f7n1P?*xgobPih+gch9N&I-oeqzX+&HGfI?rT!F!Z4kBd#TL z=*Hb~Ebef8R^K;jR;%PNXm!{kZ^M1A1k)yzkV$!|6Gthd2t}lltpb|x)vUz4xS%E- zz@8xUkvr450J`w4k?3LYI9L^!uq~)0yxDM& zI~ZZZ5uSx^g(U+Q5S9q*p<_K@;c}_k?P@>rK40A`Wg$=+wNtTU^9&_XOU?u708&KX z{AtQ!71YL*l3tVO$*sa(%{{37%!irFT>_Jv2XdbBU5tiZFNOeM^J3aQMZ>!6NU|%J zOoGN7)aX_ouryRRSinbsSNQC8;|<`M)u2fvDY3r*dc?4HgJSIgs9jKmj382J9I0!k z+08l)$Nbh_@ZdID=mGl-N28#-P2kR$F@n&hRT$U`=EjUlYacf&FaeA zncM(0Kr}l+^tTvf*5)qy=`8o$IBV_Z6xNd70X`K*HEVU2!;@SJqTY1NnNTPU^w)D1 z5nPi;zX?Cs2w`!k8$Y-sVMsaJa>!*Ktcl#Y?86Tsf}j8?GYw*l*JE;0<{nmPOEiPp z1f~R<+RdQYQoa)~Odr*bgYu&G*MPlhoigZc#jP}*;V!^1ozRcy20iP$S&PyuzH>f< z=K0sDFfH*PM{Pxb;miQ@|1@weV9=DHKWI?Bz_kMV1RfE1O5l?M&j}2N-T?nhfwduS z+b*z2;QK>I^ka02y^Q`Y^r?_RZwQQ(>Obmxbp!q5n9{ zv-_=-BPHCbQQ-0t<{v7#rNp4!0{^0fwe*C*ua>Yp?+A~Iur?|qNAxgV4QS9}Pz>54 z@cj|i&!-~~L=5@`%Ftmc!?3`~fKh5IJ?D$k4S-=fUHYNYFuf#@q8}? zLO7>-*lOvs8rTes-9!6xP^+qj53IFYYtNe#S`G%kQzw3V- z@Q3~+~>9Z3nj1!@`O=vx1$VsVWWZZ5Ip9QUh35?J6Gc{;B^M zUZe-9RIt0~u(}f`jOpXpTG+t6aqMoyn6`23IP%{7IQC)K!z1I^Nvz&E4?7CUqV#>i zPS6L{gTUT$>oD7gk(HDqZbC1?Z>nd2wRzY-0xPFIE=I4auS>k-sK;9RrbI`^&T4O} zy9v1=F2>6AbFiZVHeLTNEax8Sh1)$0D{Gt0wRZ(Sr!Ur~(RvSiR1={_)hjsW?1Jmco=#XVIu!vx1={dB! zf<7$R3Hsl@KJ5w$&SlOM^tNx8Hl4P6nC`c6XiHwrb@x-bf3G%!J}KAJ6w+I`i3Kd{+!OzO_A z`mFzPZ4TWh*d3zJIdsBp2kZ>$o^-L`oBnQi@qhO)WjqZ}{kn&h8qWgbtFW2{j92p! zP1wBEd{IL(Bb;Y7Ud=lCmQx?kf(@#C@MqmplD9ra$+IUrKpz8!XK|{vZX(4rb z*b(C=+9KK}*da)LH=@qqIQC0z3GET=uHX|!H!Y>xJ?xBeH?ZR#rm6qXmeEH%>~D-; z0{euAecf2BHPcfb_JUw%J?#5}{gYs4>2-|IOmDk&^e=+da?%Zdn5A^pOiklhNG+#p z$FXvC4ecDqrl@OaXdIiNR?z##vDs=R{Zz28(p7;5bsc@1Q(f_+hiMf`8Z`ta;A9rvVc6+sCHG!VLGZEF>L*QZB5BR{iwF3#MV!0hf7ZCAJgtDc@Xeq z$;b7_v`>^guAkOEUxMrj{6Ep2(q1U}jQ*7NO34Z>LvNOx(Vu~Y-_W1aqLGK`1(aXb zU(^;veyG2K^4(O85jShqv^KKU_qxO}r`l(v<=2H01+SUXC40Yh|8=}*w}?(kc_NurrX^=xU#->808 zTJCRFbEB317G*}O{8i*c8~it_+oOy9N9msEa{o5Nic{iO_v&xsM(`se;4~?hP z#`5Fzl-dDGvy2r*MiWPtk)S64XVd2alX%A%r3QKhu!+jGdb)=81nQAt{L-jLHn|Gr z=>libO(@S7xQK2+c^5qoc#QrHuuJGS)59pYO4*^8P~J~J0z5+h9C(ag)&3P3{PT3D z|L5vz{R8@6>*w@0^giD~--mpk_x+Ra7rrik-v7AqMdPc+j}2t=h>vGg72=ZyN&S-f zmh-a|TA=V01aD4oXr^X`LoTfp$}PUNXp|8+BJd6$bACkNqrMwZd&buZSYsu|`T%YN&)Ane6Up7mt(~h~M>C#GCoymcxEuBx(>e0NpdL)zWGi_>3 zXR|%&-d&qaXC#}aHnV4BAeHO4=q5XpH`{lcx%|3xt}hGLHZyPb<}I7nZs_jB@!w1j zZ8Fo`NopuPV76Mh-KL#qv~zPllg;Earb8R7e5OCso6cvfTw$y>Giwf{^X6qMWylsA zPZloM^a6o8)A>Pnq)~$W)`;C}(oJShYc>P1+xOCt(`(t;Ob>O9I(c)5Hul_#8Blv} zWQaO2UC8C+$E9(Z>Qfu(g2{EHZD%l@T|K(V>@Re{Y7rT|HXF?R#K7pk7jN&OFS(`K zcC6XBsiURalcK)M>>Yt}N9$Yd(cwIQ128-|N*!&BX{c{8W%{U(7pY!UJz(Z{;5iE4 z)|%E0bfLuQhX~4V{A78s3o_8qQ{VGkF5)wUaifwgSH=c&nAoLM2!n zr(U|q^wRo_li%4$J+KlkQpQ=I$>+0Xd#*2&&QZ(ou$k+_fO)nfhYRkrAa|e^r)$u% z^Rzpi9Wi(8pw{)N)*ApfcC}M?s-wMabB!gw(3f$Bv*}UCL${c=1I19}Ub^RM(bC59BNdM&!_%OxE@CvKOoFqQNd6sCW?9x!|zrK3U{~ zI_>lxp1Z^n7j|2&g$ZemZ4F(7Znm>7BiDJ|vZbuIdUsJ(vvm#}(l zMJ$+BSGwPHEqntsAnwZ)SZpya<@TI2Vw-IlTjKs``(A_thgz+n9`?==s6WGzpVC=u z27RN29iR{6y1|$Q$W3Lj>C~zUKwyPu0{^8_o z9ra!qVOQuzcV>PKz7aFU5c7EC-ZO%Iv(Uus_kuSS9IiWqK;1IVSRi-WNE&+cmsYfy zPOqIA<_+M|N~~vpW?%%{%I~Ni8cyd%FV}8()XoeHUS_;*8wtZu8eVmYjIA(!SldKJ zVV0g^#L=u_&wa!Uw`80QLh3}5wIi9n39K-CPoA(?wRdr23PC=GBx zpJ?gQuyRPzurXdFKip;*PG%QR00bFelbKELm69{joo5l;4plvzQ57lM>%E&lkCTLh zEk^vUd(ATVs3c?XA~zxQd`W_+WQY^a<&_|o#HkCXRA?ufD9osPklmff>#%Ye&IsEj z2U(YP23xWN7LuF6Ar2G6FjgiKB1fLHDzqsio*c^|-g>x*jbul%omTV%*bn@A7dC&4YB8@uI5F5wnUf`9j7ck9Zh z?Yyvh1n3+Toz)sM*(z|8PSv}I<&qoDk!$>x}(QY3a&X0O~0ehts zWlt6rGRYf@i|k*b=ayccwiLS%W|4DSdvaMT-Pgt`H19f{mV*r#SzJ+$03#Yo4^yfa zWH^Y2IqNNJn2$o97^%+POOT-K5-EykT{suHwD#LZ(%Er8N4ik&r3^)(gT_fn6Z+HG z_hAk#+3dw+Zj#+@gT*;|A+;)!MA*E63qfN-x(g&h2-$6J;2;LS z&Zm);vorCKDL%1560aXO_rswJVSx{K1rzmbZ~RczY1%^>hZA?KjCazSpiOu{p{_;J zMA35&i9)824U9e0-M1OS;&Cp((v7?EL`l5Ojaw77DI_3Rv%T&_+)~O}JvyAmLdem{ zqoH9RW`n@%xN0uKJqwv&5e^PJ5;s^yZPpMpM(gtV;o_7BH8+^%a7#7=U-PybIrm8q z7ZBJ*I?{VnIN*09`w*9D-#d(aud$(lS0s}|NaL$+FWxc?kWCitp)ATafgRWgO1&sA z$D4))c&gMu2T1ep#k&QX(M22av;^lpJP+N3Cn?+U1f|de9)rZ%@ZJ=;5&BEx=^#EP zn7kHci+X@(!R_`&3np5vL9LDcGL)k}k;S1M=z9csPN)O;C}4uW9+K_Fk17^G$|1B_ zj&TO1C0)~vl1T|^-G`EmQAfRA65z{&g7g;c5+Z2=V~l_YUqp-@ZAB-H^r13`avpc? zBrhEeNjKcN1Fjx$a7{h%_2BK52_^t>+?PQd@GVEJJLLovoWQh949z`rKP{KxZDeH+ ztVewgi<8H69B^9H517IW>x)NVoE3MWR0)iNaEZr;JOc}`FdwGlNP>Rr@g!@5)D1&k z3rZaYAFJm!%r7G)?l&(yX|!RZ;92ntc6U}+leVk#|Dm<3l@avG%Rhjgi)CR+-LY6J z7;EfE@nC6)$qHgs;h}~s0pBg)6BE94LTqn^)h`~Jm%1P7VH4nidp#8t`ezGGL#Rk{S2ZE&`#3~24To#a95f#g1Ql#B#%RV8p07i$HHS^ zBVOevO^a87QH%O9eAU!oLXXdj#_OtBrue-2iCFN5#EzQdNa1NLUZ#zu9;Ejahp?IQZz9 zYo@*R;)Z{D=t2F+mjCv%rQiMb7Y}Z}_X}Sy&wONG$s@Ndy?O8cx_@uq*YFotzIo&W zcU}9)reIUU7k}A#=H)ZjxBAvq&41sr#KZZ|{PV!udv5ETwdTxgH($N&BR8ykaQoli zb8q!C&Girb`46wZ_lc1w>1jW{8yh=`DbMgneE~pB4@D4QnxYq0l2%pDQ<~wQ;rGFu zsxU{^v=$#b9W4ofib7Dq-Z3dmEMDyw`(;g5P#AD>eC#O{rE`2J1?wAoMi4__*bjfG z;5QNRv9G!RBjuVtlgek})x=Ev2?Y!}7|`QY{Lg2^$6k=pUtq7Oib5tOt2Fk#DEf;p zj+O@eQ83|uRebDwfrJi*auo{b7~mEBXMc!BgWw%|Gd}iuAfQLV|9YQBJNmq5`SnGQ z{1Ss{2Y-cT<`TA>@AM`1;3StQ;vIi#S)b_4nrX*Oj12eT$dUhXa_)>7cHHFhZ=vZ-Njk-i!ifz-s-Kf-j8yimiHvUxlFpM)AKi;nSkAL;l#I zQ0!2tEKa;*Y9NIAjyPVWwn=L#fty!?Et}$pVkH3|m{!COO%`K@Cqan{%M2Yl5ag=> znh1osC;4BEwiu;~3FCcWbT+aX_?iw9#}2JHG^Jh}yW?Fl+3|M1=JHbEF4v29RTSq%MbFZu>BYT^mozpnYBC#^HILn^ zb#K1?kCS9gHa+ks6~fOwI{46uym05k+UV@KI!qctED-QT^%*+*So~0mT^do0m%W1* zBHqDv*AWK-(v1RdDGLPRDeP}-H=E;!w&DHZ74;k1yT_ld)fRNeU%zV99i22@#phzY z7vQwJoZBSEYZ6VB$YL*f6Nisxd5@xL8co8xETmMu#6*DY!6O)p#0Z}v1cF2l#W z8U+?&;hXCl@$IWdenIdDGMe{B5(RvVi)$u_y7&m^L!lNB)k0Zk4qharF zO*x`hffm_@-!&ZC+q&j;T~&Gf&Qy}$?e)MigRJ{i_GnKLc1N0#5llQT3bKd*TYlz&v|v? zFFOiXj?0MxcHzD7_oNfl9e9=7md$qXiB3G)H06yhbNqG=`r%KY|Lgisp#lD?7vHrb zyT4Ez_A0^&_)(ZW12~Pv&?fz@RaThwkL3c9R@-cY`2Q?mJ6f<%U zF5JH<67eyHPh@<8vT*$5Lydc^yA|bL)bO<)-^XaK)d=HmE8(zWRudf7Vz$?LjzR@kRZ}j@- z6=c~9Ygjl&Qqgmn-=(*SIkd@IxX0Q)98Yx0(-n>QX%@S}u69~ZC0#5xeZ u#o;w;!8-8)7_aFt3w{9cyV~&ju?_you1@TI=xl8HpRC9K>gfL?4g6nSg>ohU literal 0 HcmV?d00001 diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs index 88fd7a337..b14991721 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs @@ -2,11 +2,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Drawing; +using System.Drawing.Imaging; using FluentValidation.Results; +using Growl.CoreLibrary; using Growl.Connector; using NLog; using NzbDrone.Common.Instrumentation; using GrowlNotification = Growl.Connector.Notification; +using System.Reflection; +using System.IO; namespace NzbDrone.Core.Notifications.Growl { @@ -21,42 +26,121 @@ namespace NzbDrone.Core.Notifications.Growl private readonly Logger _logger; private readonly Application _growlApplication = new Application("NzbDrone"); - private GrowlConnector _growlConnector; - private readonly List _notificationTypes; + private readonly NotificationType[] _notificationTypes; + + private class GrowlRequestState + { + private AutoResetEvent _autoEvent = new AutoResetEvent(false); + private bool _isError = false; + private int _code; + private string _description; + + public void Wait(int timeoutMs) + { + try + { + if (!_autoEvent.WaitOne(timeoutMs)) + { + throw new GrowlException(ErrorCode.TIMED_OUT, ErrorDescription.TIMED_OUT, null); + } + if (_isError) + { + throw new GrowlException(_code, _description, null); + } + } + finally + { + _autoEvent.Reset(); + _isError = false; + _code = 0; + _description = null; + } + } + + public void Update() + { + _autoEvent.Set(); + } + + public void Update(int code, string description) + { + _code = code; + _description = description; + _isError = true; + _autoEvent.Set(); + } + } public GrowlService(Logger logger) { _logger = logger; _notificationTypes = GetNotificationTypes(); - _growlApplication.Icon = "https://raw.github.com/NzbDrone/NzbDrone/master/Logo/64.png"; + + var icon = Properties.Resources.Icon64; + var stream = new MemoryStream(); + icon.Save(stream, ImageFormat.Bmp); + _growlApplication.Icon = new BinaryData(stream.ToArray()); + } + + private GrowlConnector GetGrowlConnector(string hostname, int port, string password) + { + var growlConnector = new GrowlConnector(password, hostname, port); + growlConnector.OKResponse += GrowlOKResponse; + growlConnector.ErrorResponse += GrowlErrorResponse; + return growlConnector; } public void SendNotification(string title, string message, string notificationTypeName, string hostname, int port, string password) { - var notificationType = _notificationTypes.Single(n => n.Name == notificationTypeName); - var notification = new GrowlNotification("NzbDrone", notificationType.Name, DateTime.Now.Ticks.ToString(), title, message); - - _growlConnector = new GrowlConnector(password, hostname, port); - _logger.Debug("Sending Notification to: {0}:{1}", hostname, port); - _growlConnector.Notify(notification); + + var notificationType = _notificationTypes.Single(n => n.Name == notificationTypeName); + var notification = new GrowlNotification(_growlApplication.Name, notificationType.Name, DateTime.Now.Ticks.ToString(), title, message); + + var growlConnector = GetGrowlConnector(hostname, port, password); + + var requestState = new GrowlRequestState(); + growlConnector.Notify(notification, requestState); + requestState.Wait(5000); } private void Register(string host, int port, string password) { _logger.Debug("Registering NzbDrone with Growl host: {0}:{1}", host, port); - _growlConnector = new GrowlConnector(password, host, port); - _growlConnector.Register(_growlApplication, _notificationTypes.ToArray()); + + var growlConnector = GetGrowlConnector(host, port, password); + + var requestState = new GrowlRequestState(); + growlConnector.Register(_growlApplication, _notificationTypes, requestState); + requestState.Wait(5000); } - private List GetNotificationTypes() + private void GrowlErrorResponse(Response response, object state) + { + var requestState = state as GrowlRequestState; + if (requestState != null) + { + requestState.Update(response.ErrorCode, response.ErrorDescription); + } + } + + private void GrowlOKResponse(Response response, object state) + { + var requestState = state as GrowlRequestState; + if (requestState != null) + { + requestState.Update(); + } + } + + private NotificationType[] GetNotificationTypes() { var notificationTypes = new List(); notificationTypes.Add(new NotificationType("TEST", "Test")); notificationTypes.Add(new NotificationType("GRAB", "Episode Grabbed")); notificationTypes.Add(new NotificationType("DOWNLOAD", "Episode Complete")); - return notificationTypes; + return notificationTypes.ToArray(); } public ValidationFailure Test(GrowlSettings settings) @@ -68,8 +152,6 @@ namespace NzbDrone.Core.Notifications.Growl const string title = "Test Notification"; const string body = "This is a test message from NzbDrone"; - Thread.Sleep(5000); - SendNotification(title, body, "TEST", settings.Host, settings.Port, settings.Password); } catch (Exception ex) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 88d5299eb..399c6735a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -52,6 +52,14 @@ 4 + + False + ..\Libraries\Growl.Connector.dll + + + False + ..\Libraries\Growl.CoreLibrary.dll + @@ -70,12 +78,6 @@ ..\packages\FluentValidation.5.0.0.1\lib\Net40\FluentValidation.dll - - ..\packages\Growl.0.6\lib\Growl.Connector.dll - - - ..\packages\Growl.0.6\lib\Growl.CoreLibrary.dll - ..\packages\MediaInfoNet.0.3\lib\MediaInfoDotNet.dll @@ -661,6 +663,11 @@ + + True + True + Resources.resx + @@ -795,8 +802,14 @@ Always + + + + + ResXFileCodeGenerator + Resources.Designer.cs + - diff --git a/src/NzbDrone.Core/Properties/Resources.Designer.cs b/src/NzbDrone.Core/Properties/Resources.Designer.cs new file mode 100644 index 000000000..a995b6ddd --- /dev/null +++ b/src/NzbDrone.Core/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace NzbDrone.Core.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NzbDrone.Core.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Icon64 { + get { + object obj = ResourceManager.GetObject("Icon64", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/src/NzbDrone.Core/Properties/Resources.resx b/src/NzbDrone.Core/Properties/Resources.resx new file mode 100644 index 000000000..d22523afe --- /dev/null +++ b/src/NzbDrone.Core/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/src/NzbDrone.Core/Resources/64.png b/src/NzbDrone.Core/Resources/64.png new file mode 100644 index 0000000000000000000000000000000000000000..33387d7f9372b0951d676da70a366cf594312de4 GIT binary patch literal 3582 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 Ef+h&)O8@`> literal 0 HcmV?d00001 diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config index f657add80..e035259dc 100644 --- a/src/NzbDrone.Core/packages.config +++ b/src/NzbDrone.Core/packages.config @@ -2,7 +2,6 @@ - From ca09588390417558a9a1747b750ab0720e8f9647 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Aug 2014 01:27:53 -0700 Subject: [PATCH 056/105] Fixed: Remove GDI+ dependency with embedded resource --- .../Notifications/Growl/GrowlService.cs | 14 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 12 -- .../Properties/Resources.Designer.cs | 73 ----------- src/NzbDrone.Core/Properties/Resources.resx | 124 ------------------ src/NzbDrone.Core/Resources/64.png | Bin 3582 -> 0 bytes 5 files changed, 5 insertions(+), 218 deletions(-) delete mode 100644 src/NzbDrone.Core/Properties/Resources.Designer.cs delete mode 100644 src/NzbDrone.Core/Properties/Resources.resx delete mode 100644 src/NzbDrone.Core/Resources/64.png diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs index b14991721..c7c943c5b 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs @@ -2,15 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Drawing; -using System.Drawing.Imaging; using FluentValidation.Results; using Growl.CoreLibrary; using Growl.Connector; using NLog; -using NzbDrone.Common.Instrumentation; using GrowlNotification = Growl.Connector.Notification; -using System.Reflection; using System.IO; namespace NzbDrone.Core.Notifications.Growl @@ -31,7 +27,7 @@ namespace NzbDrone.Core.Notifications.Growl private class GrowlRequestState { private AutoResetEvent _autoEvent = new AutoResetEvent(false); - private bool _isError = false; + private bool _isError; private int _code; private string _description; @@ -76,10 +72,10 @@ namespace NzbDrone.Core.Notifications.Growl _logger = logger; _notificationTypes = GetNotificationTypes(); - var icon = Properties.Resources.Icon64; - var stream = new MemoryStream(); - icon.Save(stream, ImageFormat.Bmp); - _growlApplication.Icon = new BinaryData(stream.ToArray()); + var iconPath = Path.Combine("UI", "Content", "Images", "logos", "64.png"); + var bytes = File.ReadAllBytes(iconPath); + + _growlApplication.Icon = new BinaryData(bytes); } private GrowlConnector GetGrowlConnector(string hostname, int port, string password) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 399c6735a..8dbf6f83e 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -663,11 +663,6 @@ - - True - True - Resources.resx - @@ -802,13 +797,6 @@ Always - - - - - ResXFileCodeGenerator - Resources.Designer.cs - diff --git a/src/NzbDrone.Core/Properties/Resources.Designer.cs b/src/NzbDrone.Core/Properties/Resources.Designer.cs deleted file mode 100644 index a995b6ddd..000000000 --- a/src/NzbDrone.Core/Properties/Resources.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.18444 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NzbDrone.Core.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NzbDrone.Core.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap Icon64 { - get { - object obj = ResourceManager.GetObject("Icon64", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} diff --git a/src/NzbDrone.Core/Properties/Resources.resx b/src/NzbDrone.Core/Properties/Resources.resx deleted file mode 100644 index d22523afe..000000000 --- a/src/NzbDrone.Core/Properties/Resources.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Resources\64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - \ No newline at end of file diff --git a/src/NzbDrone.Core/Resources/64.png b/src/NzbDrone.Core/Resources/64.png deleted file mode 100644 index 33387d7f9372b0951d676da70a366cf594312de4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3582 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 Ef+h&)O8@`> From dd15442dcd3fdd4efa4af85b84dbcf081c84b808 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Aug 2014 01:51:19 -0700 Subject: [PATCH 057/105] Copy logo into output folder on build, fix broken tests --- build.ps1 | 4 +++- src/NzbDrone.Core/Notifications/Growl/GrowlService.cs | 3 +-- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build.ps1 b/build.ps1 index 0a20c3af3..073ab6516 100644 --- a/build.ps1 +++ b/build.ps1 @@ -161,7 +161,7 @@ Function PackageTests() Copy-Item $outputFolder\*.dll -Destination $testPackageFolder -Force Copy-Item $outputFolder\*.pdb -Destination $testPackageFolder -Force - Copy-Item .\*.sh -Destination $testPackageFolder -Force + Copy-Item .\*.sh -Destination $testPackageFolder -Force get-childitem $testPackageFolder -File -Filter *log.config | foreach ($_) {remove-item $_.fullname} @@ -170,6 +170,8 @@ Function PackageTests() Write-Host "Adding MediaInfoDotNet.dll.config (for dllmap)" Copy-Item "$sourceFolder\MediaInfoDotNet.dll.config" -Destination $testPackageFolder -Force + Copy-Item "$outputFolder\64.png" -Destination $testPackageFolder -Force + Write-Host "##teamcity[progressFinish 'Creating Test Package']" } diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs index c7c943c5b..32c81e9c7 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs @@ -72,8 +72,7 @@ namespace NzbDrone.Core.Notifications.Growl _logger = logger; _notificationTypes = GetNotificationTypes(); - var iconPath = Path.Combine("UI", "Content", "Images", "logos", "64.png"); - var bytes = File.ReadAllBytes(iconPath); + var bytes = File.ReadAllBytes("64.png"); _growlApplication.Icon = new BinaryData(bytes); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 8dbf6f83e..2f2700b26 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -794,6 +794,9 @@ + + Always + Always From 9eab1459241a0ef5d3672513e3e4e0c0a5d9d270 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Aug 2014 01:53:15 -0700 Subject: [PATCH 058/105] Fixing csproj --- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 2f2700b26..eeda53753 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -794,7 +794,8 @@ - + + 64.png Always From cd5f404cea977098f60787ec4057b05555bad234 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Aug 2014 02:12:56 -0700 Subject: [PATCH 059/105] Removing logo from growl temporarily --- src/NzbDrone.Core/Notifications/Growl/GrowlService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs index 32c81e9c7..b572747c2 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs @@ -72,9 +72,9 @@ namespace NzbDrone.Core.Notifications.Growl _logger = logger; _notificationTypes = GetNotificationTypes(); - var bytes = File.ReadAllBytes("64.png"); - - _growlApplication.Icon = new BinaryData(bytes); +// var bytes = File.ReadAllBytes("64.png"); +// +// _growlApplication.Icon = new BinaryData(bytes); } private GrowlConnector GetGrowlConnector(string hostname, int port, string password) From a5a99f64ffee4e2e17def85cc72a317d14ec3913 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 7 Aug 2014 22:12:54 +0200 Subject: [PATCH 060/105] Fixed: Parser now recognizes 848x480 as 480p. --- src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 1 + src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index bef9f4c41..bd8d56fdf 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -76,6 +76,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Hannibal.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)] [TestCase("Hannibal.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] [TestCase("Heidi Girl of the Alps (BD)(640x480(RAW) (BATCH 1) (1-13)", false)] + [TestCase("[Doki] Clannad - 02 (848x480 XviD BD MP3) [95360783]", false)] public void should_parse_dvd_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.DVD, proper); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 156a17caf..36fa12447 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex ProperRegex = new Regex(@"\b(?proper|repack)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p|640x480)|(?<_576p>576p)|(?<_720p>720p|1280x720)|(?<_1080p>1080p|1920x1080))\b", + private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p|640x480|848x480)|(?<_576p>576p)|(?<_720p>720p|1280x720)|(?<_1080p>1080p|1920x1080))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex CodecRegex = new Regex(@"\b(?:(?x264)|(?h264)|(?XvidHD)|(?Xvid)|(?divx))\b", From 6c44121b093777b16ff26d12dd4b527513f78574 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 7 Aug 2014 14:42:46 +0200 Subject: [PATCH 061/105] Fixed: Now assuming that an Ended series without any airdates are direct-to-dvd. --- .../TvTests/RefreshEpisodeServiceFixture.cs | 25 +++++++++++++++++++ src/NzbDrone.Core/Tv/RefreshEpisodeService.cs | 17 +++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index 0284b2431..16de304c9 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs @@ -306,5 +306,30 @@ namespace NzbDrone.Core.Test.TvTests _insertedEpisodes.Should().HaveCount(episodes.Count); } + + [Test] + public void should_override_empty_airdate_for_direct_to_dvd() + { + var series = GetSeries(); + series.Status = SeriesStatusType.Ended; + + var episodes = Builder.CreateListOfSize(10) + .All() + .With(v => v.AirDateUtc = null) + .BuildListOfNew(); + + Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) + .Returns(new List()); + + List updateEpisodes = null; + Mocker.GetMock().Setup(c => c.InsertMany(It.IsAny>())) + .Callback>(c => updateEpisodes = c); + + Subject.RefreshEpisodeInfo(series, episodes); + + updateEpisodes.Should().NotBeNull(); + updateEpisodes.Should().NotBeEmpty(); + updateEpisodes.All(v => v.AirDateUtc.HasValue).Should().BeTrue(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs index 8db1e864b..46b11c0f1 100644 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Tv dupeFreeRemoteEpisodes = MapAbsoluteEpisodeNumbers(series, dupeFreeRemoteEpisodes); } - foreach (var episode in OrderEpsiodes(series, dupeFreeRemoteEpisodes)) + foreach (var episode in OrderEpisodes(series, dupeFreeRemoteEpisodes)) { try { @@ -90,6 +90,7 @@ namespace NzbDrone.Core.Tv allEpisodes.AddRange(updateList); AdjustMultiEpisodeAirTime(series, allEpisodes); + AdjustDirectToDvdAirDate(series, allEpisodes); _episodeService.DeleteMany(existingEpisodes); _episodeService.UpdateMany(updateList); @@ -151,6 +152,18 @@ namespace NzbDrone.Core.Tv } } + private static void AdjustDirectToDvdAirDate(Series series, IEnumerable allEpisodes) + { + if (series.Status == SeriesStatusType.Ended && allEpisodes.All(v => !v.AirDateUtc.HasValue) && series.FirstAired.HasValue) + { + foreach (var episode in allEpisodes) + { + episode.AirDateUtc = series.FirstAired; + episode.AirDate = series.FirstAired.Value.ToString("yyyy-MM-dd"); + } + } + } + private List MapAbsoluteEpisodeNumbers(Series series, List traktEpisodes) { var tvdbEpisodes = _tvdbProxy.GetEpisodeInfo(series.TvdbId); @@ -192,7 +205,7 @@ namespace NzbDrone.Core.Tv return existingEpisodes.FirstOrDefault(e => e.SeasonNumber == episode.SeasonNumber && e.EpisodeNumber == episode.EpisodeNumber); } - private IEnumerable OrderEpsiodes(Series series, List episodes) + private IEnumerable OrderEpisodes(Series series, List episodes) { if (series.SeriesType == SeriesTypes.Anime) { From 10de8087f7ad4c8ca691fa5db9188bc1a26b35b8 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 3 Aug 2014 00:51:20 +0200 Subject: [PATCH 062/105] Added warning to Sabnzbd Client Test to disable the sabnzbd pre-check option. --- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 18 +++++++++++++++++- .../Clients/Sabnzbd/SabnzbdCategory.cs | 1 + .../Download/DownloadTrackingService.cs | 12 ++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 514bcbee8..13f7e3d77 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -355,6 +355,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { failures.AddIfNotNull(TestConnection()); failures.AddIfNotNull(TestAuthentication()); + failures.AddIfNotNull(TestGlobalConfig()); failures.AddIfNotNull(TestCategory()); if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) @@ -399,10 +400,25 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return null; } + + private ValidationFailure TestGlobalConfig() + { + var config = _proxy.GetConfig(Settings); + if (config.Misc.pre_check) + { + return new NzbDroneValidationFailure("", "Disable 'Check before download' option in Sabnbzd") + { + InfoLink = String.Format("http://{0}:{1}/sabnzbd/config/switches/", Settings.Host, Settings.Port), + DetailedDescription = "Using Check before download affects NzbDrone ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective." + }; + } + + return null; + } private ValidationFailure TestCategory() { - var config = this._proxy.GetConfig(Settings); + var config = _proxy.GetConfig(Settings); var category = GetCategories(config).FirstOrDefault((SabnzbdCategory v) => v.Name == Settings.TvCategory); if (category != null) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs index 15913c620..2068cd0a8 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public String complete_dir { get; set; } public String[] tv_categories { get; set; } public Boolean enable_tv_sorting { get; set; } + public Boolean pre_check { get; set; } } public class SabnzbdCategory diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs index 3dc255219..37ad08d4b 100644 --- a/src/NzbDrone.Core/Download/DownloadTrackingService.cs +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download State = TrackedDownloadState.Unknown }; - _logger.Debug("Started tracking download from history: {0}: {1}", trackedDownload.TrackingId, downloadItem.Title); + _logger.Debug("[{0}] Started tracking download with id {1}.", downloadItem.Title, trackingId); stateChanged = true; } @@ -157,17 +157,17 @@ namespace NzbDrone.Core.Download } } - foreach (var downloadItem in oldTrackedDownloads.Values.Where(v => !newTrackedDownloads.ContainsKey(v.TrackingId))) + foreach (var trackedDownload in oldTrackedDownloads.Values.Where(v => !newTrackedDownloads.ContainsKey(v.TrackingId))) { - if (downloadItem.State != TrackedDownloadState.Removed) + if (trackedDownload.State != TrackedDownloadState.Removed) { - downloadItem.State = TrackedDownloadState.Removed; + trackedDownload.State = TrackedDownloadState.Removed; stateChanged = true; - _logger.Debug("Item removed from download client by user: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); + _logger.Debug("[{0}] Item with id {1} removed from download client directly (possibly by user).", trackedDownload.DownloadItem.Title, trackedDownload.TrackingId); } - _logger.Debug("Stopped tracking download: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); + _logger.Debug("[{0}] Stopped tracking download with id {1}.", trackedDownload.DownloadItem.Title, trackedDownload.TrackingId); } _trackedDownloadCache.Set("tracked", newTrackedDownloads.Values.ToArray()); From a62cd042de786ee9c58a82cdf9b7feec79a102fc Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 2 Aug 2014 00:10:31 +0200 Subject: [PATCH 063/105] Added some resilience in the GetCurrentProcess calls so it doesn't cause a Fatal exit. --- .../MonitoringProviderTest.cs | 35 ------------- .../NzbDrone.Host.Test.csproj | 1 - .../EnvironmentInfo/RuntimeInfoBase.cs | 29 +++++++---- src/NzbDrone.Host/ApplicationServer.cs | 13 ++--- src/NzbDrone.Host/NzbDrone.Host.csproj | 1 - src/NzbDrone.Host/PriorityMonitor.cs | 52 ------------------- src/NzbDrone.Host/SingleInstancePolicy.cs | 31 +++++++---- 7 files changed, 43 insertions(+), 119 deletions(-) delete mode 100644 src/NzbDrone.App.Test/MonitoringProviderTest.cs delete mode 100644 src/NzbDrone.Host/PriorityMonitor.cs diff --git a/src/NzbDrone.App.Test/MonitoringProviderTest.cs b/src/NzbDrone.App.Test/MonitoringProviderTest.cs deleted file mode 100644 index e980d2c46..000000000 --- a/src/NzbDrone.App.Test/MonitoringProviderTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Model; -using NzbDrone.Common.Processes; -using NzbDrone.Host; -using NzbDrone.Test.Common; - -namespace NzbDrone.App.Test -{ - [TestFixture] - public class MonitoringProviderTest : TestBase - { - [Test] - public void Ensure_priority_doesnt_fail_on_invalid_process_id() - { - Mocker.GetMock().Setup(c => c.GetCurrentProcess()) - .Returns(Builder.CreateNew().Build()); - - Mocker.GetMock().Setup(c => c.GetProcessById(It.IsAny())).Returns((ProcessInfo)null); - - Subject.EnsurePriority(null); - } - - [Test] - public void Ensure_should_log_warn_exception_rather_than_throw() - { - Mocker.GetMock().Setup(c => c.GetCurrentProcess()).Throws(); - Subject.EnsurePriority(null); - - ExceptionVerification.ExpectedWarns(1); - } - } -} diff --git a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj index a7ddd84f0..7157fa439 100644 --- a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj +++ b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj @@ -60,7 +60,6 @@ - diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs index 6f8de3f9a..5e92b46f0 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs @@ -12,7 +12,6 @@ namespace NzbDrone.Common.EnvironmentInfo public abstract class RuntimeInfoBase : IRuntimeInfo { private readonly Logger _logger; - private static readonly string ProcessName = Process.GetCurrentProcess().ProcessName.ToLower(); public RuntimeInfoBase(IServiceProvider serviceProvider, Logger logger) { @@ -73,10 +72,12 @@ namespace NzbDrone.Common.EnvironmentInfo { get { - return (OsInfo.IsWindows && - IsUserInteractive && - ProcessName.Equals(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase)) || - OsInfo.IsMono; + if (OsInfo.IsWindows) + { + return IsUserInteractive && Process.GetCurrentProcess().ProcessName.Equals(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase); + } + + return true; } } @@ -93,11 +94,19 @@ namespace NzbDrone.Common.EnvironmentInfo if (BuildInfo.IsDebug || Debugger.IsAttached) return false; if (BuildInfo.Version.Revision > 10000) return false; //Official builds will never have such a high revision - var lowerProcessName = ProcessName.ToLower(); - if (lowerProcessName.Contains("vshost")) return false; - if (lowerProcessName.Contains("nunit")) return false; - if (lowerProcessName.Contains("jetbrain")) return false; - if (lowerProcessName.Contains("resharper")) return false; + try + { + var lowerProcessName = Process.GetCurrentProcess().ProcessName.ToLower(); + + if (lowerProcessName.Contains("vshost")) return false; + if (lowerProcessName.Contains("nunit")) return false; + if (lowerProcessName.Contains("jetbrain")) return false; + if (lowerProcessName.Contains("resharper")) return false; + } + catch + { + + } string lowerCurrentDir = Directory.GetCurrentDirectory().ToLower(); if (lowerCurrentDir.Contains("teamcity")) return false; diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index c76fcc75c..bf1eb19f1 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -21,23 +21,20 @@ namespace NzbDrone.Host private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; private readonly IHostController _hostController; - private readonly PriorityMonitor _priorityMonitor; private readonly IStartupContext _startupContext; private readonly IBrowserService _browserService; private readonly Logger _logger; - public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, + public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, IHostController hostController, - IRuntimeInfo runtimeInfo, - PriorityMonitor priorityMonitor, - IStartupContext startupContext, - IBrowserService browserService, + IRuntimeInfo runtimeInfo, + IStartupContext startupContext, + IBrowserService browserService, Logger logger) { _configFileProvider = configFileProvider; _hostController = hostController; _runtimeInfo = runtimeInfo; - _priorityMonitor = priorityMonitor; _startupContext = startupContext; _browserService = browserService; _logger = logger; @@ -63,8 +60,6 @@ namespace NzbDrone.Host { _browserService.LaunchWebUI(); } - - _priorityMonitor.Start(); } protected override void OnStop() diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index 67d954511..f95e3ffa2 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -116,7 +116,6 @@ - diff --git a/src/NzbDrone.Host/PriorityMonitor.cs b/src/NzbDrone.Host/PriorityMonitor.cs deleted file mode 100644 index 2972024dd..000000000 --- a/src/NzbDrone.Host/PriorityMonitor.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Processes; - -namespace NzbDrone.Host -{ - public class PriorityMonitor - { - private readonly IProcessProvider _processProvider; - private readonly Logger _logger; - - private Timer _processPriorityCheckTimer; - - public PriorityMonitor(IProcessProvider processProvider, Logger logger) - { - _processProvider = processProvider; - _logger = logger; - } - - public void Start() - { - _processPriorityCheckTimer = new Timer(EnsurePriority); - _processPriorityCheckTimer.Change(TimeSpan.FromSeconds(15), TimeSpan.FromMinutes(30)); - } - - public virtual void EnsurePriority(object sender) - { - try - { - if (_processProvider.GetCurrentProcessPriority() != ProcessPriorityClass.Normal) - { - _processProvider.SetPriority(_processProvider.GetCurrentProcess().Id, ProcessPriorityClass.Normal); - } - } - catch (Exception e) - { - if (OsInfo.IsMono) - { - _logger.TraceException("Unable to verify priority", e); - } - - else - { - _logger.WarnException("Unable to verify priority", e); - } - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Host/SingleInstancePolicy.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs index a1b489705..4e8d44790 100644 --- a/src/NzbDrone.Host/SingleInstancePolicy.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -52,19 +52,28 @@ namespace NzbDrone.Host private List GetOtherNzbDroneProcessIds() { - var currentId = _processProvider.GetCurrentProcess().Id; - var otherProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) - .Union(_processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) - .Select(c => c.Id) - .Except(new[] {currentId}) - .ToList(); - - if (otherProcesses.Any()) + try { - _logger.Info("{0} instance(s) of NzbDrone are running", otherProcesses.Count); - } + var currentId = _processProvider.GetCurrentProcess().Id; - return otherProcesses; + var otherProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) + .Union(_processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Select(c => c.Id) + .Except(new[] { currentId }) + .ToList(); + + if (otherProcesses.Any()) + { + _logger.Info("{0} instance(s) of NzbDrone are running", otherProcesses.Count); + } + + return otherProcesses; + } + catch (Exception ex) + { + _logger.WarnException("Failed to check for multiple instances of NzbDrone.", ex); + return new List(); + } } } } \ No newline at end of file From d1a8cd2a84b369e75c9e073660af2ac193db863e Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 9 Aug 2014 14:58:04 +0200 Subject: [PATCH 064/105] Readded Growl logo via embedded binary blob. --- src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + src/NzbDrone.Common/ResourceExtensions.cs | 28 +++++++++++++++++++ .../Notifications/Growl/GrowlService.cs | 21 +++++++------- src/NzbDrone.Core/NzbDrone.Core.csproj | 7 ++--- 4 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 src/NzbDrone.Common/ResourceExtensions.cs diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 8d4a98d3d..346c4dc04 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -138,6 +138,7 @@ + diff --git a/src/NzbDrone.Common/ResourceExtensions.cs b/src/NzbDrone.Common/ResourceExtensions.cs new file mode 100644 index 000000000..2a168a151 --- /dev/null +++ b/src/NzbDrone.Common/ResourceExtensions.cs @@ -0,0 +1,28 @@ +using NzbDrone.Common.EnsureThat; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace NzbDrone.Common +{ + public static class ResourceExtensions + { + public static Byte[] GetManifestResourceBytes(this Assembly assembly, String name) + { + var stream = assembly.GetManifestResourceStream(name); + + var result = new Byte[stream.Length]; + var read = stream.Read(result, 0, result.Length); + + if (read != result.Length) + { + throw new EndOfStreamException("Reached end of stream before reading enough bytes."); + } + + return result; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs index b572747c2..d08fbc66d 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs @@ -1,13 +1,14 @@ -using System; +using FluentValidation.Results; +using Growl.Connector; +using Growl.CoreLibrary; +using GrowlNotification = Growl.Connector.Notification; +using NLog; +using NzbDrone.Common; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; -using FluentValidation.Results; -using Growl.CoreLibrary; -using Growl.Connector; -using NLog; -using GrowlNotification = Growl.Connector.Notification; -using System.IO; namespace NzbDrone.Core.Notifications.Growl { @@ -72,9 +73,9 @@ namespace NzbDrone.Core.Notifications.Growl _logger = logger; _notificationTypes = GetNotificationTypes(); -// var bytes = File.ReadAllBytes("64.png"); -// -// _growlApplication.Icon = new BinaryData(bytes); + var logo = typeof(GrowlService).Assembly.GetManifestResourceBytes("NzbDrone.Core.Resources.Logo.64.png"); + + _growlApplication.Icon = new BinaryData(logo); } private GrowlConnector GetGrowlConnector(string hostname, int port, string password) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index eeda53753..a93414f85 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -794,10 +794,9 @@ - - 64.png - Always - + + Resources\Logo\64.png + Always From 4b5cf7d7d3b96f48aae16cdeb06ecac6b7df37e2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 10 Aug 2014 19:02:06 -0700 Subject: [PATCH 065/105] Styling fixes --- src/UI/AddSeries/addSeries.less | 6 +++++ src/UI/History/Queue/QueueLayoutTemplate.html | 4 ++-- src/UI/Series/Index/EmptyTemplate.html | 24 +++++++++++-------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index 3a6357677..19ceb4b94 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -136,4 +136,10 @@ li.add-new:hover { .validation-errors { display: none; } + + .input-group { + .form-control { + background-color: white; + } + } } diff --git a/src/UI/History/Queue/QueueLayoutTemplate.html b/src/UI/History/Queue/QueueLayoutTemplate.html index bdf520b80..4af587514 100644 --- a/src/UI/History/Queue/QueueLayoutTemplate.html +++ b/src/UI/History/Queue/QueueLayoutTemplate.html @@ -1,11 +1,11 @@ 
-
+
-
+
\ No newline at end of file diff --git a/src/UI/Series/Index/EmptyTemplate.html b/src/UI/Series/Index/EmptyTemplate.html index 4dcbb9624..7c7849d9c 100644 --- a/src/UI/Series/Index/EmptyTemplate.html +++ b/src/UI/Series/Index/EmptyTemplate.html @@ -1,12 +1,16 @@ -
-
- - You must be new around here, You should add some series. +
+
+
+ + You must be new around here, You should add some series. +
+
+
- From 78e5209cfd3edd99da2a648f4d8a4fc7ada1ec29 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 10 Aug 2014 21:13:28 -0700 Subject: [PATCH 066/105] New: Details for import/grab on Episode Activity tab --- src/UI/Cells/cells.less | 10 ++ .../Activity/EpisodeActivityDetailsCell.js | 35 +++++ .../Episode/Activity/EpisodeActivityLayout.js | 8 + .../History/Details/HistoryDetailsLayout.js | 40 +++++ .../Details/HistoryDetailsLayoutTemplate.html | 23 +++ src/UI/History/Details/HistoryDetailsView.js | 25 +--- .../Details/HistoryDetailsViewTemplate.html | 140 ++++++++---------- src/UI/Series/series.less | 4 + src/UI/Shared/Modal/ModalController.js | 6 +- 9 files changed, 184 insertions(+), 107 deletions(-) create mode 100644 src/UI/Episode/Activity/EpisodeActivityDetailsCell.js create mode 100644 src/UI/History/Details/HistoryDetailsLayout.js create mode 100644 src/UI/History/Details/HistoryDetailsLayoutTemplate.html diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 67c567269..e7cc528c0 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -129,6 +129,16 @@ td.episode-status-cell, td.quality-cell { } } +.episode-activity-details-cell { + width : 18px; +} + +.episode-detail-modal { + .episode-actions-cell { + width : 18px; + } +} + .series-actions-cell { width : 56px; min-width : 56px; diff --git a/src/UI/Episode/Activity/EpisodeActivityDetailsCell.js b/src/UI/Episode/Activity/EpisodeActivityDetailsCell.js new file mode 100644 index 000000000..d4b7c5452 --- /dev/null +++ b/src/UI/Episode/Activity/EpisodeActivityDetailsCell.js @@ -0,0 +1,35 @@ +'use strict'; + +define( + [ + 'jquery', + 'vent', + 'marionette', + 'Cells/NzbDroneCell', + 'History/Details/HistoryDetailsView', + 'bootstrap' + ], function ($, vent, Marionette, NzbDroneCell, HistoryDetailsView) { + return NzbDroneCell.extend({ + + className: 'episode-activity-details-cell', + + + render: function () { + this.$el.empty(); + this.$el.html(''); + + var html = new HistoryDetailsView({ model: this.model }).render().$el; + + this.$el.popover({ + content : html, + html : true, + trigger : 'hover', + title : 'Details', + placement: 'left', + container: this.$el + }); + + return this; + } + }); + }); diff --git a/src/UI/Episode/Activity/EpisodeActivityLayout.js b/src/UI/Episode/Activity/EpisodeActivityLayout.js index b9c9582d6..519a7d283 100644 --- a/src/UI/Episode/Activity/EpisodeActivityLayout.js +++ b/src/UI/Episode/Activity/EpisodeActivityLayout.js @@ -8,6 +8,7 @@ define( 'Cells/QualityCell', 'Cells/RelativeDateCell', 'Episode/Activity/EpisodeActivityActionsCell', + 'Episode/Activity/EpisodeActivityDetailsCell', 'Episode/Activity/NoActivityView', 'Shared/LoadingView' ], function (Marionette, @@ -17,6 +18,7 @@ define( QualityCell, RelativeDateCell, EpisodeActivityActionsCell, + EpisodeActivityDetailsCell, NoActivityView, LoadingView) { @@ -50,6 +52,12 @@ define( label: 'Date', cell : RelativeDateCell }, + { + name : 'this', + label : '', + cell : EpisodeActivityDetailsCell, + sortable: false + }, { name : 'this', label : '', diff --git a/src/UI/History/Details/HistoryDetailsLayout.js b/src/UI/History/Details/HistoryDetailsLayout.js new file mode 100644 index 000000000..0545aefee --- /dev/null +++ b/src/UI/History/Details/HistoryDetailsLayout.js @@ -0,0 +1,40 @@ +'use strict'; +define( + [ + 'jquery', + 'vent', + 'marionette', + 'History/Details/HistoryDetailsView' + ], function ($, vent, Marionette, HistoryDetailsView) { + + return Marionette.Layout.extend({ + template: 'History/Details/HistoryDetailsLayoutTemplate', + + regions: { + bodyRegion: '.modal-body' + }, + + events: { + 'click .x-mark-as-failed': '_markAsFailed' + }, + + onShow: function () { + this.bodyRegion.show(new HistoryDetailsView({ model: this.model })); + }, + + _markAsFailed: function () { + var url = window.NzbDrone.ApiRoot + '/history/failed'; + var data = { + id: this.model.get('id') + }; + + $.ajax({ + url: url, + type: 'POST', + data: data + }); + + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + }); diff --git a/src/UI/History/Details/HistoryDetailsLayoutTemplate.html b/src/UI/History/Details/HistoryDetailsLayoutTemplate.html new file mode 100644 index 000000000..897ff0549 --- /dev/null +++ b/src/UI/History/Details/HistoryDetailsLayoutTemplate.html @@ -0,0 +1,23 @@ + diff --git a/src/UI/History/Details/HistoryDetailsView.js b/src/UI/History/Details/HistoryDetailsView.js index 045189137..c7be5ad23 100644 --- a/src/UI/History/Details/HistoryDetailsView.js +++ b/src/UI/History/Details/HistoryDetailsView.js @@ -1,32 +1,11 @@ 'use strict'; define( [ - 'jquery', - 'vent', 'marionette', 'History/Details/HistoryDetailsAge' - ], function ($, vent, Marionette) { + ], function (Marionette) { return Marionette.ItemView.extend({ - template: 'History/Details/HistoryDetailsViewTemplate', - - events: { - 'click .x-mark-as-failed': '_markAsFailed' - }, - - _markAsFailed: function () { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id: this.model.get('id') - }; - - $.ajax({ - url: url, - type: 'POST', - data: data - }); - - vent.trigger(vent.Commands.CloseModalCommand); - } + template: 'History/Details/HistoryDetailsViewTemplate' }); }); diff --git a/src/UI/History/Details/HistoryDetailsViewTemplate.html b/src/UI/History/Details/HistoryDetailsViewTemplate.html index 19b6d3794..0a17b5161 100644 --- a/src/UI/History/Details/HistoryDetailsViewTemplate.html +++ b/src/UI/History/Details/HistoryDetailsViewTemplate.html @@ -1,93 +1,71 @@ - + {{#if importedPath}} +
Imported To:
+
{{importedPath}}
+ {{/if}} + {{/with}} + +{{/if_eq}} \ No newline at end of file diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index e4e05f63d..c69a1f7a5 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -266,6 +266,10 @@ margin-top : 30px; font-size : 12px; } + + .episode-activity-details-cell .popover { + max-width: 800px; + } } .season-grid { diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index 177092de3..b62aaf64b 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -7,10 +7,10 @@ define( 'Series/Edit/EditSeriesView', 'Series/Delete/DeleteSeriesView', 'Episode/EpisodeDetailsLayout', - 'History/Details/HistoryDetailsView', + 'History/Details/HistoryDetailsLayout', 'System/Logs/Table/Details/LogDetailsView', 'Rename/RenamePreviewLayout' - ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsView, LogDetailsView, RenamePreviewLayout) { + ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsLayout, LogDetailsView, RenamePreviewLayout) { return Marionette.AppRouter.extend({ @@ -49,7 +49,7 @@ define( }, _showHistory: function (options) { - var view = new HistoryDetailsView({ model: options.model }); + var view = new HistoryDetailsLayout({ model: options.model }); AppLayout.modalRegion.show(view); }, From 9ffdf1893504385842c8c10272ee451453195936 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 10 Aug 2014 21:20:06 -0700 Subject: [PATCH 067/105] Fixed: Set permissions on new series folders (mono) --- .../MediaFiles/DiskScanService.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 4bdfe04b7..13a1cd83a 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.IO; using System.Linq; using NLog; @@ -74,6 +75,7 @@ namespace NzbDrone.Core.MediaFiles { _logger.Debug("Creating missing series folder: {0}", series.Path); _diskProvider.CreateFolder(series.Path); + SetPermissions(series.Path); } else { @@ -112,6 +114,27 @@ namespace NzbDrone.Core.MediaFiles return mediaFileList.ToArray(); } + private void SetPermissions(String path) + { + if (!_configService.SetPermissionsLinux) + { + return; + } + + try + { + var permissions = _configService.FolderChmod; + _diskProvider.SetPermissions(path, permissions, _configService.ChownUser, _configService.ChownGroup); + } + + catch (Exception ex) + { + + _logger.WarnException("Unable to apply permissions to: " + path, ex); + _logger.DebugException(ex.Message, ex); + } + } + public void Handle(SeriesUpdatedEvent message) { Scan(message.Series); From 9f52daf78e4d945b3a95b5e0b8cbd458049af615 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 10 Aug 2014 22:36:51 -0700 Subject: [PATCH 068/105] New: close modal dialogs by clicking outside of modal --- .../RootFolders/RootFolderLayoutTemplate.html | 52 +++-- src/UI/Calendar/CalendarFeedViewTemplate.html | 44 ++-- .../Episode/EpisodeDetailsLayoutTemplate.html | 57 +++-- .../Details/HistoryDetailsLayoutTemplate.html | 32 ++- src/UI/Hotkeys/HotkeysViewTemplate.html | 72 +++--- .../Rename/RenamePreviewLayoutTemplate.html | 48 ++-- .../Series/Delete/DeleteSeriesTemplate.html | 70 +++--- .../Series/Edit/EditSeriesViewTemplate.html | 138 ++++++------ .../Organize/OrganizeFilesViewTemplate.html | 42 ++-- ...wnloadClientAddCollectionViewTemplate.html | 24 +- .../DownloadClientDeleteViewTemplate.html | 24 +- .../Edit/DownloadClientEditViewTemplate.html | 110 +++++---- .../Add/IndexerAddCollectionViewTemplate.html | 24 +- .../Delete/IndexerDeleteViewTemplate.html | 24 +- .../Edit/IndexerEditViewTemplate.html | 110 +++++---- .../Metadata/MetadataEditViewTemplate.html | 74 +++---- ...NotificationAddCollectionViewTemplate.html | 24 +- .../NotificationDeleteViewTemplate.html | 26 +-- .../Edit/NotificationEditViewTemplate.html | 208 +++++++++--------- .../Profile/DeleteProfileViewTemplate.html | 24 +- .../Edit/EditProfileLayoutTemplate.html | 58 +++-- src/UI/Shared/Modal/ModalRegion.js | 11 +- .../Table/Details/LogDetailsViewTemplate.html | 36 ++- 23 files changed, 645 insertions(+), 687 deletions(-) diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html index 1d0822902..8209c2da6 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html +++ b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html @@ -1,32 +1,30 @@ -