diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 8a8a20cef..29abd1468 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -5,6 +5,7 @@ using NzbDrone.Common; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; +using Omu.ValueInjecter; namespace NzbDrone.Api.ClientSchema { @@ -55,7 +56,7 @@ namespace NzbDrone.Api.ClientSchema } - public static object ReadFormSchema(List fields, Type targetType) + public static object ReadFormSchema(List fields, Type targetType, object defaults = null) { Ensure.That(targetType, () => targetType).IsNotNull(); @@ -63,6 +64,11 @@ namespace NzbDrone.Api.ClientSchema var target = Activator.CreateInstance(targetType); + if (defaults != null) + { + target.InjectFrom(defaults); + } + foreach (var propertyInfo in properties) { var fieldAttribute = propertyInfo.GetAttribute(false); diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs index 14a9eff74..5440099d7 100644 --- a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs +++ b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs @@ -9,9 +9,12 @@ namespace NzbDrone.Api.Config public String DownloadClientWorkingFolders { get; set; } public Int32 DownloadedEpisodesScanInterval { get; set; } + public Boolean EnableCompletedDownloadHandling { get; set; } + public Boolean RemoveCompletedDownloads { get; set; } + + public Boolean EnableFailedDownloadHandling { get; set; } public Boolean AutoRedownloadFailed { get; set; } public Boolean RemoveFailedDownloads { get; set; } - public Boolean EnableFailedDownloadHandling { get; set; } public Int32 BlacklistGracePeriod { get; set; } public Int32 BlacklistRetryInterval { get; set; } public Int32 BlacklistRetryLimit { get; set; } diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs index cb1054168..69cca07fe 100644 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs +++ b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs @@ -1,10 +1,11 @@ using System; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.DownloadClient { public class DownloadClientResource : ProviderResource { public Boolean Enable { get; set; } - public Int32 Protocol { get; set; } + public DownloadProtocol Protocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs deleted file mode 100644 index 58c1a2149..000000000 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Core.Download; -using Omu.ValueInjecter; - -namespace NzbDrone.Api.DownloadClient -{ - public class DownloadClientSchemaModule : NzbDroneRestModule - { - private readonly IDownloadClientFactory _notificationFactory; - - public DownloadClientSchemaModule(IDownloadClientFactory notificationFactory) - : base("downloadclient/schema") - { - _notificationFactory = notificationFactory; - GetResourceAll = GetSchema; - } - - private List GetSchema() - { - var notifications = _notificationFactory.Templates(); - - var result = new List(notifications.Count); - - foreach (var notification in notifications) - { - var notificationResource = new DownloadClientResource(); - notificationResource.InjectFrom(notification); - notificationResource.Fields = SchemaBuilder.ToSchema(notification.Settings); - - result.Add(notificationResource); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Health/HealthResource.cs b/src/NzbDrone.Api/Health/HealthResource.cs index a5bec7c06..281a55e60 100644 --- a/src/NzbDrone.Api/Health/HealthResource.cs +++ b/src/NzbDrone.Api/Health/HealthResource.cs @@ -8,5 +8,6 @@ namespace NzbDrone.Api.Health { public HealthCheckResult Type { get; set; } public String Message { get; set; } + public Uri WikiUrl { get; set; } } } diff --git a/src/NzbDrone.Api/Indexers/IndexerResource.cs b/src/NzbDrone.Api/Indexers/IndexerResource.cs index dbb55c3f0..651d57ccf 100644 --- a/src/NzbDrone.Api/Indexers/IndexerResource.cs +++ b/src/NzbDrone.Api/Indexers/IndexerResource.cs @@ -1,9 +1,11 @@ using System; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.Indexers { public class IndexerResource : ProviderResource { public Boolean Enable { get; set; } + public DownloadProtocol Protocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs b/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs deleted file mode 100644 index d433102c8..000000000 --- a/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Core.Indexers; -using Omu.ValueInjecter; - -namespace NzbDrone.Api.Indexers -{ - public class IndexerSchemaModule : NzbDroneRestModule - { - private readonly IIndexerFactory _indexerFactory; - - public IndexerSchemaModule(IIndexerFactory indexerFactory) - : base("indexer/schema") - { - _indexerFactory = indexerFactory; - GetResourceAll = GetSchema; - } - - private List GetSchema() - { - var indexers = _indexerFactory.Templates().Where(c => c.Implementation =="Newznab"); - - var result = new List(indexers.Count()); - - foreach (var indexer in indexers) - { - var indexerResource = new IndexerResource(); - indexerResource.InjectFrom(indexer); - indexerResource.Fields = SchemaBuilder.ToSchema(indexer.Settings); - - result.Add(indexerResource); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index c99982d69..859399588 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.Indexers { @@ -30,5 +31,6 @@ namespace NzbDrone.Api.Indexers public String DownloadUrl { get; set; } public String InfoUrl { get; set; } public Boolean DownloadAllowed { get; set; } + public DownloadProtocol DownloadProtocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs b/src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs deleted file mode 100644 index 920b9bb7b..000000000 --- a/src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Core.Notifications; -using Omu.ValueInjecter; - -namespace NzbDrone.Api.Notifications -{ - public class NotificationSchemaModule : NzbDroneRestModule - { - private readonly INotificationFactory _notificationFactory; - - public NotificationSchemaModule(INotificationFactory notificationFactory) - : base("notification/schema") - { - _notificationFactory = notificationFactory; - GetResourceAll = GetSchema; - } - - private List GetSchema() - { - var notifications = _notificationFactory.Templates(); - - var result = new List(notifications.Count); - - foreach (var notification in notifications) - { - var notificationResource = new NotificationResource(); - notificationResource.InjectFrom(notification); - notificationResource.Fields = SchemaBuilder.ToSchema(notification.Settings); - - result.Add(notificationResource); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index da8ffac05..0c23ee731 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -144,11 +144,9 @@ - - @@ -172,7 +170,6 @@ - diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index 33f567850..943f27077 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using FluentValidation; using Nancy; @@ -22,7 +23,7 @@ namespace NzbDrone.Api : base(resource) { _providerFactory = providerFactory; - Get["templates"] = x => GetTemplates(); + Get["schema"] = x => GetTemplates(); GetResourceAll = GetAll; GetResourceById = GetProviderById; CreateResource = CreateProvider; @@ -30,10 +31,11 @@ namespace NzbDrone.Api DeleteResource = DeleteProvider; SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); - PostValidator.RuleFor(c => c.Fields).NotEmpty(); + PostValidator.RuleFor(c => c.Fields).NotNull(); } private TProviderResource GetProviderById(int id) @@ -81,8 +83,13 @@ namespace NzbDrone.Api definition.InjectFrom(providerResource); + var preset = _providerFactory.GetPresetDefinitions(definition) + .Where(v => v.Name == definition.Name) + .Select(v => v.Settings) + .FirstOrDefault(); + var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); - definition.Settings = (IProviderConfig)SchemaBuilder.ReadFormSchema(providerResource.Fields, configContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFormSchema(providerResource.Fields, configContract, preset); Validate(definition); @@ -96,15 +103,29 @@ namespace NzbDrone.Api private Response GetTemplates() { - var templates = _providerFactory.Templates(); + var defaultDefinitions = _providerFactory.GetDefaultDefinitions(); - var result = new List(templates.Count()); + var result = new List(defaultDefinitions.Count()); - foreach (var providerDefinition in templates) + foreach (var providerDefinition in defaultDefinitions) { var providerResource = new TProviderResource(); providerResource.InjectFrom(providerDefinition); providerResource.Fields = SchemaBuilder.ToSchema(providerDefinition.Settings); + providerResource.InfoLink = String.Format("https://github.com/NzbDrone/NzbDrone/wiki/Supported-{0}#{1}", + typeof(TProviderResource).Name.Replace("Resource", "s"), + providerDefinition.Implementation.ToLower()); + + var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); + + providerResource.Presets = presetDefinitions.Select(v => + { + var presetResource = new TProviderResource(); + presetResource.InjectFrom(v); + presetResource.Fields = SchemaBuilder.ToSchema(v.Settings); + + return presetResource as ProviderResource; + }).ToList(); result.Add(providerResource); } diff --git a/src/NzbDrone.Api/ProviderResource.cs b/src/NzbDrone.Api/ProviderResource.cs index f866341e0..60c5ad78d 100644 --- a/src/NzbDrone.Api/ProviderResource.cs +++ b/src/NzbDrone.Api/ProviderResource.cs @@ -11,5 +11,8 @@ namespace NzbDrone.Api public List Fields { get; set; } public String Implementation { get; set; } public String ConfigContract { get; set; } + public String InfoLink { get; set; } + + public List Presets { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 0adfe1e79..d47dbbd8f 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -2,18 +2,20 @@ using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Api.Series; +using NzbDrone.Api.Episodes; namespace NzbDrone.Api.Queue { public class QueueResource : RestResource { - public Core.Tv.Series Series { get; set; } - public Episode Episode { get; set; } + public SeriesResource Series { get; set; } + public EpisodeResource Episode { get; set; } public QualityModel Quality { get; set; } public Decimal Size { get; set; } public String Title { get; set; } public Decimal Sizeleft { get; set; } - public TimeSpan Timeleft { get; set; } + public TimeSpan? Timeleft { get; set; } public String Status { get; set; } } } diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index c69bcd5fc..0439fdb27 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using NzbDrone.Common; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles.Events; @@ -40,7 +41,8 @@ namespace NzbDrone.Api.Series PathExistsValidator pathExistsValidator, SeriesPathValidator seriesPathValidator, SeriesExistsValidator seriesExistsValidator, - DroneFactoryValidator droneFactoryValidator + DroneFactoryValidator droneFactoryValidator, + SeriesAncestorValidator seriesAncestorValidator ) : base(commandExecutor) { @@ -59,17 +61,21 @@ namespace NzbDrone.Api.Series SharedValidator.RuleFor(s => s.QualityProfileId).ValidId(); - PutValidator.RuleFor(s => s.Path) - .Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator); + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(seriesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(seriesAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => String.IsNullOrEmpty(s.RootFolderPath)); - PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path)); + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); } private void PopulateAlternativeTitles(List resources) diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs index 8c62331de..5af9890ba 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs @@ -137,7 +137,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests } [Test] - public void move_read_only_file() + public void should_be_able_to_move_read_only_file() { var source = GetTempFilePath(); var destination = GetTempFilePath(); @@ -151,6 +151,23 @@ namespace NzbDrone.Common.Test.DiskProviderTests Subject.MoveFile(source, destination); } + [Test] + public void should_be_able_to_delete_directory_with_read_only_file() + { + var sourceDir = GetTempFilePath(); + var source = Path.Combine(sourceDir, "test.txt"); + + Directory.CreateDirectory(sourceDir); + + Subject.WriteAllText(source, "SourceFile"); + + File.SetAttributes(source, FileAttributes.ReadOnly); + + Subject.DeleteFolder(sourceDir, true); + + Directory.Exists(sourceDir).Should().BeFalse(); + } + [Test] public void empty_folder_should_return_folder_modified_date() { diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs index a9bc32930..9de173002 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs @@ -5,32 +5,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Common.Test.DiskProviderTests { - public class IsParentFixture : TestBase + public class IsParentPathFixture : TestBase { - private string _parent = @"C:\Test".AsOsAgnostic(); - - [Test] - public void should_return_false_when_not_a_child() - { - var path = @"C:\Another Folder".AsOsAgnostic(); - - DiskProviderBase.IsParent(_parent, path).Should().BeFalse(); - } - - [Test] - public void should_return_true_when_folder_is_parent_of_another_folder() - { - var path = @"C:\Test\TV".AsOsAgnostic(); - - DiskProviderBase.IsParent(_parent, path).Should().BeTrue(); - } - - [Test] - public void should_return_true_when_folder_is_parent_of_a_file() - { - var path = @"C:\Test\30.Rock.S01E01.Pilot.avi".AsOsAgnostic(); - - DiskProviderBase.IsParent(_parent, path).Should().BeTrue(); - } } } diff --git a/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs b/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs new file mode 100644 index 000000000..27fe63480 --- /dev/null +++ b/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; +using System.IO; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Common.Test +{ + [TestFixture] + public class LevenshteinDistanceFixture : TestBase + { + [TestCase("", "", 0)] + [TestCase("abc", "abc", 0)] + [TestCase("abc", "abcd", 1)] + [TestCase("abcd", "abc", 1)] + [TestCase("abc", "abd", 1)] + [TestCase("abc", "adc", 1)] + [TestCase("abcdefgh", "abcghdef", 4)] + [TestCase("a.b.c.", "abc", 3)] + [TestCase("Agents Of SHIELD", "Marvel's Agents Of S.H.I.E.L.D.", 15)] + [TestCase("Agents of cracked", "Agents of shield", 6)] + [TestCase("ABCxxx", "ABC1xx", 1)] + [TestCase("ABC1xx", "ABCxxx", 1)] + public void LevenshteinDistance(String text, String other, Int32 expected) + { + text.LevenshteinDistance(other).Should().Be(expected); + } + + [TestCase("", "", 0)] + [TestCase("abc", "abc", 0)] + [TestCase("abc", "abcd", 1)] + [TestCase("abcd", "abc", 3)] + [TestCase("abc", "abd", 3)] + [TestCase("abc", "adc", 3)] + [TestCase("abcdefgh", "abcghdef", 8)] + [TestCase("a.b.c.", "abc", 0)] + [TestCase("Agents of shield", "Marvel's Agents Of S.H.I.E.L.D.", 9)] + [TestCase("Agents of shield", "Agents of cracked", 14)] + [TestCase("Agents of shield", "the shield", 24)] + [TestCase("ABCxxx", "ABC1xx", 3)] + [TestCase("ABC1xx", "ABCxxx", 3)] + public void LevenshteinDistanceClean(String text, String other, Int32 expected) + { + text.ToLower().LevenshteinDistanceClean(other.ToLower()).Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index ae5e48bf8..e7f6a681f 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -67,6 +67,7 @@ + diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index fd59e7eec..4b8c355dd 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Common.Test [TestFixture] public class PathExtensionFixture : TestBase { + private string _parent = @"C:\Test".AsOsAgnostic(); private IAppFolderInfo GetIAppDirectoryInfo() { @@ -85,6 +86,64 @@ namespace NzbDrone.Common.Test { first.AsOsAgnostic().PathEquals(second.AsOsAgnostic()).Should().BeFalse(); } + + [Test] + public void should_return_false_when_not_a_child() + { + var path = @"C:\Another Folder".AsOsAgnostic(); + + _parent.IsParentPath(path).Should().BeFalse(); + } + + [Test] + public void should_return_true_when_folder_is_parent_of_another_folder() + { + var path = @"C:\Test\TV".AsOsAgnostic(); + + _parent.IsParentPath(path).Should().BeTrue(); + } + + [Test] + public void should_return_true_when_folder_is_parent_of_a_file() + { + var path = @"C:\Test\30.Rock.S01E01.Pilot.avi".AsOsAgnostic(); + + _parent.IsParentPath(path).Should().BeTrue(); + } + [TestCase(@"C:\Test\", @"C:\Test\mydir")] + [TestCase(@"C:\Test\", @"C:\Test\mydir\")] + [TestCase(@"C:\Test", @"C:\Test\30.Rock.S01E01.Pilot.avi")] + public void path_should_be_parent(string parentPath, string childPath) + { + parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeTrue(); + } + + [TestCase(@"C:\Test2\", @"C:\Test")] + [TestCase(@"C:\Test\Test\", @"C:\Test\")] + [TestCase(@"C:\Test\", @"C:\Test")] + [TestCase(@"C:\Test\", @"C:\Test\")] + public void path_should_not_be_parent(string parentPath, string childPath) + { + parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeFalse(); + } + + [TestCase(@"C:\test\", @"C:\Test\mydir")] + [TestCase(@"C:\test", @"C:\Test\mydir\")] + public void path_should_be_parent_on_windows_only(string parentPath, string childPath) + { + var expectedResult = OsInfo.IsWindows; + + parentPath.IsParentPath(childPath).Should().Be(expectedResult); + } + + [Test] + [Ignore] + public void should_not_be_parent_when_it_is_grandparent() + { + var path = Path.Combine(_parent, "parent", "child"); + + _parent.IsParentPath(path).Should().BeFalse(); + } [Test] public void normalize_path_exception_empty() diff --git a/src/NzbDrone.Common/ConvertBase32.cs b/src/NzbDrone.Common/ConvertBase32.cs new file mode 100644 index 000000000..0b69c2d6b --- /dev/null +++ b/src/NzbDrone.Common/ConvertBase32.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common +{ + public static class ConvertBase32 + { + private static string ValidChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + public static byte[] FromBase32String(string str) + { + int numBytes = str.Length * 5 / 8; + byte[] bytes = new Byte[numBytes]; + + // all UPPERCASE chars + str = str.ToUpper(); + + int bitBuffer = 0; + int bitBufferCount = 0; + int index = 0; + + for (int i = 0; i < str.Length;i++ ) + { + bitBuffer = (bitBuffer << 5) | ValidChars.IndexOf(str[i]); + bitBufferCount += 5; + + if (bitBufferCount >= 8) + { + bitBufferCount -= 8; + bytes[index++] = (byte)(bitBuffer >> bitBufferCount); + } + } + + return bytes; + } + } +} diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 47a14be5c..c9de8d11c 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -25,45 +25,17 @@ namespace NzbDrone.Common.Disk public abstract void SetPermissions(string path, string mask, string user, string group); public abstract long? GetTotalSize(string path); - public static string GetRelativePath(string parentPath, string childPath) + + public DateTime FolderGetCreationTimeUtc(string path) { - if (!IsParent(parentPath, childPath)) - { - throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); - } + CheckFolderExists(path); - return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); - } - - public static bool IsParent(string parentPath, string childPath) - { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); - - var parent = new DirectoryInfo(parentPath); - var child = new DirectoryInfo(childPath); - - while (child.Parent != null) - { - if (child.Parent.FullName == parent.FullName) - { - return true; - } - - child = child.Parent; - } - - return false; + return new DirectoryInfo(path).CreationTimeUtc; } public DateTime FolderGetLastWrite(string path) { - Ensure.That(path, () => path).IsValidPath(); - - if (!FolderExists(path)) - { - throw new DirectoryNotFoundException("Directory doesn't exist. " + path); - } + CheckFolderExists(path); var dirFiles = GetFiles(path, SearchOption.AllDirectories).ToList(); @@ -76,21 +48,38 @@ namespace NzbDrone.Common.Disk .Max(c => c.LastWriteTimeUtc); } + public DateTime FileGetCreationTimeUtc(string path) + { + CheckFileExists(path); + + return new FileInfo(path).CreationTimeUtc; + } + public DateTime FileGetLastWrite(string path) { - PathEnsureFileExists(path); + CheckFileExists(path); return new FileInfo(path).LastWriteTime; } public DateTime FileGetLastWriteUtc(string path) { - PathEnsureFileExists(path); + CheckFileExists(path); return new FileInfo(path).LastWriteTimeUtc; } - private void PathEnsureFileExists(string path) + private void CheckFolderExists(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + if (!FolderExists(path)) + { + throw new DirectoryNotFoundException("Directory doesn't exist. " + path); + } + } + + private void CheckFileExists(string path) { Ensure.That(path, () => path).IsValidPath(); @@ -286,6 +275,9 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(); + var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + Array.ForEach(files, RemoveReadOnly); + Directory.Delete(path, recursive); } diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index b57486ff9..afbd7ce60 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -11,7 +11,9 @@ namespace NzbDrone.Common.Disk void InheritFolderPermissions(string filename); void SetPermissions(string path, string mask, string user, string group); long? GetTotalSize(string path); + DateTime FolderGetCreationTimeUtc(string path); DateTime FolderGetLastWrite(string path); + DateTime FileGetCreationTimeUtc(string path); DateTime FileGetLastWrite(string path); DateTime FileGetLastWriteUtc(string path); void EnsureFolder(string path); diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index bdd7c14ac..ad85d0c14 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -24,11 +24,14 @@ namespace NzbDrone.Common.EnvironmentInfo if (!IsMono) { Os = Os.Windows; - } + PathStringComparison = StringComparison.OrdinalIgnoreCase; + } else { Os = IsOsx ? Os.Osx : Os.Linux; + + PathStringComparison = StringComparison.Ordinal; } } @@ -40,6 +43,7 @@ namespace NzbDrone.Common.EnvironmentInfo public static bool IsWindows { get; private set; } public static Os Os { get; private set; } public static DayOfWeek FirstDayOfWeek { get; private set; } + public static StringComparison PathStringComparison { get; private set; } //Borrowed from: https://github.com/jpobst/Pinta/blob/master/Pinta.Core/Managers/SystemManager.cs //From Managed.Windows.Forms/XplatUI diff --git a/src/NzbDrone.Common/Http/HttpProvider.cs b/src/NzbDrone.Common/Http/HttpProvider.cs index 35d9f2eb7..6b04cb548 100644 --- a/src/NzbDrone.Common/Http/HttpProvider.cs +++ b/src/NzbDrone.Common/Http/HttpProvider.cs @@ -84,6 +84,7 @@ namespace NzbDrone.Common.Http public Stream DownloadStream(string url, NetworkCredential credential = null) { var request = (HttpWebRequest)WebRequest.Create(url); + request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; request.UserAgent = _userAgent; request.Timeout = 20 * 1000; diff --git a/src/NzbDrone.Common/Http/NzbDroneWebClient.cs b/src/NzbDrone.Common/Http/NzbDroneWebClient.cs index bc39ac4cb..ccd369bb7 100644 --- a/src/NzbDrone.Common/Http/NzbDroneWebClient.cs +++ b/src/NzbDrone.Common/Http/NzbDroneWebClient.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Common.Http if (request is HttpWebRequest) { ((HttpWebRequest)request).KeepAlive = false; + ((HttpWebRequest)request).ServicePoint.Expect100Continue = false; } return request; diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index 9230a428d..099dd71bc 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -22,7 +22,8 @@ namespace NzbDrone.Common.Instrumentation RegisterDebugger(); } - RegisterExceptron(); + //Disabling for now - until its fixed or we yank it out + //RegisterExceptron(); if (updateApp) { diff --git a/src/NzbDrone.Common/LevenstheinExtensions.cs b/src/NzbDrone.Common/LevenstheinExtensions.cs new file mode 100644 index 000000000..3bc54d5b2 --- /dev/null +++ b/src/NzbDrone.Common/LevenstheinExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using ICSharpCode.SharpZipLib.Zip; + +namespace NzbDrone.Common +{ + public static class LevenstheinExtensions + { + public static Int32 LevenshteinDistance(this String text, String other, Int32 costInsert = 1, Int32 costDelete = 1, Int32 costSubstitute = 1) + { + if (text == other) return 0; + if (text.Length == 0) return other.Length * costInsert; + if (other.Length == 0) return text.Length * costDelete; + + Int32[] matrix = new Int32[other.Length + 1]; + + for (var i = 1; i < matrix.Length; i++) + { + matrix[i] = i * costInsert; + } + + for (var i = 0; i < text.Length; i++) + { + Int32 topLeft = matrix[0]; + matrix[0] = matrix[0] + costDelete; + + for (var j = 0; j < other.Length; j++) + { + Int32 top = matrix[j]; + Int32 left = matrix[j + 1]; + + var sumIns = top + costInsert; + var sumDel = left + costDelete; + var sumSub = topLeft + (text[i] == other[j] ? 0 : costSubstitute); + + topLeft = matrix[j + 1]; + matrix[j + 1] = Math.Min(Math.Min(sumIns, sumDel), sumSub); + } + } + + return matrix[other.Length]; + } + + public static Int32 LevenshteinDistanceClean(this String expected, String other) + { + expected = expected.ToLower().Replace(".", ""); + other = other.ToLower().Replace(".", ""); + + return expected.LevenshteinDistance(other, 1, 3, 3); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index f2b61218e..bab538ec7 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -60,6 +60,7 @@ + @@ -113,6 +114,7 @@ + diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index a5c171039..7598f84f8 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -39,14 +39,39 @@ namespace NzbDrone.Common public static bool PathEquals(this string firstPath, string secondPath) { - if (OsInfo.IsMono) + if (firstPath.Equals(secondPath, OsInfo.PathStringComparison)) return true; + return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), OsInfo.PathStringComparison); + } + + public static string GetRelativePath(this string parentPath, string childPath) + { + if (!parentPath.IsParentPath(childPath)) { - if (firstPath.Equals(secondPath)) return true; - return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath()); + throw new Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath); } - if (firstPath.Equals(secondPath, StringComparison.OrdinalIgnoreCase)) return true; - return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), StringComparison.OrdinalIgnoreCase); + return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); + } + + public static bool IsParentPath(this string parentPath, string childPath) + { + parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); + childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); + + var parent = new DirectoryInfo(parentPath); + var child = new DirectoryInfo(childPath); + + while (child.Parent != null) + { + if (child.Parent.FullName.Equals(parent.FullName, OsInfo.PathStringComparison)) + { + return true; + } + + child = child.Parent; + } + + return false; } private static readonly Regex WindowsPathWithDriveRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled); diff --git a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs index 35caa1216..5f686eac3 100644 --- a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs @@ -14,6 +14,8 @@ namespace NzbDrone.Core.Test.Configuration public void SetUp() { Mocker.SetConstant(Mocker.Resolve()); + + Db.All().ForEach(Db.Delete); } [Test] diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 48710e6f9..0fc46ab1f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -67,7 +67,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 3)).Returns(null); Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + .Setup(c => c.GetDownloadClients()) + .Returns(new IDownloadClient[] { Mocker.GetMock().Object }); } private void WithFirstReportUpgradable() @@ -83,7 +84,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenSabnzbdDownloadClient() { Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); + .Setup(c => c.GetDownloadClients()) + .Returns(new IDownloadClient[] { Mocker.Resolve() }); } private void GivenMostRecentForEpisode(HistoryEventType eventType) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index 4a3be3627..a41d36669 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Queue; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -18,7 +19,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private Series _series; private Episode _episode; private RemoteEpisode _remoteEpisode; - private Mock _downloadClient; private Series _otherSeries; private Episode _otherEpisode; @@ -50,34 +50,30 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Episodes = new List { _episode }) .With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD)}) .Build(); - - _downloadClient = Mocker.GetMock(); - - Mocker.GetMock() - .Setup(s => s.GetDownloadClient()) - .Returns(_downloadClient.Object); } private void GivenEmptyQueue() { - _downloadClient.Setup(s => s.GetQueue()) - .Returns(new List()); + Mocker.GetMock() + .Setup(s => s.GetQueue()) + .Returns(new List()); } private void GivenQueue(IEnumerable remoteEpisodes) { - var queue = new List(); + var queue = new List(); foreach (var remoteEpisode in remoteEpisodes) { - queue.Add(new QueueItem - { - RemoteEpisode = remoteEpisode + queue.Add(new Queue.Queue + { + RemoteEpisode = remoteEpisode }); } - _downloadClient.Setup(s => s.GetQueue()) - .Returns(queue); + Mocker.GetMock() + .Setup(s => s.GetQueue()) + .Returns(queue); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs new file mode 100644 index 000000000..0d7b6d1e9 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -0,0 +1,439 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Test.Common; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Test.Download +{ + [TestFixture] + public class CompletedDownloadServiceFixture : CoreTest + { + private List _completed; + + [SetUp] + public void Setup() + { + _completed = Builder.CreateListOfSize(1) + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.RemoteEpisode = new RemoteEpisode + { + Episodes = new List { new Episode { Id = 1 } } + }) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(c => c.GetDownloadClients()) + .Returns( new IDownloadClient[] { Mocker.GetMock().Object }); + + Mocker.GetMock() + .SetupGet(c => c.Definition) + .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); + + Mocker.GetMock() + .SetupGet(s => s.EnableCompletedDownloadHandling) + .Returns(true); + + Mocker.GetMock() + .SetupGet(s => s.RemoveCompletedDownloads) + .Returns(true); + + Mocker.GetMock() + .Setup(s => s.Failed()) + .Returns(new List()); + + Mocker.SetConstant(Mocker.Resolve()); + } + + private void GivenNoGrabbedHistory() + { + Mocker.GetMock() + .Setup(s => s.Grabbed()) + .Returns(new List()); + } + + private void GivenGrabbedHistory(List history) + { + Mocker.GetMock() + .Setup(s => s.Grabbed()) + .Returns(history); + } + + private void GivenNoImportedHistory() + { + Mocker.GetMock() + .Setup(s => s.Imported()) + .Returns(new List()); + } + + private void GivenImportedHistory(List importedHistory) + { + Mocker.GetMock() + .Setup(s => s.Imported()) + .Returns(importedHistory); + } + + private void GivenCompletedDownloadClientHistory(bool hasStorage = true) + { + Mocker.GetMock() + .Setup(s => s.GetItems()) + .Returns(_completed); + + Mocker.GetMock() + .Setup(c => c.FolderExists(It.IsAny())) + .Returns(hasStorage); + } + + private void GivenCompletedImport() + { + Mocker.GetMock() + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Returns(new List() { new Core.MediaFiles.EpisodeImport.ImportDecision(null) }); + } + + private void GivenFailedImport() + { + Mocker.GetMock() + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Returns(new List()); + } + + private void VerifyNoImports() + { + Mocker.GetMock() + .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny()), Times.Never()); + } + + private void VerifyImports() + { + Mocker.GetMock() + .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny()), Times.Once()); + } + + [Test] + public void should_process_if_matching_history_is_not_found_but_category_specified() + { + _completed.First().Category = "tv"; + + GivenCompletedDownloadClientHistory(); + GivenNoGrabbedHistory(); + GivenNoImportedHistory(); + GivenCompletedImport(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyImports(); + } + + [Test] + public void should_not_process_if_matching_history_is_not_found_and_no_category_specified() + { + _completed.First().Category = null; + + GivenCompletedDownloadClientHistory(); + GivenNoGrabbedHistory(); + GivenNoImportedHistory(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_not_process_if_grabbed_history_contains_null_downloadclient_id() + { + _completed.First().Category = null; + + GivenCompletedDownloadClientHistory(); + + var historyGrabbed = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", null); + + GivenGrabbedHistory(historyGrabbed); + GivenNoImportedHistory(); + GivenFailedImport(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_process_if_failed_history_contains_null_downloadclient_id() + { + GivenCompletedDownloadClientHistory(); + + var historyGrabbed = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + GivenGrabbedHistory(historyGrabbed); + + var historyImported = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyImported.First().Data.Add("downloadClient", "SabnzbdClient"); + historyImported.First().Data.Add("downloadClientId", null); + + GivenImportedHistory(historyImported); + GivenCompletedImport(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyImports(); + } + + [Test] + public void should_not_process_if_already_added_to_history_as_imported() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenImportedHistory(history); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_process_if_not_already_in_imported_history() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyImports(); + } + + [Test] + public void should_not_process_if_storage_directory_does_not_exist() + { + GivenCompletedDownloadClientHistory(false); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_not_process_if_storage_directory_in_drone_factory() + { + GivenCompletedDownloadClientHistory(true); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + + Mocker.GetMock() + .SetupGet(v => v.DownloadedEpisodesFolder) + .Returns(@"C:\DropFolder".AsOsAgnostic()); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_process_as_already_imported_if_drone_factory_import_history_exists() + { + GivenCompletedDownloadClientHistory(false); + + _completed.Clear(); + _completed.AddRange(Builder.CreateListOfSize(2) + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.RemoteEpisode = new RemoteEpisode + { + Episodes = new List { new Episode { Id = 1 } } + }) + .Build()); + + var grabbedHistory = Builder.CreateListOfSize(2) + .All() + .With(d => d.Data["downloadClient"] = "SabnzbdClient") + .TheFirst(1) + .With(d => d.Data["downloadClientId"] = _completed.First().DownloadClientId) + .With(d => d.SourceTitle = "Droned.S01E01.720p-LAZY") + .TheLast(1) + .With(d => d.Data["downloadClientId"] = _completed.Last().DownloadClientId) + .With(d => d.SourceTitle = "Droned.S01E01.Proper.720p-LAZY") + .Build() + .ToList(); + + var importedHistory = Builder.CreateListOfSize(2) + .All() + .With(d => d.EpisodeId = 1) + .TheFirst(1) + .With(d => d.Data["droppedPath"] = @"C:\mydownload\Droned.S01E01.720p-LAZY\lzy-dr101.mkv".AsOsAgnostic()) + .TheLast(1) + .With(d => d.Data["droppedPath"] = @"C:\mydownload\Droned.S01E01.Proper.720p-LAZY\lzy-dr101.mkv".AsOsAgnostic()) + .Build() + .ToList(); + + GivenGrabbedHistory(grabbedHistory); + GivenImportedHistory(importedHistory); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + + Mocker.GetMock() + .Verify(v => v.UpdateHistoryData(It.IsAny(), It.IsAny>()), Times.Exactly(2)); + } + + [Test] + public void should_not_remove_if_config_disabled() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Mocker.GetMock() + .SetupGet(s => s.RemoveCompletedDownloads) + .Returns(false); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_not_remove_while_readonly() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + _completed.First().IsReadOnly = true; + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_not_remove_if_imported_failed() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenFailedImport(); + + _completed.First().IsReadOnly = true; + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_remove_if_imported() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs new file mode 100644 index 000000000..f19023b9c --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs @@ -0,0 +1,118 @@ +using System.IO; +using System.Net; +using System.Linq; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using NzbDrone.Test.Common; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Download.Clients.UsenetBlackhole; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole +{ + + [TestFixture] + public class UsenetBlackholeFixture : DownloadClientFixtureBase + { + protected string _completedDownloadFolder; + protected string _blackholeFolder; + protected string _filePath; + + [SetUp] + public void Setup() + { + _completedDownloadFolder = @"c:\blackhole\completed".AsOsAgnostic(); + _blackholeFolder = @"c:\blackhole\nzb".AsOsAgnostic(); + _filePath = (@"c:\blackhole\nzb\" + _title + ".nzb").AsOsAgnostic(); + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new UsenetBlackholeSettings + { + NzbFolder = _blackholeFolder, + WatchFolder = _completedDownloadFolder + }; + } + + protected void WithSuccessfulDownload() + { + + } + + protected void WithFailedDownload() + { + Mocker.GetMock() + .Setup(c => c.DownloadFile(It.IsAny(), It.IsAny())) + .Throws(new WebException()); + } + + protected void GivenCompletedItem() + { + var targetDir = Path.Combine(_completedDownloadFolder, _title); + Mocker.GetMock() + .Setup(c => c.GetDirectories(_completedDownloadFolder)) + .Returns(new[] { targetDir }); + + Mocker.GetMock() + .Setup(c => c.GetFiles(targetDir, SearchOption.AllDirectories)) + .Returns(new[] { Path.Combine(_completedDownloadFolder, "somefile.mkv") }); + + Mocker.GetMock() + .Setup(c => c.GetFileSize(It.IsAny())) + .Returns(1000000); + } + + [Test] + public void completed_download_should_have_required_properties() + { + GivenCompletedItem(); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void Download_should_download_file_if_it_doesnt_exist() + { + var remoteEpisode = CreateRemoteEpisode(); + + Subject.Download(remoteEpisode); + + Mocker.GetMock().Verify(c => c.DownloadFile(_downloadUrl, _filePath), Times.Once()); + } + + [Test] + public void Download_should_replace_illegal_characters_in_title() + { + var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]"; + var expectedFilename = Path.Combine(_blackholeFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV]" + Path.GetExtension(_filePath)); + + var remoteEpisode = CreateRemoteEpisode(); + remoteEpisode.Release.Title = illegalTitle; + + Subject.Download(remoteEpisode); + + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename), Times.Once()); + } + + [Test] + public void GetItems_should_considered_locked_files_downloading() + { + GivenCompletedItem(); + + Mocker.GetMock() + .Setup(c => c.IsFileLocked(It.IsAny())) + .Returns(true); + + var result = Subject.GetItems().Single(); + + result.Status.Should().Be(DownloadItemStatus.Downloading); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs deleted file mode 100644 index 6e993c320..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.IO; -using System.Net; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients; -using NzbDrone.Core.Download.Clients.Blackhole; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests -{ - [TestFixture] - public class BlackholeProviderFixture : CoreTest - { - private const string _nzbUrl = "http://www.nzbs.com/url"; - private const string _title = "some_nzb_title"; - private string _blackHoleFolder; - private string _nzbPath; - private RemoteEpisode _remoteEpisode; - - [SetUp] - public void Setup() - { - _blackHoleFolder = @"c:\nzb\blackhole\".AsOsAgnostic(); - _nzbPath = @"c:\nzb\blackhole\some_nzb_title.nzb".AsOsAgnostic(); - - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = _title; - _remoteEpisode.Release.DownloadUrl = _nzbUrl; - - Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new FolderSettings - { - Folder = _blackHoleFolder - }; - } - - private void WithExistingFile() - { - Mocker.GetMock().Setup(c => c.FileExists(_nzbPath)).Returns(true); - } - - private void WithFailedDownload() - { - Mocker.GetMock().Setup(c => c.DownloadFile(It.IsAny(), It.IsAny())).Throws(new WebException()); - } - - [Test] - public void DownloadNzb_should_download_file_if_it_doesnt_exist() - { - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); - } - - [Test] - public void should_replace_illegal_characters_in_title() - { - var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]"; - var expectedFilename = Path.Combine(_blackHoleFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV].nzb"); - _remoteEpisode.Release.Title = illegalTitle; - - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs new file mode 100644 index 000000000..2ba721adb --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -0,0 +1,111 @@ +using System; +using System.Text; +using System.Linq; +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests +{ + public abstract class DownloadClientFixtureBase : CoreTest + where TSubject : class, IDownloadClient + { + protected readonly string _title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"; + protected readonly string _downloadUrl = "http://somewhere.com/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.ext"; + + [SetUp] + public void SetupBase() + { + Mocker.GetMock() + .SetupGet(s => s.DownloadClientHistoryLimit) + .Returns(30); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), null)) + .Returns(CreateRemoteEpisode()); + } + + protected virtual RemoteEpisode CreateRemoteEpisode() + { + var remoteEpisode = new RemoteEpisode(); + remoteEpisode.Release = new ReleaseInfo(); + remoteEpisode.Release.Title = _title; + remoteEpisode.Release.DownloadUrl = _downloadUrl; + remoteEpisode.Release.DownloadProtocol = Subject.Protocol; + + remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo(); + remoteEpisode.ParsedEpisodeInfo.FullSeason = false; + + remoteEpisode.Episodes = new List(); + + remoteEpisode.Series = new Series(); + + return remoteEpisode; + } + + protected void VerifyIdentifiable(DownloadClientItem downloadClientItem) + { + downloadClientItem.DownloadClient.Should().Be(Subject.Definition.Name); + downloadClientItem.DownloadClientId.Should().NotBeNullOrEmpty(); + + downloadClientItem.Title.Should().NotBeNullOrEmpty(); + + downloadClientItem.RemoteEpisode.Should().NotBeNull(); + + } + + protected void VerifyQueued(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + downloadClientItem.RemainingSize.Should().NotBe(0); + //downloadClientItem.RemainingTime.Should().NotBe(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Queued); + } + + protected void VerifyPaused(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.RemainingSize.Should().NotBe(0); + //downloadClientItem.RemainingTime.Should().NotBe(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Paused); + } + + protected void VerifyDownloading(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.RemainingSize.Should().NotBe(0); + //downloadClientItem.RemainingTime.Should().NotBe(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Downloading); + } + + protected void VerifyCompleted(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.Title.Should().NotBeNullOrEmpty(); + downloadClientItem.RemainingSize.Should().Be(0); + downloadClientItem.RemainingTime.Should().Be(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Completed); + } + + protected void VerifyFailed(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.Status.Should().Be(DownloadItemStatus.Failed); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs deleted file mode 100644 index 848bc237e..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients.Nzbget; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests -{ - public class DownloadNzbFixture : CoreTest - { - private const string _url = "http://www.nzbdrone.com"; - private const string _title = "30.Rock.S01E01.Pilot.720p.hdtv"; - private RemoteEpisode _remoteEpisode; - - [SetUp] - public void Setup() - { - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = _title; - _remoteEpisode.Release.DownloadUrl = _url; - - _remoteEpisode.Episodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) - .Build() - .ToList(); - - Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new NzbgetSettings - { - Host = "localhost", - Port = 6789, - Username = "nzbget", - Password = "pass", - TvCategory = "tv", - RecentTvPriority = (int)NzbgetPriority.High - }; - } - - [Test] - public void should_add_item_to_queue() - { - Mocker.GetMock() - .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns("id"); - - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock() - .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs new file mode 100644 index 000000000..0dffee397 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -0,0 +1,227 @@ +using System; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Nzbget; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using System.Collections.Generic; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests +{ + [TestFixture] + public class NzbgetFixture : DownloadClientFixtureBase + { + private NzbgetQueueItem _queued; + private NzbgetHistoryItem _failed; + private NzbgetHistoryItem _completed; + + [SetUp] + public void Setup() + { + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new NzbgetSettings + { + Host = "192.168.5.55", + Port = 2222, + Username = "admin", + Password = "pass", + TvCategory = "tv", + RecentTvPriority = (int)NzbgetPriority.High + }; + + _queued = new NzbgetQueueItem + { + FileSizeLo = 1000, + RemainingSizeLo = 10, + Category = "tv", + NzbName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + Parameters = new List { new NzbgetParameter { Name = "drone", Value = "id" } } + }; + + _failed = new NzbgetHistoryItem + { + FileSizeLo = 1000, + Category = "tv", + Name = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + DestDir = "somedirectory", + Parameters = new List { new NzbgetParameter { Name = "drone", Value = "id" } }, + ParStatus = "Some Error", + UnpackStatus = "NONE", + MoveStatus = "NONE", + ScriptStatus = "NONE", + DeleteStatus = "NONE", + MarkStatus = "NONE" + }; + + _completed = new NzbgetHistoryItem + { + FileSizeLo = 1000, + Category = "tv", + Name = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + DestDir = "somedirectory", + Parameters = new List { new NzbgetParameter { Name = "drone", Value = "id" } }, + ParStatus = "SUCCESS", + UnpackStatus = "NONE", + MoveStatus = "SUCCESS", + ScriptStatus = "NONE", + DeleteStatus = "NONE", + MarkStatus = "NONE" + }; + + Mocker.GetMock() + .Setup(s => s.GetGlobalStatus(It.IsAny())) + .Returns(new NzbgetGlobalStatus + { + DownloadRate = 7000000 + }); + } + + protected void WithFailedDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((String)null); + } + + protected void WithSuccessfulDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Guid.NewGuid().ToString().Replace("-", "")); + } + + protected virtual void WithQueue(NzbgetQueueItem queue) + { + var list = new List(); + + if (queue != null) + { + list.Add(queue); + } + + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny())) + .Returns(list); + + Mocker.GetMock() + .Setup(s => s.GetPostQueue(It.IsAny())) + .Returns(new List()); + } + + protected virtual void WithHistory(NzbgetHistoryItem history) + { + var list = new List(); + + if (history != null) + { + list.Add(history); + } + + Mocker.GetMock() + .Setup(s => s.GetHistory(It.IsAny())) + .Returns(list); + } + + [Test] + public void GetItems_should_return_no_items_when_queue_is_empty() + { + WithQueue(null); + WithHistory(null); + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void queued_item_should_have_required_properties() + { + _queued.ActiveDownloads = 0; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyQueued(result); + } + + [Test] + public void paused_item_should_have_required_properties() + { + _queued.PausedSizeLo = _queued.RemainingSizeLo; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyPaused(result); + } + + [Test] + public void downloading_item_should_have_required_properties() + { + _queued.ActiveDownloads = 1; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyDownloading(result); + } + + [Test] + public void completed_download_should_have_required_properties() + { + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void failed_item_should_have_required_properties() + { + WithQueue(null); + WithHistory(_failed); + + var result = Subject.GetItems().Single(); + + VerifyFailed(result); + } + + [Test] + public void Download_should_return_unique_id() + { + WithSuccessfulDownload(); + + var remoteEpisode = CreateRemoteEpisode(); + + var id = Subject.Download(remoteEpisode); + + id.Should().NotBeNullOrEmpty(); + } + + [Test] + public void GetItems_should_ignore_downloads_from_other_categories() + { + _completed.Category = "mycat"; + + WithQueue(null); + WithHistory(_completed); + + var items = Subject.GetItems(); + + items.Should().BeEmpty(); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs deleted file mode 100644 index ec7befef0..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients.Nzbget; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests -{ - public class QueueFixture : CoreTest - { - private List _queue; - - [SetUp] - public void Setup() - { - _queue = Builder.CreateListOfSize(5) - .All() - .With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb") - .With(q => q.Parameters = new List - { - new NzbgetParameter { Name = "drone", Value = "id" } - }) - .Build() - .ToList(); - - Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new NzbgetSettings - { - Host = "localhost", - Port = 6789, - Username = "nzbget", - Password = "pass", - TvCategory = "tv", - RecentTvPriority = (int)NzbgetPriority.High - }; - } - - private void WithFullQueue() - { - Mocker.GetMock() - .Setup(s => s.GetQueue(It.IsAny())) - .Returns(_queue); - } - - private void WithEmptyQueue() - { - Mocker.GetMock() - .Setup(s => s.GetQueue(It.IsAny())) - .Returns(new List()); - } - - [Test] - public void should_return_no_items_when_queue_is_empty() - { - WithEmptyQueue(); - - Subject.GetQueue() - .Should() - .BeEmpty(); - } - - [Test] - public void should_return_item_when_queue_has_item() - { - WithFullQueue(); - - Mocker.GetMock() - .Setup(s => s.Map(It.IsAny(), 0, null)) - .Returns(new RemoteEpisode {Series = new Series()}); - - Subject.GetQueue() - .Should() - .HaveCount(_queue.Count); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index 5903fa993..a0e33f1bd 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -45,9 +45,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _remoteEpisode.ParsedEpisodeInfo.FullSeason = false; Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new FolderSettings + Subject.Definition.Settings = new PneumaticSettings { - Folder = _pneumaticFolder + NzbFolder = _pneumaticFolder }; } @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests [Test] public void should_download_file_if_it_doesnt_exist() { - Subject.DownloadNzb(_remoteEpisode); + Subject.Download(_remoteEpisode); Mocker.GetMock().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); } @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests { WithFailedDownload(); - Assert.Throws(() => Subject.DownloadNzb(_remoteEpisode)); + Assert.Throws(() => Subject.Download(_remoteEpisode)); } [Test] @@ -84,7 +84,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _remoteEpisode.Release.Title = "30 Rock - Season 1"; _remoteEpisode.ParsedEpisodeInfo.FullSeason = true; - Assert.Throws(() => Subject.DownloadNzb(_remoteEpisode)); + Assert.Throws(() => Subject.Download(_remoteEpisode)); + } + + [Test] + public void should_throw_item_is_removed() + { + Assert.Throws(() => Subject.RemoveItem("")); } [Test] @@ -94,7 +100,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests var expectedFilename = Path.Combine(_pneumaticFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV].nzb"); _remoteEpisode.Release.Title = illegalTitle; - Subject.DownloadNzb(_remoteEpisode); + Subject.Download(_remoteEpisode); Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename), Times.Once()); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index fe9529ef1..3fd83618e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -12,30 +13,20 @@ using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests { [TestFixture] - public class SabnzbdFixture : CoreTest + public class SabnzbdFixture : DownloadClientFixtureBase { - private const string URL = "http://www.nzbclub.com/nzb_download.aspx?mid=1950232"; - private const string TITLE = "My Series Name - 5x2-5x3 - My title [Bluray720p] [Proper]"; - private RemoteEpisode _remoteEpisode; + private SabnzbdQueue _queued; + private SabnzbdHistory _failed; + private SabnzbdHistory _completed; [SetUp] public void Setup() { - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = TITLE; - _remoteEpisode.Release.DownloadUrl = URL; - - _remoteEpisode.Episodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) - .Build() - .ToList(); - Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new SabnzbdSettings { @@ -47,19 +38,248 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests TvCategory = "tv", RecentTvPriority = (int)SabnzbdPriority.High }; + _queued = new SabnzbdQueue + { + Paused = false, + Items = new List() + { + new SabnzbdQueueItem + { + Status = SabnzbdDownloadStatus.Downloading, + Size = 1000, + Sizeleft = 10, + Timeleft = TimeSpan.FromSeconds(10), + Category = "tv", + Id = "sabnzbd_nzb12345", + Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE" + } + } + }; + + _failed = new SabnzbdHistory + { + Items = new List() + { + new SabnzbdHistoryItem + { + Status = SabnzbdDownloadStatus.Failed, + Size = 1000, + Category = "tv", + Id = "sabnzbd_nzb12345", + Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE" + } + } + }; + + _completed = new SabnzbdHistory + { + Items = new List() + { + new SabnzbdHistoryItem + { + Status = SabnzbdDownloadStatus.Completed, + Size = 1000, + Category = "tv", + Id = "sabnzbd_nzb12345", + Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + Storage = "somedirectory" + } + } + }; + } + + protected void WithFailedDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((SabnzbdAddResponse)null); + } + + protected void WithSuccessfulDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SabnzbdAddResponse() + { + Status = true, + Ids = new List { "sabznbd_nzo12345" } + }); + } + + protected virtual void WithQueue(SabnzbdQueue queue) + { + if (queue == null) + { + queue = new SabnzbdQueue() { Items = new List() }; + } + + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(queue); + } + + protected virtual void WithHistory(SabnzbdHistory history) + { + if (history == null) + history = new SabnzbdHistory() { Items = new List() }; + + Mocker.GetMock() + .Setup(s => s.GetHistory(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(history); } [Test] - public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_true() + public void GetItems_should_return_no_items_when_queue_is_empty() + { + WithQueue(null); + WithHistory(null); + + Subject.GetItems().Should().BeEmpty(); + } + + [TestCase(SabnzbdDownloadStatus.Grabbing)] + [TestCase(SabnzbdDownloadStatus.Queued)] + public void queued_item_should_have_required_properties(SabnzbdDownloadStatus status) + { + _queued.Items.First().Status = status; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyQueued(result); + result.RemainingTime.Should().NotBe(TimeSpan.Zero); + } + + [TestCase(SabnzbdDownloadStatus.Paused)] + public void paused_item_should_have_required_properties(SabnzbdDownloadStatus status) + { + _queued.Items.First().Status = status; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyPaused(result); + } + + [TestCase(SabnzbdDownloadStatus.Checking)] + [TestCase(SabnzbdDownloadStatus.Downloading)] + [TestCase(SabnzbdDownloadStatus.QuickCheck)] + [TestCase(SabnzbdDownloadStatus.Verifying)] + [TestCase(SabnzbdDownloadStatus.Repairing)] + [TestCase(SabnzbdDownloadStatus.Fetching)] + [TestCase(SabnzbdDownloadStatus.Extracting)] + [TestCase(SabnzbdDownloadStatus.Moving)] + [TestCase(SabnzbdDownloadStatus.Running)] + public void downloading_item_should_have_required_properties(SabnzbdDownloadStatus status) + { + _queued.Items.First().Status = status; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyDownloading(result); + result.RemainingTime.Should().NotBe(TimeSpan.Zero); + } + + [Test] + public void completed_download_should_have_required_properties() + { + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void failed_item_should_have_required_properties() + { + _completed.Items.First().Status = SabnzbdDownloadStatus.Failed; + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + VerifyFailed(result); + } + + [Test] + public void Download_should_return_unique_id() + { + WithSuccessfulDownload(); + + var remoteEpisode = CreateRemoteEpisode(); + + var id = Subject.Download(remoteEpisode); + + id.Should().NotBeNullOrEmpty(); + } + + [Test] + public void GetItems_should_ignore_downloads_from_other_categories() + { + _completed.Items.First().Category = "myowncat"; + + WithQueue(null); + WithHistory(_completed); + + var items = Subject.GetItems(); + + items.Should().BeEmpty(); + } + + [Test] + public void Download_should_use_sabRecentTvPriority_when_recentEpisode_is_true() { Mocker.GetMock() .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabnzbdPriority.High, It.IsAny())) .Returns(new SabnzbdAddResponse()); - Subject.DownloadNzb(_remoteEpisode); + var remoteEpisode = CreateRemoteEpisode(); + remoteEpisode.Episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) + .Build() + .ToList(); + + Subject.Download(remoteEpisode); Mocker.GetMock() .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabnzbdPriority.High, It.IsAny()), Times.Once()); } + + [Test] + public void should_return_path_to_folder_instead_of_file() + { + _completed.Items.First().Storage = @"C:\sorted\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE\Droned.S01E01_Pilot_1080p_WEB-DL-DRONE.mkv".AsOsAgnostic(); + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"C:\sorted\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + } + + [Test] + public void should_not_blow_up_if_storage_is_drive_root() + { + _completed.Items.First().Storage = @"C:\".AsOsAgnostic(); + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"C:\".AsOsAgnostic()); + } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index 0d3755468..eb9736870 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using System.Collections.Generic; namespace NzbDrone.Core.Test.Download { @@ -16,12 +17,19 @@ namespace NzbDrone.Core.Test.Download public class DownloadServiceFixture : CoreTest { private RemoteEpisode _parseResult; - + private List _downloadClients; [SetUp] public void Setup() { + _downloadClients = new List(); + Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + .Setup(v => v.GetDownloadClients()) + .Returns(_downloadClients); + + Mocker.GetMock() + .Setup(v => v.GetDownloadClient(It.IsAny())) + .Returns(v => _downloadClients.FirstOrDefault(d => d.Protocol == v)); var episodes = Builder.CreateListOfSize(2) .TheFirst(1).With(s => s.Id = 12) @@ -29,31 +37,43 @@ namespace NzbDrone.Core.Test.Download .All().With(s => s.SeriesId = 5) .Build().ToList(); + var releaseInfo = Builder.CreateNew() + .With(v => v.DownloadProtocol = Indexers.DownloadProtocol.Usenet) + .Build(); + _parseResult = Builder.CreateNew() .With(c => c.Series = Builder.CreateNew().Build()) - .With(c => c.Release = Builder.CreateNew().Build()) + .With(c => c.Release = releaseInfo) .With(c => c.Episodes = episodes) .Build(); } - private void WithSuccessfulAdd() + private Mock WithUsenetClient() { - Mocker.GetMock() - .Setup(s => s.DownloadNzb(It.IsAny())); + var mock = new Mock(Moq.MockBehavior.Default); + _downloadClients.Add(mock.Object); + + mock.SetupGet(v => v.Protocol).Returns(Indexers.DownloadProtocol.Usenet); + + return mock; } - private void WithFailedAdd() + private Mock WithTorrentClient() { - Mocker.GetMock() - .Setup(s => s.DownloadNzb(It.IsAny())) - .Throws(new WebException()); + var mock = new Mock(Moq.MockBehavior.Default); + _downloadClients.Add(mock.Object); + + mock.SetupGet(v => v.Protocol).Returns(Indexers.DownloadProtocol.Torrent); + + return mock; } [Test] public void Download_report_should_publish_on_grab_event() { - WithSuccessfulAdd(); - + var mock = WithUsenetClient(); + mock.Setup(s => s.Download(It.IsAny())); + Subject.DownloadReport(_parseResult); VerifyEventPublished(); @@ -62,18 +82,20 @@ namespace NzbDrone.Core.Test.Download [Test] public void Download_report_should_grab_using_client() { - WithSuccessfulAdd(); - + var mock = WithUsenetClient(); + mock.Setup(s => s.Download(It.IsAny())); + Subject.DownloadReport(_parseResult); - Mocker.GetMock() - .Verify(s => s.DownloadNzb(It.IsAny()), Times.Once()); + mock.Verify(s => s.Download(It.IsAny()), Times.Once()); } [Test] public void Download_report_should_not_publish_on_failed_grab_event() { - WithFailedAdd(); + var mock = WithUsenetClient(); + mock.Setup(s => s.Download(It.IsAny())) + .Throws(new WebException()); Assert.Throws(() => Subject.DownloadReport(_parseResult)); @@ -83,15 +105,38 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_attempt_download_if_client_isnt_configure() { - Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns((IDownloadClient)null); - Subject.DownloadReport(_parseResult); - Mocker.GetMock().Verify(c => c.DownloadNzb(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.Download(It.IsAny()), Times.Never()); VerifyEventNotPublished(); ExceptionVerification.ExpectedWarns(1); } + + [Test] + public void should_send_download_to_correct_usenet_client() + { + var mockTorrent = WithTorrentClient(); + var mockUsenet = WithUsenetClient(); + + Subject.DownloadReport(_parseResult); + + mockTorrent.Verify(c => c.Download(It.IsAny()), Times.Never()); + mockUsenet.Verify(c => c.Download(It.IsAny()), Times.Once()); + } + + [Test] + public void should_send_download_to_correct_torrent_client() + { + var mockTorrent = WithTorrentClient(); + var mockUsenet = WithUsenetClient(); + + _parseResult.Release.DownloadProtocol = Indexers.DownloadProtocol.Torrent; + + Subject.DownloadReport(_parseResult); + + mockTorrent.Verify(c => c.Download(It.IsAny()), Times.Once()); + mockUsenet.Verify(c => c.Download(It.IsAny()), Times.Never()); + } } } diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs index dec70e91f..44e563718 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -13,32 +13,43 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Download { [TestFixture] - public class FailedDownloadServiceFixture : CoreTest + public class FailedDownloadServiceFixture : CoreTest { - private List _completed; - private List _failed; + private List _completed; + private List _failed; [SetUp] public void Setup() { - _completed = Builder.CreateListOfSize(5) + _completed = Builder.CreateListOfSize(5) .All() - .With(h => h.Status = HistoryStatus.Completed) + .With(h => h.Status = DownloadItemStatus.Completed) .Build() .ToList(); - _failed = Builder.CreateListOfSize(1) + _failed = Builder.CreateListOfSize(1) .All() - .With(h => h.Status = HistoryStatus.Failed) + .With(h => h.Status = DownloadItemStatus.Failed) .Build() .ToList(); Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + .Setup(c => c.GetDownloadClients()) + .Returns( new IDownloadClient[] { Mocker.GetMock().Object }); + + Mocker.GetMock() + .SetupGet(c => c.Definition) + .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); Mocker.GetMock() .SetupGet(s => s.EnableFailedDownloadHandling) .Returns(true); + + Mocker.GetMock() + .Setup(s => s.Imported()) + .Returns(new List()); + + Mocker.SetConstant(Mocker.Resolve()); } private void GivenNoGrabbedHistory() @@ -72,7 +83,7 @@ namespace NzbDrone.Core.Test.Download private void GivenFailedDownloadClientHistory() { Mocker.GetMock() - .Setup(s => s.GetHistory(0, 20)) + .Setup(s => s.GetItems()) .Returns(_failed); } @@ -102,10 +113,10 @@ namespace NzbDrone.Core.Test.Download public void should_not_process_if_no_download_client_history() { Mocker.GetMock() - .Setup(s => s.GetHistory(0, 20)) - .Returns(new List()); + .Setup(s => s.GetItems()) + .Returns(new List()); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); Mocker.GetMock() .Verify(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed), @@ -117,11 +128,14 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_process_if_no_failed_items_in_download_client_history() { + GivenNoGrabbedHistory(); + GivenNoFailedHistory(); + Mocker.GetMock() - .Setup(s => s.GetHistory(0, 20)) + .Setup(s => s.GetItems()) .Returns(_completed); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); Mocker.GetMock() .Verify(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed), @@ -136,7 +150,7 @@ namespace NzbDrone.Core.Test.Download GivenNoGrabbedHistory(); GivenFailedDownloadClientHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -156,7 +170,7 @@ namespace NzbDrone.Core.Test.Download GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -171,7 +185,7 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); GivenGrabbedHistory(historyGrabbed); @@ -184,7 +198,7 @@ namespace NzbDrone.Core.Test.Download GivenFailedHistory(historyFailed); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -202,9 +216,9 @@ namespace NzbDrone.Core.Test.Download GivenFailedHistory(history); history.First().Data.Add("downloadClient", "SabnzbdClient"); - history.First().Data.Add("downloadClientId", _failed.First().Id); + history.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -222,9 +236,9 @@ namespace NzbDrone.Core.Test.Download GivenNoFailedHistory(); history.First().Data.Add("downloadClient", "SabnzbdClient"); - history.First().Data.Add("downloadClientId", _failed.First().Id); + history.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -244,10 +258,10 @@ namespace NzbDrone.Core.Test.Download history.ForEach(h => { h.Data.Add("downloadClient", "SabnzbdClient"); - h.Data.Add("downloadClientId", _failed.First().Id); + h.Data.Add("downloadClientId", _failed.First().DownloadClientId); }); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(2); } @@ -259,7 +273,7 @@ namespace NzbDrone.Core.Test.Download .SetupGet(s => s.EnableFailedDownloadHandling) .Returns(false); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -276,7 +290,7 @@ namespace NzbDrone.Core.Test.Download _failed.First().Message = "Unpacking failed, write error or disk is full?"; - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -291,12 +305,12 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -311,13 +325,13 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); historyGrabbed.First().Data.Add("ageHours", "48"); GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -332,14 +346,14 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); historyGrabbed.First().Data.Add("ageHours", "48"); GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); GivenGracePeriod(6); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -354,7 +368,7 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); historyGrabbed.First().Data.Add("ageHours", "1"); GivenGrabbedHistory(historyGrabbed); @@ -362,7 +376,7 @@ namespace NzbDrone.Core.Test.Download GivenGracePeriod(6); GivenRetryLimit(1); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs new file mode 100644 index 000000000..ade6278e3 --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class AppDataLocationFixture : CoreTest + { + [Test] + public void should_return_warning_when_app_data_is_child_of_startup_folder() + { + Mocker.GetMock() + .Setup(s => s.StartUpFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Mocker.GetMock() + .Setup(s => s.AppDataFolder) + .Returns(@"C:\NzbDrone\AppData".AsOsAgnostic()); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_warning_when_app_data_is_same_as_startup_folder() + { + Mocker.GetMock() + .Setup(s => s.StartUpFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Mocker.GetMock() + .Setup(s => s.AppDataFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_ok_when_no_conflict() + { + Mocker.GetMock() + .Setup(s => s.StartUpFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Mocker.GetMock() + .Setup(s => s.AppDataFolder) + .Returns(@"C:\ProgramData\NzbDrone".AsOsAgnostic()); + + Subject.Check().ShouldBeOk(); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs index 9b0982976..a573d2662 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs @@ -15,8 +15,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_return_warning_when_download_client_has_not_been_configured() { Mocker.GetMock() - .Setup(s => s.GetDownloadClient()) - .Returns((IDownloadClient)null); + .Setup(s => s.GetDownloadClients()) + .Returns(new IDownloadClient[0]); Subject.Check().ShouldBeWarning(); } @@ -26,12 +26,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var downloadClient = Mocker.GetMock(); - downloadClient.Setup(s => s.GetQueue()) + downloadClient.Setup(s => s.GetItems()) .Throws(); Mocker.GetMock() - .Setup(s => s.GetDownloadClient()) - .Returns(downloadClient.Object); + .Setup(s => s.GetDownloadClients()) + .Returns(new IDownloadClient[] { downloadClient.Object }); Subject.Check().ShouldBeError(); } @@ -41,12 +41,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var downloadClient = Mocker.GetMock(); - downloadClient.Setup(s => s.GetQueue()) - .Returns(new List()); + downloadClient.Setup(s => s.GetItems()) + .Returns(new List()); Mocker.GetMock() - .Setup(s => s.GetDownloadClient()) - .Returns(downloadClient.Object); + .Setup(s => s.GetDownloadClients()) + .Returns(new IDownloadClient[] { downloadClient.Object }); Subject.Check().ShouldBeOk(); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs index c3a9ef0c3..b21d29eae 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs @@ -25,17 +25,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER)) .Returns(exists); } - - [Test] - public void should_return_warning_when_drone_factory_folder_is_not_configured() - { - Mocker.GetMock() - .SetupGet(s => s.DownloadedEpisodesFolder) - .Returns(""); - - Subject.Check().ShouldBeWarning(); - } - + [Test] public void should_return_error_when_drone_factory_folder_does_not_exist() { diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs new file mode 100644 index 000000000..4e0724e84 --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using Moq; +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Test.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Download; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class ImportMechanismCheckFixture : CoreTest + { + private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted"; + + private IList _completed; + + private void GivenCompletedDownloadHandling(bool? enabled = null) + { + if (enabled.HasValue) + { + Mocker.GetMock() + .Setup(s => s.IsDefined("EnableCompletedDownloadHandling")) + .Returns(true); + + Mocker.GetMock() + .SetupGet(s => s.EnableCompletedDownloadHandling) + .Returns(enabled.Value); + } + + _completed = Builder.CreateListOfSize(1) + .All() + .With(v => v.State == TrackedDownloadState.Downloading) + .With(v => v.DownloadItem = new DownloadClientItem()) + .With(v => v.DownloadItem.Status = DownloadItemStatus.Completed) + .With(v => v.DownloadItem.OutputPath = @"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic()) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetCompletedDownloads()) + .Returns(_completed.ToArray()); + } + + private void GivenDroneFactoryFolder(bool exists = false) + { + Mocker.GetMock() + .SetupGet(s => s.DownloadedEpisodesFolder) + .Returns(DRONE_FACTORY_FOLDER.AsOsAgnostic()); + + Mocker.GetMock() + .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER.AsOsAgnostic())) + .Returns(exists); + } + + [Test] + public void should_return_warning_when_completed_download_handling_not_configured() + { + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_warning_when_both_completeddownloadhandling_and_dronefactory_are_not_configured() + { + GivenCompletedDownloadHandling(false); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_warning_when_downloadclient_drops_in_dronefactory_folder() + { + GivenCompletedDownloadHandling(true); + GivenDroneFactoryFolder(true); + + _completed.First().DownloadItem.OutputPath = (DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic(); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_ok_when_no_issues_found() + { + GivenCompletedDownloadHandling(true); + GivenDroneFactoryFolder(true); + + Subject.Check().ShouldBeOk(); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs index 7e6fd213c..68123676a 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs @@ -1,10 +1,9 @@ using System; -using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.HealthCheck; +using NzbDrone.Core.Configuration; using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.Test.Framework; @@ -28,5 +27,25 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Subject.Check().ShouldBeError(); } + + [Test] + public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled() + { + MonoOnly(); + + Mocker.GetMock() + .Setup(s => s.UpdateAutomatically) + .Returns(true); + + Mocker.GetMock() + .Setup(s => s.StartUpFolder) + .Returns(@"/opt/nzbdrone"); + + Mocker.GetMock() + .Setup(s => s.WriteAllText(It.IsAny(), It.IsAny())) + .Throws(); + + Subject.Check().ShouldBeError(); + } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs index 6a7a65736..ae5915799 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs @@ -29,37 +29,6 @@ namespace NzbDrone.Core.Test.IndexerTests Mocker.SetConstant>(_indexers); } - [Test] - public void should_create_default_indexer_on_startup() - { - IList storedIndexers = null; - - Mocker.GetMock() - .Setup(c => c.InsertMany(It.IsAny>())) - .Callback>(indexers => storedIndexers = indexers); - - Subject.Handle(new ApplicationStartedEvent()); - - storedIndexers.Should().NotBeEmpty(); - storedIndexers.Select(c => c.Name).Should().OnlyHaveUniqueItems(); - storedIndexers.Select(c => c.Enable).Should().NotBeEmpty(); - storedIndexers.Select(c => c.Implementation).Should().NotContainNulls(); - } - - [Test] - public void getting_list_of_indexers() - { - Mocker.SetConstant(Mocker.Resolve()); - - Subject.Handle(new ApplicationStartedEvent()); - - var indexers = Subject.All().ToList(); - indexers.Should().NotBeEmpty(); - indexers.Should().NotContain(c => c.Settings == null); - indexers.Should().NotContain(c => c.Name == null); - indexers.Select(c => c.Name).Should().OnlyHaveUniqueItems(); - } - [Test] public void should_remove_missing_indexers_on_startup() { diff --git a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs index 1d12f233e..83047365c 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using FluentAssertions; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Eztv; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Wombles; using NzbDrone.Core.Parser.Model; @@ -37,39 +36,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests ValidateResult(result, skipSize: true, skipInfo: true); } - - [Test] - public void extv_rss() - { - var indexer = new Eztv(); - indexer.Definition = new IndexerDefinition - { - Name = "Eztv", - Settings = NullConfig.Instance - }; - - var result = Subject.FetchRss(indexer); - - ValidateTorrentResult(result, skipSize: false, skipInfo: true); - } - - [Test] - public void nzbsorg_rss() - { - var indexer = new Newznab(); - - indexer.Definition = new IndexerDefinition(); - indexer.Definition.Name = "nzbs.org"; - indexer.Definition.Settings = new NewznabSettings - { - ApiKey = "64d61d3cfd4b75e51d01cbc7c6a78275", - Url = "http://nzbs.org" - }; - - var result = Subject.FetchRss(indexer); - - ValidateResult(result); - } private void ValidateResult(IList reports, bool skipSize = false, bool skipInfo = false) { diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs index 7fb252ff3..74fda4de8 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests indexer.Setup(s => s.GetSeasonSearchUrls(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { "http://www.nzbdrone.com" }); - indexer.SetupGet(s => s.SupportsPaging).Returns(paging); + indexer.SetupGet(s => s.SupportedPageSize).Returns(paging ? 100 : 0); var definition = new IndexerDefinition(); definition.Name = "Test"; diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index a0eecb805..3cc90f748 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns("c:\\drop\\".AsOsAgnostic()); Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true)) + .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(new List()); } @@ -77,6 +77,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_skip_if_file_is_in_use_by_another_process() { + GivenValidSeries(); + Mocker.GetMock().Setup(c => c.IsFileLocked(It.IsAny())) .Returns(true); @@ -122,7 +124,7 @@ namespace NzbDrone.Core.Test.MediaFiles public void should_not_delete_folder_if_no_files_were_imported() { Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), false)) + .Setup(s => s.Import(It.IsAny>(), false, null)) .Returns(new List()); Subject.Execute(new DownloadedEpisodesScanCommand()); @@ -132,7 +134,7 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_delete_folder_if_files_were_imported_and_video_files_remain() + public void should_not_delete_folder_if_files_were_imported_and_video_files_remain() { GivenValidSeries(); @@ -146,7 +148,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(imported); Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true)) + .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(imported); Subject.Execute(new DownloadedEpisodesScanCommand()); @@ -172,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(imported); Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true)) + .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(imported); Mocker.GetMock() @@ -211,13 +213,13 @@ namespace NzbDrone.Core.Test.MediaFiles private void VerifyNoImport() { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true), + Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null), Times.Never()); } private void VerifyImport() { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true), + Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null), Times.Once()); } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs deleted file mode 100644 index 2ede2be18..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs +++ /dev/null @@ -1,75 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications -{ - [TestFixture] - public class NotInUseSpecificationFixture : CoreTest - { - private LocalEpisode _localEpisode; - - [SetUp] - public void Setup() - { - _localEpisode = new LocalEpisode - { - Path = @"C:\Test\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(), - Size = 100, - Series = Builder.CreateNew().Build() - }; - } - - private void GivenChildOfSeries() - { - _localEpisode.ExistingFile = true; - } - - [Test] - public void should_return_true_if_file_is_under_series_folder() - { - GivenChildOfSeries(); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_not_check_for_file_in_use_if_child_of_series_folder() - { - GivenChildOfSeries(); - - Subject.IsSatisfiedBy(_localEpisode); - - Mocker.GetMock() - .Verify(v => v.IsFileLocked(It.IsAny()), Times.Never()); - } - - [Test] - public void should_return_false_if_file_is_in_use() - { - Mocker.GetMock() - .Setup(s => s.IsFileLocked(It.IsAny())) - .Returns(true); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - } - - [Test] - public void should_return_true_if_file_is_not_in_use() - { - Mocker.GetMock() - .Setup(s => s.IsFileLocked(It.IsAny())) - .Returns(false); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs index 155420917..57c444521 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications private void GivenInWorkingFolder() { - _localEpisode.Path = @"C:\Test\Unsorted TV\_UNPACK_30.rock\30.rock.s01e01.avi".AsOsAgnostic(); + _localEpisode.Path = @"C:\Test\Unsorted TV\_UNPACK_30.rock\someSubFolder\30.rock.s01e01.avi".AsOsAgnostic(); } private void GivenLastWriteTimeUtc(DateTime time) diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index a1c9a22e1..904a0e5c7 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -57,20 +57,20 @@ namespace NzbDrone.Core.Test.MediaFiles } Mocker.GetMock() - .Setup(s => s.UpgradeEpisodeFile(It.IsAny(), It.IsAny())) + .Setup(s => s.UpgradeEpisodeFile(It.IsAny(), It.IsAny(), false)) .Returns(new EpisodeFileMoveResult()); } [Test] public void should_return_empty_list_if_there_are_no_approved_decisions() { - Subject.Import(_rejectedDecisions).Should().BeEmpty(); + Subject.Import(_rejectedDecisions, false).Should().BeEmpty(); } [Test] public void should_import_each_approved() { - Subject.Import(_approvedDecisions).Should().HaveCount(5); + Subject.Import(_approvedDecisions, false).Should().HaveCount(5); } [Test] @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.MediaFiles all.AddRange(_rejectedDecisions); all.AddRange(_approvedDecisions); - Subject.Import(all).Should().HaveCount(5); + Subject.Import(all, false).Should().HaveCount(5); } [Test] @@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.MediaFiles all.AddRange(_approvedDecisions); all.Add(new ImportDecision(_approvedDecisions.First().LocalEpisode)); - Subject.Import(all).Should().HaveCount(5); + Subject.Import(all, false).Should().HaveCount(5); } [Test] @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Import(new List {_approvedDecisions.First()}, true); Mocker.GetMock() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode), + .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode, false), Times.Once()); } @@ -115,10 +115,10 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_not_move_existing_files() { - Subject.Import(new List { _approvedDecisions.First() }); + Subject.Import(new List { _approvedDecisions.First() }, false); Mocker.GetMock() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode), + .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode, false), Times.Never()); } @@ -143,7 +143,7 @@ namespace NzbDrone.Core.Test.MediaFiles all.Add(fileDecision); all.Add(sampleDecision); - var results = Subject.Import(all); + var results = Subject.Import(all, false); results.Should().HaveCount(1); results.Should().ContainSingle(d => d.LocalEpisode.Size == fileDecision.LocalEpisode.Size); diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs new file mode 100644 index 000000000..06cf6fb51 --- /dev/null +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs @@ -0,0 +1,78 @@ +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Metadata; +using NzbDrone.Core.Metadata.Consumers.Roksbox; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [TestCase("Specials")] + [TestCase("specials")] + [TestCase("Season 1")] + public void should_return_season_image(string folder) + { + var path = Path.Combine(_series.Path, folder, folder + ".jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + } + + [TestCase(".xml", MetadataType.EpisodeMetadata)] + [TestCase(".jpg", MetadataType.EpisodeImage)] + public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(type); + } + + [TestCase(".xml")] + [TestCase(".jpg")] + public void should_return_null_if_not_valid_file_for_episode(string extension) + { + var path = Path.Combine(_series.Path, "the.series.episode" + extension); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_not_return_metadata_if_image_file_is_a_thumb() + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode-thumb.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_series_image_for_folder_jpg_in_series_folder() + { + var path = Path.Combine(_series.Path, new DirectoryInfo(_series.Path).Name + ".jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + } + } +} diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs new file mode 100644 index 000000000..4bdbecc1a --- /dev/null +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Metadata; +using NzbDrone.Core.Metadata.Consumers.Wdtv; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [TestCase("Specials")] + [TestCase("specials")] + [TestCase("Season 1")] + public void should_return_season_image(string folder) + { + var path = Path.Combine(_series.Path, folder, "folder.jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + } + + [TestCase(".xml", MetadataType.EpisodeMetadata)] + [TestCase(".metathumb", MetadataType.EpisodeImage)] + public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(type); + } + + [TestCase(".xml")] + [TestCase(".metathumb")] + public void should_return_null_if_not_valid_file_for_episode(string extension) + { + var path = Path.Combine(_series.Path, "the.series.episode" + extension); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_series_image_for_folder_jpg_in_series_folder() + { + var path = Path.Combine(_series.Path, "folder.jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 82fc19ecc..1305b127d 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -115,18 +115,21 @@ - - - + + + + + + @@ -156,7 +159,6 @@ - @@ -172,6 +174,8 @@ + + diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index d86a4b563..7fb5d1986 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -33,13 +33,14 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "Hack Sign", 1, 0, 0)] [TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)] [TestCase("ducktales_e66_time_is_money_part_one_marking_time", "DuckTales", 66, 0, 0)] + [TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0].mkv", "No Game No Life", 1, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); result.Should().NotBeNull(); - result.AbsoluteEpisodeNumbers.First().Should().Be(absoluteEpisodeNumber); + result.AbsoluteEpisodeNumbers.Single().Should().Be(absoluteEpisodeNumber); result.SeasonNumber.Should().Be(seasonNumber); - result.EpisodeNumbers.FirstOrDefault().Should().Be(episodeNumber); + result.EpisodeNumbers.SingleOrDefault().Should().Be(episodeNumber); result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); result.FullSeason.Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 3b4ed3746..5eea42c15 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -40,6 +40,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)] [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)] [TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA", Language.English)] + [TestCase("Revolution S01E03 No Quarter 2012 WEB-DL 720p Nordic-philipo mkv", Language.Norwegian)] public void should_parse_language(string postTitle, Language language) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 0b4a71cf2..dfa5b5989 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -16,12 +16,26 @@ namespace NzbDrone.Core.Test.ParserTests new object[] { Quality.DVD }, new object[] { Quality.WEBDL480p }, new object[] { Quality.HDTV720p }, + new object[] { Quality.HDTV1080p }, new object[] { Quality.WEBDL720p }, new object[] { Quality.WEBDL1080p }, new object[] { Quality.Bluray720p }, new object[] { Quality.Bluray1080p } }; + public static object[] OtherSourceQualityParserCases = + { + new object[] { "SD TV", Quality.SDTV }, + new object[] { "SD DVD", Quality.DVD }, + new object[] { "480p WEB-DL", Quality.WEBDL480p }, + new object[] { "HD TV", Quality.HDTV720p }, + new object[] { "1080p HD TV", Quality.HDTV1080p }, + new object[] { "720p WEB-DL", Quality.WEBDL720p }, + new object[] { "1080p WEB-DL", Quality.WEBDL1080p }, + new object[] { "720p BluRay", Quality.Bluray720p }, + new object[] { "1080p BluRay", Quality.Bluray1080p } + }; + [TestCase("S07E23 .avi ", false)] [TestCase("The.Shield.S01E13.x264-CtrlSD", false)] [TestCase("Nikita S02E01 HDTV XviD 2HD", false)] @@ -64,7 +78,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Elementary.S01E10.The.Leviathan.480p.WEB-DL.x264-mSD", false)] [TestCase("Glee.S04E10.Glee.Actually.480p.WEB-DL.x264-mSD", false)] - [TestCase("The.Big.Bang.Theory.S06E11.The.Santa.Simulation.480p.WEB-DL.x264-mSD", false)] + [TestCase("The.Big.Bang.Theory.S06E11.The.Santa.Simulation.480p.WEB-DL.x264-mSD", false)] + [TestCase("Da.Vincis.Demons.S02E04.480p.WEB.DL.nSD.x264-NhaNc3", false)] public void should_parse_webdl480p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL480p, proper); @@ -105,6 +120,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("S07E23 - [WEBDL].mkv ", false)] [TestCase("Fringe S04E22 720p WEB-DL DD5.1 H264-EbP.mkv", false)] [TestCase("House.S04.720p.Web-Dl.Dd5.1.h264-P2PACK", false)] + [TestCase("Da.Vincis.Demons.S02E04.720p.WEB.DL.nSD.x264-NhaNc3", false)] public void should_parse_webdl720p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL720p, proper); @@ -144,6 +160,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("POI S02E11 1080i HDTV DD5.1 MPEG2-TrollHD", false)] [TestCase("How I Met Your Mother S01E18 Nothing Good Happens After 2 A.M. 720p HDTV DD5.1 MPEG2-TrollHD", false)] [TestCase("The Voice S01E11 The Finals 1080i HDTV DD5.1 MPEG2-TrollHD", false)] + [TestCase("Californication.S07E11.1080i.HDTV.DD5.1.MPEG2-NTb.ts", false)] + [TestCase("Game of Thrones S04E10 1080i HDTV MPEG2 DD5.1-CtrlHD.ts", false)] + [TestCase("VICE.S02E05.1080i.HDTV.DD2.0.MPEG2-NTb.ts", false)] public void should_parse_raw_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.RAWHD, proper); @@ -164,6 +183,18 @@ namespace NzbDrone.Core.Test.ParserTests result.Quality.Should().Be(quality); } + [Test, TestCaseSource("OtherSourceQualityParserCases")] + public void should_parse_quality_from_other_source(string qualityString, Quality quality) + { + foreach (var c in new char[] { '-', '.', ' ', '_' }) + { + var title = String.Format("My series S01E01 {0}", qualityString.Replace(' ', c)); + + ParseAndVerifyQuality(title, quality, false); + } + } + + private void ParseAndVerifyQuality(string title, Quality quality, bool proper) { var result = Parser.QualityParser.ParseQuality(title); diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 7c1504d4a..2b7bd476b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -4,7 +4,6 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ParserTests { - [TestFixture] public class ReleaseGroupParserFixture : CoreTest { @@ -19,6 +18,11 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] [TestCase("The.Walking.Dead.S04E13.720p.WEB-DL.AAC2.0.H.264-Cyphanix", "Cyphanix")] [TestCase("Arrow.S02E01.720p.WEB-DL.DD5.1.H.264.mkv", "DRONE")] + [TestCase("Series Title S01E01 Episode Title", "DRONE")] + [TestCase("The Colbert Report - 2014-06-02 - Thomas Piketty.mkv", "DRONE")] + [TestCase("Real Time with Bill Maher S12E17 May 23, 2014.mp4", "DRONE")] + [TestCase("Reizen Waes - S01E08 - Transistrië, Zuid-Ossetië en Abchazië SDTV.avi", "DRONE")] + [TestCase("Simpsons 10x11 - Wild Barts Cant Be Broken [rl].avi", "DRONE")] public void should_parse_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index cdf3492bd..b375ed4a2 100644 Binary files a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs and b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs differ diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs index a7eabef06..c62504f12 100644 Binary files a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs and b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs differ diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs index 9522472e9..86cbe7320 100644 Binary files a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs and b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs differ diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs index 5d507967e..23f77223c 100644 Binary files a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs and b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs differ diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index e01a08761..e08094aa2 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -49,6 +49,9 @@ namespace NzbDrone.Core.Test.UpdateTests } Mocker.GetMock().SetupGet(c => c.TempFolder).Returns(TempFolder); + Mocker.GetMock().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + Mocker.GetMock().SetupGet(c => c.AppDataFolder).Returns(@"C:\ProgramData\NzbDrone".AsOsAgnostic); + Mocker.GetMock().Setup(c => c.AvailableUpdate()).Returns(_updatePackage); Mocker.GetMock().Setup(c => c.Verify(It.IsAny(), It.IsAny())).Returns(true); @@ -101,7 +104,6 @@ namespace NzbDrone.Core.Test.UpdateTests Subject.Execute(new ApplicationUpdateCommand()); - Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive)); } @@ -112,7 +114,6 @@ namespace NzbDrone.Core.Test.UpdateTests Subject.Execute(new ApplicationUpdateCommand()); - Mocker.GetMock().Verify(c => c.Extract(updateArchive, _sandboxFolder)); } @@ -239,6 +240,26 @@ namespace NzbDrone.Core.Test.UpdateTests updateSubFolder.GetFiles().Should().NotBeEmpty(); } + [Test] + public void should_log_error_when_app_data_is_child_of_startup_folder() + { + Mocker.GetMock().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + Mocker.GetMock().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone\AppData".AsOsAgnostic); + + Subject.Execute(new ApplicationUpdateCommand()); + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_log_error_when_app_data_is_same_as_startup_folder() + { + Mocker.GetMock().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + Mocker.GetMock().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + + Subject.Execute(new ApplicationUpdateCommand()); + ExceptionVerification.ExpectedErrors(1); + } + [TearDown] public void TearDown() { diff --git a/src/NzbDrone.Core.Test/app.config b/src/NzbDrone.Core.Test/app.config new file mode 100644 index 000000000..a6a2b7fa9 --- /dev/null +++ b/src/NzbDrone.Core.Test/app.config @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index ce3252221..e1462a561 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -72,6 +72,11 @@ namespace NzbDrone.Core.Configuration _eventAggregator.PublishEvent(new ConfigSavedEvent()); } + public Boolean IsDefined(String key) + { + return _repository.Get(key.ToLower()) != null; + } + public String DownloadedEpisodesFolder { get { return GetValue(ConfigKey.DownloadedEpisodesFolder.ToString()); } @@ -117,6 +122,27 @@ namespace NzbDrone.Core.Configuration set { SetValue("AutoDownloadPropers", value); } } + public Boolean EnableCompletedDownloadHandling + { + get { return GetValueBoolean("EnableCompletedDownloadHandling", false); } + + set { SetValue("EnableCompletedDownloadHandling", value); } + } + + public Boolean RemoveCompletedDownloads + { + get { return GetValueBoolean("RemoveCompletedDownloads", false); } + + set { SetValue("RemoveCompletedDownloads", value); } + } + + public Boolean EnableFailedDownloadHandling + { + get { return GetValueBoolean("EnableFailedDownloadHandling", true); } + + set { SetValue("EnableFailedDownloadHandling", value); } + } + public Boolean AutoRedownloadFailed { get { return GetValueBoolean("AutoRedownloadFailed", true); } @@ -152,13 +178,6 @@ namespace NzbDrone.Core.Configuration set { SetValue("BlacklistRetryLimit", value); } } - public Boolean EnableFailedDownloadHandling - { - get { return GetValueBoolean("EnableFailedDownloadHandling", true); } - - set { SetValue("EnableFailedDownloadHandling", value); } - } - public Boolean CreateEmptySeriesFolders { get { return GetValueBoolean("CreateEmptySeriesFolders", false); } @@ -186,6 +205,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("DownloadedEpisodesScanInterval", value); } } + public Int32 DownloadClientHistoryLimit + { + get { return GetValueInt("DownloadClientHistoryLimit", 30); } + + set { SetValue("DownloadClientHistoryLimit", value); } + } + public Boolean SkipFreeSpaceCheckWhenImporting { get { return GetValueBoolean("SkipFreeSpaceCheckWhenImporting", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 10a1843a5..f471762c5 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -11,15 +11,21 @@ namespace NzbDrone.Core.Configuration Dictionary AllWithDefaults(); void SaveConfigDictionary(Dictionary configValues); + Boolean IsDefined(String key); + //Download Client String DownloadedEpisodesFolder { get; set; } String DownloadClientWorkingFolders { get; set; } Int32 DownloadedEpisodesScanInterval { get; set; } + Int32 DownloadClientHistoryLimit { get; set; } - //Failed Download Handling (Download client) + //Completed/Failed Download Handling (Download client) + Boolean EnableCompletedDownloadHandling { get; set; } + Boolean RemoveCompletedDownloads { get; set; } + + Boolean EnableFailedDownloadHandling { get; set; } Boolean AutoRedownloadFailed { get; set; } Boolean RemoveFailedDownloads { get; set; } - Boolean EnableFailedDownloadHandling { get; set; } Int32 BlacklistGracePeriod { get; set; } Int32 BlacklistRetryInterval { get; set; } Int32 BlacklistRetryLimit { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs new file mode 100644 index 000000000..c773e6224 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs @@ -0,0 +1,243 @@ +using System; +using System.Data; +using System.Linq; +using System.Collections.Generic; +using FluentMigrator; +using Newtonsoft.Json; +using NzbDrone.Common; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.IO; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(51)] + public class download_client_import : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(EnableCompletedDownloadHandlingForNewUsers); + + Execute.WithConnection(ConvertFolderSettings); + + Execute.WithConnection(AssociateImportedHistoryItems); + } + + private void EnableCompletedDownloadHandlingForNewUsers(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; + + var result = cmd.ExecuteScalar(); + + if (result == null) + { + cmd.CommandText = @"INSERT INTO Config (Key, Value) VALUES ('enablecompleteddownloadhandling', 'True')"; + cmd.ExecuteNonQuery(); + } + } + } + + private void ConvertFolderSettings(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand downloadClientsCmd = conn.CreateCommand()) + { + downloadClientsCmd.Transaction = tran; + downloadClientsCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; + var downloadedEpisodesFolder = downloadClientsCmd.ExecuteScalar() as String; + + downloadClientsCmd.Transaction = tran; + downloadClientsCmd.CommandText = @"SELECT Id, Implementation, Settings, ConfigContract FROM DownloadClients WHERE ConfigContract = 'FolderSettings'"; + using (IDataReader downloadClientReader = downloadClientsCmd.ExecuteReader()) + { + while (downloadClientReader.Read()) + { + var id = downloadClientReader.GetInt32(0); + var implementation = downloadClientReader.GetString(1); + var settings = downloadClientReader.GetString(2); + var configContract = downloadClientReader.GetString(3); + + var settingsJson = JsonConvert.DeserializeObject(settings) as Newtonsoft.Json.Linq.JObject; + + if (implementation == "Blackhole") + { + var newSettings = new + { + NzbFolder = settingsJson.Value("folder"), + WatchFolder = downloadedEpisodesFolder + }.ToJson(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE DownloadClients SET Implementation = ?, Settings = ?, ConfigContract = ? WHERE Id = ?"; + updateCmd.AddParameter("UsenetBlackhole"); + updateCmd.AddParameter(newSettings); + updateCmd.AddParameter("UsenetBlackholeSettings"); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + else if (implementation == "Pneumatic") + { + var newSettings = new + { + NzbFolder = settingsJson.Value("folder") + }.ToJson(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ?, ConfigContract = ? WHERE Id = ?"; + updateCmd.AddParameter(newSettings); + updateCmd.AddParameter("PneumaticSettings"); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + else + { + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "DELETE FROM DownloadClients WHERE Id = ?"; + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } + + private sealed class MigrationHistoryItem + { + public Int32 Id { get; set; } + public Int32 EpisodeId { get; set; } + public Int32 SeriesId { get; set; } + public String SourceTitle { get; set; } + public DateTime Date { get; set; } + public Dictionary Data { get; set; } + public MigrationHistoryEventType EventType { get; set; } + } + + private enum MigrationHistoryEventType + { + Unknown = 0, + Grabbed = 1, + SeriesFolderImported = 2, + DownloadFolderImported = 3, + DownloadFailed = 4 + } + + private void AssociateImportedHistoryItems(IDbConnection conn, IDbTransaction tran) + { + var historyItems = new List(); + + using (IDbCommand historyCmd = conn.CreateCommand()) + { + historyCmd.Transaction = tran; + historyCmd.CommandText = @"SELECT Id, EpisodeId, SeriesId, SourceTitle, Date, Data, EventType FROM History WHERE EventType NOT NULL"; + using (IDataReader historyRead = historyCmd.ExecuteReader()) + { + while (historyRead.Read()) + { + historyItems.Add(new MigrationHistoryItem + { + Id = historyRead.GetInt32(0), + EpisodeId = historyRead.GetInt32(1), + SeriesId = historyRead.GetInt32(2), + SourceTitle = historyRead.GetString(3), + Date = historyRead.GetDateTime(4), + Data = Json.Deserialize>(historyRead.GetString(5)), + EventType = (MigrationHistoryEventType)historyRead.GetInt32(6) + }); + } + } + } + + var numHistoryItemsNotAssociated = historyItems.Count(v => v.EventType == MigrationHistoryEventType.DownloadFolderImported && + v.Data.GetValueOrDefault("downloadClientId") == null); + + if (numHistoryItemsNotAssociated == 0) + { + return; + } + + var historyItemsToAssociate = new Dictionary(); + + var historyItemsLookup = historyItems.ToLookup(v => v.EpisodeId); + + foreach (var historyItemGroup in historyItemsLookup) + { + var list = historyItemGroup.ToList(); + + for (int i = 0; i < list.Count - 1; i++) + { + var grabbedEvent = list[i]; + if (grabbedEvent.EventType != MigrationHistoryEventType.Grabbed) continue; + if (grabbedEvent.Data.GetValueOrDefault("downloadClient") == null || grabbedEvent.Data.GetValueOrDefault("downloadClientId") == null) continue; + + // Check if it is already associated with a failed/imported event. + int j; + for (j = i + 1; j < list.Count;j++) + { + if (list[j].EventType != MigrationHistoryEventType.DownloadFolderImported && + list[j].EventType != MigrationHistoryEventType.DownloadFailed) + { + continue; + } + + if (list[j].Data.ContainsKey("downloadClient") && list[j].Data["downloadClient"] == grabbedEvent.Data["downloadClient"] && + list[j].Data.ContainsKey("downloadClientId") && list[j].Data["downloadClientId"] == grabbedEvent.Data["downloadClientId"]) + { + break; + } + } + + if (j != list.Count) + { + list.RemoveAt(j); + list.RemoveAt(i--); + continue; + } + + var importedEvent = list[i + 1]; + if (importedEvent.EventType != MigrationHistoryEventType.DownloadFolderImported) continue; + + var droppedPath = importedEvent.Data.GetValueOrDefault("droppedPath"); + if (droppedPath != null && new FileInfo(droppedPath).Directory.Name == grabbedEvent.SourceTitle) + { + historyItemsToAssociate[importedEvent] = grabbedEvent; + + list.RemoveAt(i + 1); + list.RemoveAt(i--); + } + } + } + + foreach (var pair in historyItemsToAssociate) + { + using (IDbCommand updateHistoryCmd = conn.CreateCommand()) + { + pair.Key.Data["downloadClient"] = pair.Value.Data["downloadClient"]; + pair.Key.Data["downloadClientId"] = pair.Value.Data["downloadClientId"]; + + updateHistoryCmd.Transaction = tran; + updateHistoryCmd.CommandText = "UPDATE History SET Data = ? WHERE Id = ?"; + updateHistoryCmd.AddParameter(pair.Key.Data.ToJson()); + updateHistoryCmd.AddParameter(pair.Key.Id); + + updateHistoryCmd.ExecuteNonQuery(); + } + } + + _logger.Info("Updated old History items. {0}/{1} old ImportedEvents were associated with GrabbedEvents.", historyItemsToAssociate.Count, numHistoryItemsNotAssociated); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs index da9dde57a..5655649cd 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs @@ -6,11 +6,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework { public abstract class NzbDroneMigrationBase : FluentMigrator.Migration { - private Logger _logger; + protected readonly Logger _logger; protected NzbDroneMigrationBase() { - _logger = NzbDroneLogger.GetLogger(); + _logger = NzbDroneLogger.GetLogger(this); } protected virtual void MainDbUpgrade() diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 74ab43f69..edb84e624 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -37,7 +37,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("Config"); Mapper.Entity().RegisterModel("RootFolders").Ignore(r => r.FreeSpace); - Mapper.Entity().RegisterModel("Indexers"); + Mapper.Entity().RegisterModel("Indexers") + .Ignore(s => s.Protocol); Mapper.Entity().RegisterModel("ScheduledTasks"); Mapper.Entity().RegisterModel("Notifications"); Mapper.Entity().RegisterModel("Metadata"); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index 4fe2010de..92f73d562 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -5,17 +5,18 @@ using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Queue; namespace NzbDrone.Core.DecisionEngine.Specifications { public class NotInQueueSpecification : IDecisionEngineSpecification { - private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IQueueService _queueService; private readonly Logger _logger; - public NotInQueueSpecification(IProvideDownloadClient downloadClientProvider, Logger logger) + public NotInQueueSpecification(IQueueService queueService, Logger logger) { - _downloadClientProvider = downloadClientProvider; + _queueService = queueService; _logger = logger; } @@ -29,15 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { - var downloadClient = _downloadClientProvider.GetDownloadClient(); - - if (downloadClient == null) - { - _logger.Warn("Download client isn't configured yet."); - return true; - } - - var queue = downloadClient.GetQueue().Select(q => q.RemoteEpisode); + var queue = _queueService.GetQueue().Select(q => q.RemoteEpisode); if (IsInQueue(subject, queue)) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 0132f4adf..9dd1fc5c4 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -1,3 +1,4 @@ +using System.Linq; using NLog; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Sabnzbd; @@ -41,9 +42,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return true; } - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClients = _downloadClientProvider.GetDownloadClients(); - if (downloadClient != null && downloadClient.GetType() == typeof (Sabnzbd)) + foreach (var downloadClient in downloadClients.OfType()) { _logger.Debug("Performing history status check on report"); foreach (var episode in subject.Episodes) diff --git a/src/NzbDrone.Core/Download/CheckForFailedDownloadCommand.cs b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs similarity index 61% rename from src/NzbDrone.Core/Download/CheckForFailedDownloadCommand.cs rename to src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs index a1714d35f..7dc987d84 100644 --- a/src/NzbDrone.Core/Download/CheckForFailedDownloadCommand.cs +++ b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download { - public class CheckForFailedDownloadCommand : Command + public class CheckForFinishedDownloadCommand : Command { } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs deleted file mode 100644 index 057556420..000000000 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using NLog; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Clients.Blackhole -{ - public class Blackhole : DownloadClientBase, IExecute - { - private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; - - public Blackhole(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) - { - _diskProvider = diskProvider; - _httpProvider = httpProvider; - _logger = logger; - } - - public override string DownloadNzb(RemoteEpisode remoteEpisode) - { - var url = remoteEpisode.Release.DownloadUrl; - var title = remoteEpisode.Release.Title; - - title = FileNameBuilder.CleanFilename(title); - - var filename = Path.Combine(Settings.Folder, title + ".nzb"); - - - _logger.Debug("Downloading NZB from: {0} to: {1}", url, filename); - _httpProvider.DownloadFile(url, filename); - _logger.Debug("NZB Download succeeded, saved to: {0}", filename); - - return null; - } - - public override IEnumerable GetQueue() - { - return new QueueItem[0]; - } - - public override IEnumerable GetHistory(int start = 0, int limit = 10) - { - return new HistoryItem[0]; - } - - public override void RemoveFromQueue(string id) - { - } - - public override void RemoveFromHistory(string id) - { - } - - public override void RetryDownload(string id) - { - throw new NotImplementedException(); - } - - public override void Test() - { - PerformTest(Settings.Folder); - } - - private void PerformTest(string folder) - { - var testPath = Path.Combine(folder, "drone_test.txt"); - _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); - _diskProvider.DeleteFile(testPath); - } - - public void Execute(TestBlackholeCommand message) - { - PerformTest(message.Folder); - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/FolderSettings.cs b/src/NzbDrone.Core/Download/Clients/FolderSettings.cs deleted file mode 100644 index cacb847ea..000000000 --- a/src/NzbDrone.Core/Download/Clients/FolderSettings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using FluentValidation; -using FluentValidation.Results; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation.Paths; - -namespace NzbDrone.Core.Download.Clients -{ - public class FolderSettingsValidator : AbstractValidator - { - public FolderSettingsValidator() - { - //Todo: Validate that the path actually exists - RuleFor(c => c.Folder).IsValidPath(); - } - } - - public class FolderSettings : IProviderConfig - { - private static readonly FolderSettingsValidator Validator = new FolderSettingsValidator(); - - [FieldDefinition(0, Label = "Folder", Type = FieldType.Path)] - public String Folder { get; set; } - - public ValidationResult Validate() - { - return Validator.Validate(this); - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs index 38292bb26..a951bd57d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs @@ -5,15 +5,20 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { public class NzbgetQueueItem { - private string _nzbName; public Int32 NzbId { get; set; } public Int32 FirstId { get; set; } public Int32 LastId { get; set; } - public string NzbName { get; set; } + public String NzbName { get; set; } public String Category { get; set; } - public Int32 FileSizeMb { get; set; } - public Int32 RemainingSizeMb { get; set; } - public Int32 PausedSizeMb { get; set; } + public UInt32 FileSizeLo { get; set; } + public UInt32 FileSizeHi { get; set; } + public UInt32 RemainingSizeLo { get; set; } + public UInt32 RemainingSizeHi { get; set; } + public UInt32 PausedSizeLo { get; set; } + public UInt32 PausedSizeHi { get; set; } + public Int32 MinPriority { get; set; } + public Int32 MaxPriority { get; set; } + public Int32 ActiveDownloads { get; set; } public List Parameters { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 531f56898..e99382894 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -1,9 +1,12 @@ using System; -using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Collections.Generic; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -14,22 +17,28 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public class Nzbget : DownloadClientBase, IExecute { private readonly INzbgetProxy _proxy; - private readonly IParsingService _parsingService; private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; public Nzbget(INzbgetProxy proxy, + IConfigService configService, IParsingService parsingService, IHttpProvider httpProvider, Logger logger) + : base(configService, parsingService, logger) { _proxy = proxy; - _parsingService = parsingService; _httpProvider = httpProvider; - _logger = logger; } - public override string DownloadNzb(RemoteEpisode remoteEpisode) + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title + ".nzb"; @@ -48,80 +57,129 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } } - public override IEnumerable GetQueue() + private IEnumerable GetQueue() { + NzbgetGlobalStatus globalStatus; List queue; + Dictionary postQueue; try { + globalStatus = _proxy.GetGlobalStatus(Settings); queue = _proxy.GetQueue(Settings); + postQueue = _proxy.GetPostQueue(Settings).ToDictionary(v => v.NzbId); } catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); + return Enumerable.Empty(); } - var queueItems = new List(); + var queueItems = new List(); + + Int64 totalRemainingSize = 0; foreach (var item in queue) { + var postQueueItem = postQueue.GetValueOrDefault(item.NzbId); + + var totalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + var pausedSize = MakeInt64(item.PausedSizeHi, item.PausedSizeLo); + var remainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); + var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); - var queueItem = new QueueItem(); - queueItem.Id = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); + var queueItem = new DownloadClientItem(); + queueItem.DownloadClientId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); queueItem.Title = item.NzbName; - queueItem.Size = item.FileSizeMb; - queueItem.Sizeleft = item.RemainingSizeMb; - queueItem.Status = item.FileSizeMb == item.PausedSizeMb ? "paused" : "queued"; + queueItem.TotalSize = totalSize; + queueItem.Category = item.Category; - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); - if (parsedEpisodeInfo == null) continue; + if (postQueueItem != null) + { + queueItem.Status = DownloadItemStatus.Downloading; + queueItem.Message = postQueueItem.ProgressLabel; - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) continue; + if (postQueueItem.StageProgress != 0) + { + queueItem.RemainingTime = TimeSpan.FromSeconds(postQueueItem.StageTimeSec * 1000 / postQueueItem.StageProgress - postQueueItem.StageTimeSec); + } + } + else if (globalStatus.DownloadPaused || remainingSize == pausedSize) + { + queueItem.Status = DownloadItemStatus.Paused; + queueItem.RemainingSize = remainingSize; + } + else + { + if (item.ActiveDownloads == 0 && remainingSize != 0) + { + queueItem.Status = DownloadItemStatus.Queued; + } + else + { + queueItem.Status = DownloadItemStatus.Downloading; + } + + queueItem.RemainingSize = remainingSize - pausedSize; + + if (globalStatus.DownloadRate != 0) + { + queueItem.RemainingTime = TimeSpan.FromSeconds((totalRemainingSize + queueItem.RemainingSize) / globalStatus.DownloadRate); + totalRemainingSize += queueItem.RemainingSize; + } + } - queueItem.RemoteEpisode = remoteEpisode; queueItems.Add(queueItem); } return queueItems; } - public override IEnumerable GetHistory(int start = 0, int limit = 10) + private IEnumerable GetHistory() { List history; try { - history = _proxy.GetHistory(Settings); + history = _proxy.GetHistory(Settings).Take(_configService.DownloadClientHistoryLimit).ToList(); } catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); + return Enumerable.Empty(); } - var historyItems = new List(); - var successStatues = new[] {"SUCCESS", "NONE"}; + var historyItems = new List(); + var successStatus = new[] {"SUCCESS", "NONE"}; foreach (var item in history) { var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); - var status = successStatues.Contains(item.ParStatus) && - successStatues.Contains(item.ScriptStatus) - ? HistoryStatus.Completed - : HistoryStatus.Failed; - var historyItem = new HistoryItem(); - historyItem.Id = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); + var historyItem = new DownloadClientItem(); + historyItem.DownloadClient = Definition.Name; + historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); historyItem.Title = item.Name; - historyItem.Size = item.FileSizeMb.ToString(); //Why is this a string? - historyItem.DownloadTime = 0; - historyItem.Storage = item.DestDir; + historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + historyItem.OutputPath = item.DestDir; historyItem.Category = item.Category; - historyItem.Message = String.Format("PAR Status: {0} - Script Status: {1}", item.ParStatus, item.ScriptStatus); - historyItem.Status = status; + historyItem.Message = String.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus); + historyItem.Status = DownloadItemStatus.Completed; + historyItem.RemainingTime = TimeSpan.Zero; + + if (item.DeleteStatus == "MANUAL") + { + continue; + } + + if (!successStatus.Contains(item.ParStatus) || + !successStatus.Contains(item.UnpackStatus) || + !successStatus.Contains(item.MoveStatus) || + !successStatus.Contains(item.ScriptStatus)) + { + historyItem.Status = DownloadItemStatus.Failed; + } historyItems.Add(historyItem); } @@ -129,12 +187,20 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return historyItems; } - public override void RemoveFromQueue(string id) + public override IEnumerable GetItems() { - throw new NotImplementedException(); + foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) + { + if (downloadClientItem.Category != Settings.TvCategory) continue; + + downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); + if (downloadClientItem.RemoteEpisode == null) continue; + + yield return downloadClientItem; + } } - public override void RemoveFromHistory(string id) + public override void RemoveItem(string id) { _proxy.RemoveFromHistory(id, Settings); } @@ -144,22 +210,94 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy.RetryDownload(id, Settings); } - public override void Test() + public override DownloadClientStatus GetStatus() { - _proxy.GetVersion(Settings); + var config = _proxy.GetConfig(Settings); + + var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); + + var status = new DownloadClientStatus + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" + }; + + if (category != null) + { + status.OutputRootFolders = new List { category.DestDir }; + } + + return status; } - private VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null) + protected IEnumerable GetCategories(Dictionary config) + { + for (int i = 1; i < 100; i++) + { + var name = config.GetValueOrDefault("Category" + i + ".Name"); + + if (name == null) yield break; + + var destDir = config.GetValueOrDefault("Category" + i + ".DestDir"); + + if (destDir.IsNullOrWhiteSpace()) + { + var mainDir = config.GetValueOrDefault("MainDir"); + destDir = config.GetValueOrDefault("DestDir", String.Empty).Replace("${MainDir}", mainDir); + + if (config.GetValueOrDefault("AppendCategoryDir", "yes") == "yes") + { + destDir = Path.Combine(destDir, name); + } + } + + yield return new NzbgetCategory + { + Name = name, + DestDir = destDir, + Unpack = config.GetValueOrDefault("Category" + i + ".Unpack") == "yes", + DefScript = config.GetValueOrDefault("Category" + i + ".DefScript"), + Aliases = config.GetValueOrDefault("Category" + i + ".Aliases"), + }; + } + } + + private String GetVersion(string host = null, int port = 0, string username = null, string password = null) { return _proxy.GetVersion(Settings); } + public override void Test(NzbgetSettings settings) + { + _proxy.GetVersion(settings); + + var config = _proxy.GetConfig(settings); + + var categories = GetCategories(config); + + if (!categories.Any(v => v.Name == settings.TvCategory)) + { + throw new ApplicationException("Category does not exist"); + } + } + public void Execute(TestNzbgetCommand message) { var settings = new NzbgetSettings(); settings.InjectFrom(message); - _proxy.GetVersion(settings); + Test(settings); + } + + // Javascript doesn't support 64 bit integers natively so json officially doesn't either. + // NzbGet api thus sends it in two 32 bit chunks. Here we join the two chunks back together. + // Simplified decimal example: "42" splits into "4" and "2". To join them I shift (<<) the "4" 1 digit to the left = "40". combine it with "2". which becomes "42" again. + private Int64 MakeInt64(UInt32 high, UInt32 low) + { + Int64 result = high; + + result = (result << 32) | (Int64)low; + + return result; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs deleted file mode 100644 index 6c536ba7d..000000000 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download.Clients.Nzbget -{ - public class NzbgetBooleanResponse - { - public String Version { get; set; } - public Boolean Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs new file mode 100644 index 000000000..8a0615ad7 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetCategory + { + public String Name { get; set; } + public String DestDir { get; set; } + public Boolean Unpack { get; set; } + public String DefScript { get; set; } + public String Aliases { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs new file mode 100644 index 000000000..e04aad77f --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetConfigItem + { + public String Name { get; set; } + public String Value { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs new file mode 100644 index 000000000..fcf5f6e46 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetGlobalStatus + { + public UInt32 RemainingSizeLo { get; set; } + public UInt32 RemainingSizeHi { get; set; } + public UInt32 DownloadedSizeLo { get; set; } + public UInt32 DownloadedSizeHi { get; set; } + public UInt32 DownloadRate { get; set; } + public UInt32 AverageDownloadRate { get; set; } + public UInt32 DownloadLimit { get; set; } + public Boolean DownloadPaused { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs index af90178a8..f02c483f3 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs @@ -5,13 +5,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { public class NzbgetHistoryItem { - private string _nzbName; public Int32 Id { get; set; } public String Name { get; set; } public String Category { get; set; } - public Int32 FileSizeMb { get; set; } + public UInt32 FileSizeLo { get; set; } + public UInt32 FileSizeHi { get; set; } public String ParStatus { get; set; } + public String UnpackStatus { get; set; } + public String MoveStatus { get; set; } public String ScriptStatus { get; set; } + public String DeleteStatus { get; set; } + public String MarkStatus { get; set; } public String DestDir { get; set; } public List Parameters { get; set; } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs new file mode 100644 index 000000000..450e07eab --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetPostQueueItem + { + public Int32 NzbId { get; set; } + public String NzbName { get; set; } + public String Stage { get; set; } + public String ProgressLabel { get; set; } + public Int32 FileProgress { get; set; } + public Int32 StageProgress { get; set; } + public Int32 TotalTimeSec { get; set; } + public Int32 StageTimeSec { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index a4467f38f..6f7f50a6f 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -13,9 +13,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public interface INzbgetProxy { string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings); + NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings); List GetQueue(NzbgetSettings settings); + List GetPostQueue(NzbgetSettings settings); List GetHistory(NzbgetSettings settings); - VersionResponse GetVersion(NzbgetSettings settings); + String GetVersion(NzbgetSettings settings); + Dictionary GetConfig(NzbgetSettings settings); void RemoveFromHistory(string id, NzbgetSettings settings); void RetryDownload(string id, NzbgetSettings settings); } @@ -34,8 +37,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzb.ToBytes()) }; var request = BuildRequest(new JsonRequest("append", parameters)); - var response = Json.Deserialize(ProcessRequest(request, settings)); - _logger.Debug("Queue Response: [{0}]", response.Result); + var response = Json.Deserialize>(ProcessRequest(request, settings)); + _logger.Trace("Response: [{0}]", response.Result); if (!response.Result) { @@ -61,31 +64,53 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return droneId; } + public NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("status")); + + return Json.Deserialize>(ProcessRequest(request, settings)).Result; + } + public List GetQueue(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("listgroups")); - return Json.Deserialize>(ProcessRequest(request, settings)).QueueItems; + return Json.Deserialize>>(ProcessRequest(request, settings)).Result; + } + + public List GetPostQueue(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("postqueue")); + + return Json.Deserialize>>(ProcessRequest(request, settings)).Result; } public List GetHistory(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("history")); - return Json.Deserialize>(ProcessRequest(request, settings)).QueueItems; + return Json.Deserialize>>(ProcessRequest(request, settings)).Result; } - public VersionResponse GetVersion(NzbgetSettings settings) + public String GetVersion(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("version")); - return Json.Deserialize(ProcessRequest(request, settings)); + return Json.Deserialize>(ProcessRequest(request, settings)).Version; } + public Dictionary GetConfig(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("config")); + + return Json.Deserialize>>(ProcessRequest(request, settings)).Result.ToDictionary(v => v.Name, v => v.Value); + } + + public void RemoveFromHistory(string id, NzbgetSettings settings) { var history = GetHistory(settings); - var item = history.SingleOrDefault(h => h.Parameters.SingleOrDefault(p => p.Name == "drone") != null); + var item = history.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string))); if (item == null) { @@ -120,7 +145,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var parameters = new object[] { command, offset, editText, id }; var request = BuildRequest(new JsonRequest("editqueue", parameters)); - var response = Json.Deserialize(ProcessRequest(request, settings)); + var response = Json.Deserialize>(ProcessRequest(request, settings)); return response.Result; } @@ -129,7 +154,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var client = BuildClient(settings); var response = client.Execute(restRequest); - _logger.Debug("Response: {0}", response.Content); + _logger.Trace("Response: {0}", response.Content); CheckForError(response); @@ -145,6 +170,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget settings.Host, settings.Port); + _logger.Debug("Url: " + url); + var client = new RestClient(url); client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetResponse.cs similarity index 57% rename from src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetResponse.cs index bb51dbcc6..d13f53fa5 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetResponse.cs @@ -4,11 +4,11 @@ using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Nzbget { - public class NzbgetListResponse + public class NzbgetResponse { public String Version { get; set; } - [JsonProperty(PropertyName = "result")] - public List QueueItems { get; set; } + public T Result { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs index 805b4d19a..f596d6b25 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs @@ -13,9 +13,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } } + public String Host { get; set; } public Int32 Port { get; set; } public String Username { get; set; } public String Password { get; set; } + public String TvCategory { get; set; } + public Int32 RecentTvPriority { get; set; } + public Int32 OlderTvPriority { get; set; } + public Boolean UseSsl { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs deleted file mode 100644 index 780fd90ad..000000000 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download.Clients.Nzbget -{ - public class VersionResponse - { - public String Version { get; set; } - public String Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 639b4e545..e8f0b7a09 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -7,42 +7,55 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using Omu.ValueInjecter; namespace NzbDrone.Core.Download.Clients.Pneumatic { - public class Pneumatic : DownloadClientBase, IExecute + public class Pneumatic : DownloadClientBase, IExecute { - private readonly IConfigService _configService; private readonly IHttpProvider _httpProvider; private readonly IDiskProvider _diskProvider; private static readonly Logger logger = NzbDroneLogger.GetLogger(); - public Pneumatic(IConfigService configService, IHttpProvider httpProvider, - IDiskProvider diskProvider) + public Pneumatic(IHttpProvider httpProvider, + IDiskProvider diskProvider, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(configService, parsingService, logger) { - _configService = configService; _httpProvider = httpProvider; _diskProvider = diskProvider; } - public override string DownloadNzb(RemoteEpisode remoteEpisode) + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; if (remoteEpisode.ParsedEpisodeInfo.FullSeason) { - throw new NotImplementedException("Full season releases are not supported with Pneumatic."); + throw new NotSupportedException("Full season releases are not supported with Pneumatic."); } title = FileNameBuilder.CleanFilename(title); //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) - var filename = Path.Combine(Settings.Folder, title + ".nzb"); + var filename = Path.Combine(Settings.NzbFolder, title + ".nzb"); logger.Debug("Downloading NZB from: {0} to: {1}", url, filename); _httpProvider.DownloadFile(url, filename); @@ -59,39 +72,41 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { get { - return !string.IsNullOrWhiteSpace(Settings.Folder); + return !string.IsNullOrWhiteSpace(Settings.NzbFolder); } } - public override IEnumerable GetQueue() + public override IEnumerable GetItems() { - return new QueueItem[0]; + return new DownloadClientItem[0]; } - - public override IEnumerable GetHistory(int start = 0, int limit = 10) - { - return new HistoryItem[0]; - } - - public override void RemoveFromQueue(string id) - { - } - - public override void RemoveFromHistory(string id) + + public override void RemoveItem(string id) { + throw new NotSupportedException(); } public override void RetryDownload(string id) { - throw new NotImplementedException(); + throw new NotSupportedException(); } - public override void Test() + public override DownloadClientStatus GetStatus() { - PerformTest(Settings.Folder); + var status = new DownloadClientStatus + { + IsLocalhost = true + }; + + return status; } - private void PerformTest(string folder) + public override void Test(PneumaticSettings settings) + { + PerformWriteTest(settings.NzbFolder); + } + + private void PerformWriteTest(string folder) { var testPath = Path.Combine(folder, "drone_test.txt"); _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); @@ -100,7 +115,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic public void Execute(TestPneumaticCommand message) { - PerformTest(message.Folder); + var settings = new PneumaticSettings(); + settings.InjectFrom(message); + + Test(settings); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs new file mode 100644 index 000000000..29b414c26 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs @@ -0,0 +1,32 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Core.Download.Clients.Pneumatic +{ + public class PneumaticSettingsValidator : AbstractValidator + { + public PneumaticSettingsValidator() + { + //Todo: Validate that the path actually exists + RuleFor(c => c.NzbFolder).IsValidPath(); + } + } + + public class PneumaticSettings : IProviderConfig + { + private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator(); + + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path)] + public String NzbFolder { get; set; } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 931b919cf..e14c15336 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -1,10 +1,12 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common; -using NzbDrone.Common.Cache; using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -15,25 +17,28 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public class Sabnzbd : DownloadClientBase, IExecute { private readonly IHttpProvider _httpProvider; - private readonly IParsingService _parsingService; private readonly ISabnzbdProxy _proxy; - private readonly ICached> _queueCache; - private readonly Logger _logger; public Sabnzbd(IHttpProvider httpProvider, - ICacheManager cacheManager, - IParsingService parsingService, ISabnzbdProxy proxy, + IConfigService configService, + IParsingService parsingService, Logger logger) + : base(configService, parsingService, logger) { _httpProvider = httpProvider; - _parsingService = parsingService; _proxy = proxy; - _queueCache = cacheManager.GetCache>(GetType(), "queue"); - _logger = logger; } - public override string DownloadNzb(RemoteEpisode remoteEpisode) + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; @@ -54,76 +59,118 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } } - public override IEnumerable GetQueue() + private IEnumerable GetQueue() { - return _queueCache.Get("queue", () => + SabnzbdQueue sabQueue; + + try { - SabnzbdQueue sabQueue; + sabQueue = _proxy.GetQueue(0, 0, Settings); + } + catch (DownloadClientException ex) + { + _logger.ErrorException(ex.Message, ex); + return Enumerable.Empty(); + } - try + var queueItems = new List(); + + foreach (var sabQueueItem in sabQueue.Items) + { + var queueItem = new DownloadClientItem(); + queueItem.DownloadClient = Definition.Name; + queueItem.DownloadClientId = sabQueueItem.Id; + queueItem.Category = sabQueueItem.Category; + queueItem.Title = sabQueueItem.Title; + queueItem.TotalSize = (long)(sabQueueItem.Size * 1024 * 1024); + queueItem.RemainingSize = (long)(sabQueueItem.Sizeleft * 1024 * 1024); + queueItem.RemainingTime = sabQueueItem.Timeleft; + + if (sabQueue.Paused || sabQueueItem.Status == SabnzbdDownloadStatus.Paused) { - sabQueue = _proxy.GetQueue(0, 0, Settings); + queueItem.Status = DownloadItemStatus.Paused; + + queueItem.RemainingTime = null; } - catch (DownloadClientException ex) + else if (sabQueueItem.Status == SabnzbdDownloadStatus.Queued || sabQueueItem.Status == SabnzbdDownloadStatus.Grabbing) { - _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); + queueItem.Status = DownloadItemStatus.Queued; + } + else + { + queueItem.Status = DownloadItemStatus.Downloading; } - var queueItems = new List(); - - foreach (var sabQueueItem in sabQueue.Items) + if (queueItem.Title.StartsWith("ENCRYPTED /")) { - var queueItem = new QueueItem(); - queueItem.Id = sabQueueItem.Id; - queueItem.Title = sabQueueItem.Title; - queueItem.Size = sabQueueItem.Size; - queueItem.Sizeleft = sabQueueItem.Sizeleft; - queueItem.Timeleft = sabQueueItem.Timeleft; - queueItem.Status = sabQueueItem.Status; - - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title.Replace("ENCRYPTED / ", "")); - if (parsedEpisodeInfo == null) continue; - - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) continue; - - queueItem.RemoteEpisode = remoteEpisode; - - queueItems.Add(queueItem); + queueItem.Title = queueItem.Title.Substring(11); + queueItem.IsEncrypted = true; } - return queueItems; - }, TimeSpan.FromSeconds(10)); + queueItems.Add(queueItem); + } + + return queueItems; } - public override IEnumerable GetHistory(int start = 0, int limit = 10) + private IEnumerable GetHistory() { SabnzbdHistory sabHistory; try { - sabHistory = _proxy.GetHistory(start, limit, Settings); + sabHistory = _proxy.GetHistory(0, _configService.DownloadClientHistoryLimit, Settings); } catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); + return Enumerable.Empty(); } - var historyItems = new List(); + var historyItems = new List(); foreach (var sabHistoryItem in sabHistory.Items) { - var historyItem = new HistoryItem(); - historyItem.Id = sabHistoryItem.Id; - historyItem.Title = sabHistoryItem.Title; - historyItem.Size = sabHistoryItem.Size; - historyItem.DownloadTime = sabHistoryItem.DownloadTime; - historyItem.Storage = sabHistoryItem.Storage; - historyItem.Category = sabHistoryItem.Category; - historyItem.Message = sabHistoryItem.FailMessage; - historyItem.Status = sabHistoryItem.Status == "Failed" ? HistoryStatus.Failed : HistoryStatus.Completed; + var historyItem = new DownloadClientItem + { + DownloadClient = Definition.Name, + DownloadClientId = sabHistoryItem.Id, + Category = sabHistoryItem.Category, + Title = sabHistoryItem.Title, + + TotalSize = sabHistoryItem.Size, + RemainingSize = 0, + DownloadTime = TimeSpan.FromSeconds(sabHistoryItem.DownloadTime), + RemainingTime = TimeSpan.Zero, + + Message = sabHistoryItem.FailMessage + }; + + if (sabHistoryItem.Status == SabnzbdDownloadStatus.Failed) + { + historyItem.Status = DownloadItemStatus.Failed; + } + else if (sabHistoryItem.Status == SabnzbdDownloadStatus.Completed) + { + historyItem.Status = DownloadItemStatus.Completed; + } + else // Verifying/Moving etc + { + historyItem.Status = DownloadItemStatus.Downloading; + } + + if (!sabHistoryItem.Storage.IsNullOrWhiteSpace()) + { + var parent = Directory.GetParent(sabHistoryItem.Storage); + if (parent != null && parent.Name == sabHistoryItem.Title) + { + historyItem.OutputPath = parent.FullName; + } + else + { + historyItem.OutputPath = sabHistoryItem.Storage; + } + } historyItems.Add(historyItem); } @@ -131,14 +178,29 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return historyItems; } - public override void RemoveFromQueue(string id) + public override IEnumerable GetItems() { - _proxy.RemoveFrom("queue", id, Settings); + foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) + { + if (downloadClientItem.Category != Settings.TvCategory) continue; + + downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); + if (downloadClientItem.RemoteEpisode == null) continue; + + yield return downloadClientItem; + } } - public override void RemoveFromHistory(string id) + public override void RemoveItem(string id) { - _proxy.RemoveFrom("history", id, Settings); + if (GetQueue().Any(v => v.DownloadClientId == id)) + { + _proxy.RemoveFrom("queue", id, Settings); + } + else + { + _proxy.RemoveFrom("history", id, Settings); + } } public override void RetryDownload(string id) @@ -146,9 +208,24 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd _proxy.RetryDownload(id, Settings); } - public override void Test() + public override DownloadClientStatus GetStatus() { - _proxy.GetCategories(Settings); + var status = new DownloadClientStatus + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" + }; + + return status; + } + + public override void Test(SabnzbdSettings settings) + { + var categories = _proxy.GetCategories(settings); + + if (!categories.Any(v => v == settings.TvCategory)) + { + throw new ApplicationException("Category does not exist"); + } } public void Execute(TestSabnzbdCommand message) @@ -156,7 +233,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var settings = new SabnzbdSettings(); settings.InjectFrom(message); - _proxy.GetCategories(settings); + Test(settings); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs new file mode 100644 index 000000000..16a3853ec --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs @@ -0,0 +1,22 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public enum SabnzbdDownloadStatus + { + Grabbing, + Queued, + Paused, + Checking, + Downloading, + QuickCheck, + Verifying, + Repairing, + Fetching, // Fetching additional blocks + Extracting, + Moving, + Running, // Running PP Script + Completed, + Failed + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs index 166b25c94..5a5f80ceb 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Sabnzbd { @@ -7,7 +8,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [JsonProperty(PropertyName = "fail_message")] public string FailMessage { get; set; } - public string Size { get; set; } + [JsonProperty(PropertyName = "bytes")] + public Int64 Size { get; set; } public string Category { get; set; } [JsonProperty(PropertyName = "nzb_name")] @@ -17,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public int DownloadTime { get; set; } public string Storage { get; set; } - public string Status { get; set; } + public SabnzbdDownloadStatus Status { get; set; } [JsonProperty(PropertyName = "nzo_id")] public string Id { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 4c699ab64..00e943721 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -1,12 +1,13 @@ using System; using System.IO; +using System.Linq; +using System.Collections.Generic; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; -using NzbDrone.Core.Instrumentation.Extensions; using RestSharp; namespace NzbDrone.Core.Download.Clients.Sabnzbd @@ -17,7 +18,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd void RemoveFrom(string source, string id, SabnzbdSettings settings); string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings); SabnzbdVersionResponse GetVersion(SabnzbdSettings settings); - SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings); + List GetCategories(SabnzbdSettings settings); SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings); SabnzbdHistory GetHistory(int start, int limit, SabnzbdSettings settings); void RetryDownload(string id, SabnzbdSettings settings); @@ -84,12 +85,12 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return response; } - public SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings) + public List GetCategories(SabnzbdSettings settings) { var request = new RestRequest(); var action = "mode=get_cats"; - var response = Json.Deserialize(ProcessRequest(request, action, settings)); + var response = Json.Deserialize(ProcessRequest(request, action, settings)).Categories; return response; } @@ -135,7 +136,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd action, authentication); - _logger.Debug(url); + _logger.Debug("Url: " + url); return new RestClient(url); } @@ -167,7 +168,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd result.Error = response.Content.Replace("error: ", ""); } - + if (result.Failed) throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error); } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs index a3a74452f..78e80f52c 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { public class SabnzbdQueueItem { - public string Status { get; set; } + public SabnzbdDownloadStatus Status { get; set; } public int Index { get; set; } [JsonConverter(typeof(SabnzbdQueueTimeConverter))] @@ -15,8 +15,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [JsonProperty(PropertyName = "mb")] public decimal Size { get; set; } - private string _title; - [JsonProperty(PropertyName = "filename")] public string Title { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs index 2c1d2eb9d..458b62f3a 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs @@ -13,11 +13,15 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } } + public String Host { get; set; } public Int32 Port { get; set; } public String ApiKey { get; set; } public String Username { get; set; } public String Password { get; set; } + public String TvCategory { get; set; } + public Int32 RecentTvPriority { get; set; } + public Int32 OlderTvPriority { get; set; } public Boolean UseSsl { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/TestUsenetBlackholeCommand.cs similarity index 51% rename from src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs rename to src/NzbDrone.Core/Download/Clients/UsenetBlackhole/TestUsenetBlackholeCommand.cs index 10898f80a..e4db46d4a 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/TestUsenetBlackholeCommand.cs @@ -1,9 +1,9 @@ using System; using NzbDrone.Core.Messaging.Commands; -namespace NzbDrone.Core.Download.Clients.Blackhole +namespace NzbDrone.Core.Download.Clients.UsenetBlackhole { - public class TestBlackholeCommand : Command + public class TestUsenetBlackholeCommand : Command { public override bool SendUpdatesToClient { @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole } } - public String Folder { get; set; } + public String NzbFolder { get; set; } + public String WatchFolder { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs new file mode 100644 index 000000000..290eb8564 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.MediaFiles; +using Omu.ValueInjecter; + +namespace NzbDrone.Core.Download.Clients.UsenetBlackhole +{ + public class UsenetBlackhole : DownloadClientBase, IExecute + { + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + private readonly IHttpProvider _httpProvider; + + public UsenetBlackhole(IDiskProvider diskProvider, + IDiskScanService diskScanService, + IHttpProvider httpProvider, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(configService, parsingService, logger) + { + _diskProvider = diskProvider; + _diskScanService = diskScanService; + _httpProvider = httpProvider; + } + + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + + title = FileNameBuilder.CleanFilename(title); + + var filename = Path.Combine(Settings.NzbFolder, title + ".nzb"); + + _logger.Debug("Downloading NZB from: {0} to: {1}", url, filename); + _httpProvider.DownloadFile(url, filename); + _logger.Debug("NZB Download succeeded, saved to: {0}", filename); + + return null; + } + + public override IEnumerable GetItems() + { + foreach (var folder in _diskProvider.GetDirectories(Settings.WatchFolder)) + { + var title = FileNameBuilder.CleanFilename(Path.GetFileName(folder)); + + var files = _diskProvider.GetFiles(folder, SearchOption.AllDirectories); + + var historyItem = new DownloadClientItem + { + DownloadClient = Definition.Name, + DownloadClientId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTimeUtc(folder).Ticks, + Title = title, + + TotalSize = files.Select(_diskProvider.GetFileSize).Sum(), + + OutputPath = folder + }; + + if (files.Any(_diskProvider.IsFileLocked)) + { + historyItem.Status = DownloadItemStatus.Downloading; + } + else + { + historyItem.Status = DownloadItemStatus.Completed; + + historyItem.RemainingTime = TimeSpan.Zero; + } + + historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); + if (historyItem.RemoteEpisode == null) continue; + + yield return historyItem; + } + + foreach (var videoFile in _diskScanService.GetVideoFiles(Settings.WatchFolder, false)) + { + var title = FileNameBuilder.CleanFilename(Path.GetFileName(videoFile)); + + var historyItem = new DownloadClientItem + { + DownloadClient = Definition.Name, + DownloadClientId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWriteUtc(videoFile).Ticks, + Title = title, + + TotalSize = _diskProvider.GetFileSize(videoFile), + + OutputPath = videoFile + }; + + if (_diskProvider.IsFileLocked(videoFile)) + { + historyItem.Status = DownloadItemStatus.Downloading; + } + else + { + historyItem.Status = DownloadItemStatus.Completed; + } + + historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); + if (historyItem.RemoteEpisode == null) continue; + + yield return historyItem; + } + } + + public override void RemoveItem(string id) + { + throw new NotSupportedException(); + } + + public override void RetryDownload(string id) + { + throw new NotSupportedException(); + } + + public override DownloadClientStatus GetStatus() + { + return new DownloadClientStatus + { + IsLocalhost = true, + OutputRootFolders = new List { Settings.WatchFolder } + }; + } + + public override void Test(UsenetBlackholeSettings settings) + { + PerformWriteTest(settings.NzbFolder); + PerformWriteTest(settings.WatchFolder); + } + + private void PerformWriteTest(string folder) + { + var testPath = Path.Combine(folder, "drone_test.txt"); + _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); + _diskProvider.DeleteFile(testPath); + } + + public void Execute(TestUsenetBlackholeCommand message) + { + var settings = new UsenetBlackholeSettings(); + settings.InjectFrom(message); + + Test(settings); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs new file mode 100644 index 000000000..dd5371af8 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs @@ -0,0 +1,36 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Core.Download.Clients.UsenetBlackhole +{ + public class UsenetBlackholeSettingsValidator : AbstractValidator + { + public UsenetBlackholeSettingsValidator() + { + //Todo: Validate that the path actually exists + RuleFor(c => c.NzbFolder).IsValidPath(); + RuleFor(c => c.WatchFolder).IsValidPath(); + } + } + + public class UsenetBlackholeSettings : IProviderConfig + { + private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); + + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path)] + public String NzbFolder { get; set; } + + [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path)] + public String WatchFolder { get; set; } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs new file mode 100644 index 000000000..7ae4b422b --- /dev/null +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using System.IO; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Download +{ + public interface ICompletedDownloadService + { + void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List grabbedHistory, List importedHistory); + } + + public class CompletedDownloadService : ICompletedDownloadService + { + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly IDiskProvider _diskProvider; + private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IHistoryService _historyService; + private readonly Logger _logger; + + public CompletedDownloadService(IEventAggregator eventAggregator, + IConfigService configService, + IDiskProvider diskProvider, + IDownloadedEpisodesImportService downloadedEpisodesImportService, + IHistoryService historyService, + Logger logger) + { + _eventAggregator = eventAggregator; + _configService = configService; + _diskProvider = diskProvider; + _downloadedEpisodesImportService = downloadedEpisodesImportService; + _historyService = historyService; + _logger = logger; + } + + private List GetHistoryItems(List grabbedHistory, string downloadClientId) + { + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + public void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List grabbedHistory, List importedHistory) + { + if (!_configService.EnableCompletedDownloadHandling) + { + return; + } + + if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Completed && trackedDownload.State == TrackedDownloadState.Downloading) + { + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (!grabbedItems.Any() && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) + { + _logger.Trace("Ignoring download that wasn't grabbed by drone: " + trackedDownload.DownloadItem.Title); + return; + } + + var importedItems = GetHistoryItems(importedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (importedItems.Any()) + { + trackedDownload.State = TrackedDownloadState.Imported; + + _logger.Debug("Already added to history as imported: " + trackedDownload.DownloadItem.Title); + } + else + { + string downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; + string downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; + if (downloadItemOutputPath.IsNullOrWhiteSpace()) + { + _logger.Trace("Storage path not specified: " + trackedDownload.DownloadItem.Title); + return; + } + + if (!downloadedEpisodesFolder.IsNullOrWhiteSpace() && (downloadedEpisodesFolder.PathEquals(downloadItemOutputPath) || downloadedEpisodesFolder.IsParentPath(downloadItemOutputPath))) + { + _logger.Trace("Storage path inside drone factory, ignoring download: " + trackedDownload.DownloadItem.Title); + return; + } + + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath)) + { + var decisions = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem); + + if (decisions.Any()) + { + trackedDownload.State = TrackedDownloadState.Imported; + } + } + else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath)) + { + var decisions = _downloadedEpisodesImportService.ProcessFile(new FileInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem); + + if (decisions.Any()) + { + trackedDownload.State = TrackedDownloadState.Imported; + } + } + else + { + if (grabbedItems.Any()) + { + var episodeIds = trackedDownload.DownloadItem.RemoteEpisode.Episodes.Select(v => v.Id).ToList(); + + // Check if we can associate it with a previous drone factory import. + importedItems = importedHistory.Where(v => v.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID) == null && + episodeIds.Contains(v.EpisodeId) && + v.Data.GetValueOrDefault("droppedPath") != null && + new FileInfo(v.Data["droppedPath"]).Directory.Name == grabbedItems.First().SourceTitle + ).ToList(); + if (importedItems.Count == 1) + { + var importedFile = new FileInfo(importedItems.First().Data["droppedPath"]); + + if (importedFile.Directory.Name == grabbedItems.First().SourceTitle) + { + trackedDownload.State = TrackedDownloadState.Imported; + + importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT]; + importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID]; + _historyService.UpdateHistoryData(importedItems.First().Id, importedItems.First().Data); + + _logger.Debug("Storage path does not exist, but found probable drone factory ImportEvent: " + trackedDownload.DownloadItem.Title); + return; + } + } + } + + _logger.Debug("Storage path does not exist: " + trackedDownload.DownloadItem.Title); + return; + } + } + } + + if (_configService.RemoveCompletedDownloads && trackedDownload.State == TrackedDownloadState.Imported && !trackedDownload.DownloadItem.IsReadOnly) + { + try + { + _logger.Info("Removing completed download from history: {0}", trackedDownload.DownloadItem.Title); + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId); + + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath)) + { + _logger.Info("Removing completed download directory: {0}", trackedDownload.DownloadItem.OutputPath); + _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath, true); + } + else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath)) + { + _logger.Info("Removing completed download file: {0}", trackedDownload.DownloadItem.OutputPath); + _diskProvider.DeleteFile(trackedDownload.DownloadItem.OutputPath); + } + + trackedDownload.State = TrackedDownloadState.Removed; + } + catch (NotSupportedException) + { + _logger.Debug("Removing item not supported by your download client"); + } + } + } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 8cf5a0717..2176ba21f 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,12 +1,22 @@ using System; +using System.Linq; using System.Collections.Generic; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Configuration; +using NLog; namespace NzbDrone.Core.Download { - public abstract class DownloadClientBase : IDownloadClient where TSettings : IProviderConfig, new() + public abstract class DownloadClientBase : IDownloadClient + where TSettings : IProviderConfig, new() { + protected readonly IConfigService _configService; + private readonly IParsingService _parsingService; + protected readonly Logger _logger; + public Type ConfigContract { get @@ -33,17 +43,42 @@ namespace NzbDrone.Core.Download } } + protected DownloadClientBase(IConfigService configService, IParsingService parsingService, Logger logger) + { + _configService = configService; + _parsingService = parsingService; + _logger = logger; + } + public override string ToString() { return GetType().Name; } - public abstract string DownloadNzb(RemoteEpisode remoteEpisode); - public abstract IEnumerable GetQueue(); - public abstract IEnumerable GetHistory(int start = 0, int limit = 10); - public abstract void RemoveFromQueue(string id); - public abstract void RemoveFromHistory(string id); + + + public abstract DownloadProtocol Protocol + { + get; + } + + public abstract string Download(RemoteEpisode remoteEpisode); + public abstract IEnumerable GetItems(); + public abstract void RemoveItem(string id); public abstract void RetryDownload(string id); - public abstract void Test(); + public abstract DownloadClientStatus GetStatus(); + + public abstract void Test(TSettings settings); + + protected RemoteEpisode GetRemoteEpisode(String title) + { + var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); + if (parsedEpisodeInfo == null) return null; + + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); + if (remoteEpisode.Series == null) return null; + + return remoteEpisode; + } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index 25b4ee1c8..d48d6d456 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Download { public interface IDownloadClientFactory : IProviderFactory { - List Enabled(); + } public class DownloadClientFactory : ProviderFactory, IDownloadClientFactory @@ -22,9 +22,18 @@ namespace NzbDrone.Core.Download _providerRepository = providerRepository; } - public List Enabled() + protected override List Active() { - return GetAvailableProviders().Where(n => ((DownloadClientDefinition)n.Definition).Enable).ToList(); + return base.Active().Where(c => c.Enable).ToList(); + } + + protected override DownloadClientDefinition GetProviderCharacteristics(IDownloadClient provider, DownloadClientDefinition definition) + { + definition = base.GetProviderCharacteristics(provider, definition); + + definition.Protocol = provider.Protocol; + + return definition; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs new file mode 100644 index 000000000..8b9365c9c --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -0,0 +1,30 @@ +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientItem + { + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } + public String Category { get; set; } + public String Title { get; set; } + + public Int64 TotalSize { get; set; } + public Int64 RemainingSize { get; set; } + public TimeSpan? DownloadTime { get; set; } + public TimeSpan? RemainingTime { get; set; } + + public String OutputPath { get; set; } + public String Message { get; set; } + + public DownloadItemStatus Status { get; set; } + public Boolean IsEncrypted { get; set; } + public Boolean IsReadOnly { get; set; } + + public RemoteEpisode RemoteEpisode { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 8fae72188..d0da96f6d 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -1,10 +1,14 @@ -using System.Linq; +using System; +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Download { public interface IProvideDownloadClient { - IDownloadClient GetDownloadClient(); + IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol); + IEnumerable GetDownloadClients(); } public class DownloadClientProvider : IProvideDownloadClient @@ -16,9 +20,14 @@ namespace NzbDrone.Core.Download _downloadClientFactory = downloadClientFactory; } - public IDownloadClient GetDownloadClient() + public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol) { - return _downloadClientFactory.Enabled().FirstOrDefault(); + return _downloadClientFactory.GetAvailableProviders().FirstOrDefault(v => v.Protocol == downloadProtocol); + } + + public IEnumerable GetDownloadClients() + { + return _downloadClientFactory.GetAvailableProviders(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadClientStatus.cs b/src/NzbDrone.Core/Download/DownloadClientStatus.cs new file mode 100644 index 000000000..ef4f71b38 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientStatus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientStatus + { + public Boolean IsLocalhost { get; set; } + public List OutputRootFolders { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadItemStatus.cs b/src/NzbDrone.Core/Download/DownloadItemStatus.cs new file mode 100644 index 000000000..4ea8f4342 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadItemStatus.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download +{ + public enum DownloadItemStatus + { + Queued = 0, + Paused = 1, + Downloading = 2, + Completed = 3, + Failed = 4 + } +} diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index b8598cec1..874683db3 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -19,7 +19,6 @@ namespace NzbDrone.Core.Download private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; - public DownloadService(IProvideDownloadClient downloadClientProvider, IEventAggregator eventAggregator, Logger logger) { @@ -34,15 +33,15 @@ namespace NzbDrone.Core.Download Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); var downloadTitle = remoteEpisode.Release.Title; - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClient = _downloadClientProvider.GetDownloadClient(remoteEpisode.Release.DownloadProtocol); if (downloadClient == null) { - _logger.Warn("Download client isn't configured yet."); + _logger.Warn("{0} Download client isn't configured yet.", remoteEpisode.Release.DownloadProtocol); return; } - var downloadClientId = downloadClient.DownloadNzb(remoteEpisode); + var downloadClientId = downloadClient.Download(remoteEpisode); var episodeGrabbedEvent = new EpisodeGrabbedEvent(remoteEpisode); episodeGrabbedEvent.DownloadClient = downloadClient.GetType().Name; diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs new file mode 100644 index 000000000..463c06d97 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Queue; + +namespace NzbDrone.Core.Download +{ + public interface IDownloadTrackingService + { + TrackedDownload[] GetTrackedDownloads(); + TrackedDownload[] GetCompletedDownloads(); + TrackedDownload[] GetQueuedDownloads(); + } + + public class DownloadTrackingService : IDownloadTrackingService, IExecute, IHandle, IHandle + { + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly ICompletedDownloadService _completedDownloadService; + private readonly Logger _logger; + + private readonly ICached _trackedDownloadCache; + + public static string DOWNLOAD_CLIENT = "downloadClient"; + public static string DOWNLOAD_CLIENT_ID = "downloadClientId"; + + public DownloadTrackingService(IProvideDownloadClient downloadClientProvider, + IHistoryService historyService, + IEventAggregator eventAggregator, + IConfigService configService, + ICacheManager cacheManager, + IFailedDownloadService failedDownloadService, + ICompletedDownloadService completedDownloadService, + Logger logger) + { + _downloadClientProvider = downloadClientProvider; + _historyService = historyService; + _eventAggregator = eventAggregator; + _configService = configService; + _failedDownloadService = failedDownloadService; + _completedDownloadService = completedDownloadService; + _logger = logger; + + _trackedDownloadCache = cacheManager.GetCache(GetType()); + } + + public TrackedDownload[] GetTrackedDownloads() + { + return _trackedDownloadCache.Get("tracked", () => new TrackedDownload[0]); + } + + public TrackedDownload[] GetCompletedDownloads() + { + return GetTrackedDownloads() + .Where(v => v.State == TrackedDownloadState.Downloading && v.DownloadItem.Status == DownloadItemStatus.Completed) + .ToArray(); + } + + public TrackedDownload[] GetQueuedDownloads() + { + return _trackedDownloadCache.Get("queued", () => + { + UpdateTrackedDownloads(); + + return FilterQueuedDownloads(GetTrackedDownloads()); + + }, TimeSpan.FromSeconds(5.0)); + } + + private TrackedDownload[] FilterQueuedDownloads(IEnumerable trackedDownloads) + { + var enabledFailedDownloadHandling = _configService.EnableFailedDownloadHandling; + var enabledCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; + + return trackedDownloads + .Where(v => v.State == TrackedDownloadState.Downloading) + .Where(v => + v.DownloadItem.Status == DownloadItemStatus.Queued || + v.DownloadItem.Status == DownloadItemStatus.Paused || + v.DownloadItem.Status == DownloadItemStatus.Downloading || + v.DownloadItem.Status == DownloadItemStatus.Failed && enabledFailedDownloadHandling || + v.DownloadItem.Status == DownloadItemStatus.Completed && enabledCompletedDownloadHandling) + .ToArray(); + } + + private List GetHistoryItems(List grabbedHistory, string downloadClientId) + { + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + private Boolean UpdateTrackedDownloads() + { + var downloadClients = _downloadClientProvider.GetDownloadClients(); + + var oldTrackedDownloads = GetTrackedDownloads().ToDictionary(v => v.TrackingId); + var newTrackedDownloads = new Dictionary(); + + var stateChanged = false; + + foreach (var downloadClient in downloadClients) + { + var downloadClientHistory = downloadClient.GetItems().ToList(); + foreach (var downloadItem in downloadClientHistory) + { + var trackingId = String.Format("{0}-{1}", downloadClient.Definition.Id, downloadItem.DownloadClientId); + TrackedDownload trackedDownload; + + if (newTrackedDownloads.ContainsKey(trackingId)) continue; + + if (!oldTrackedDownloads.TryGetValue(trackingId, out trackedDownload)) + { + trackedDownload = new TrackedDownload + { + TrackingId = trackingId, + DownloadClient = downloadClient.Definition.Id, + StartedTracking = DateTime.UtcNow, + State = TrackedDownloadState.Unknown + }; + + _logger.Trace("Started tracking download from history: {0}: {1}", trackedDownload.TrackingId, downloadItem.Title); + stateChanged = true; + } + + trackedDownload.DownloadItem = downloadItem; + + newTrackedDownloads[trackingId] = trackedDownload; + } + } + + foreach (var downloadItem in oldTrackedDownloads.Values.Where(v => !newTrackedDownloads.ContainsKey(v.TrackingId))) + { + if (downloadItem.State != TrackedDownloadState.Removed) + { + downloadItem.State = TrackedDownloadState.Removed; + stateChanged = true; + + _logger.Debug("Item removed from download client by user: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); + } + + _logger.Trace("Stopped tracking download: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); + } + + _trackedDownloadCache.Set("tracked", newTrackedDownloads.Values.ToArray()); + + return stateChanged; + } + + private void ProcessTrackedDownloads() + { + var grabbedHistory = _historyService.Grabbed(); + var failedHistory = _historyService.Failed(); + var importedHistory = _historyService.Imported(); + + var stateChanged = UpdateTrackedDownloads(); + + var downloadClients = _downloadClientProvider.GetDownloadClients(); + var trackedDownloads = GetTrackedDownloads(); + + foreach (var trackedDownload in trackedDownloads) + { + var downloadClient = downloadClients.Single(v => v.Definition.Id == trackedDownload.DownloadClient); + + var state = trackedDownload.State; + + if (trackedDownload.State == TrackedDownloadState.Unknown) + { + trackedDownload.State = TrackedDownloadState.Downloading; + } + + _failedDownloadService.CheckForFailedItem(downloadClient, trackedDownload, grabbedHistory, failedHistory); + _completedDownloadService.CheckForCompletedItem(downloadClient, trackedDownload, grabbedHistory, importedHistory); + + if (state != trackedDownload.State) + { + stateChanged = true; + } + } + + _trackedDownloadCache.Set("queued", FilterQueuedDownloads(trackedDownloads), TimeSpan.FromSeconds(5.0)); + + if (stateChanged) + { + _eventAggregator.PublishEvent(new UpdateQueueEvent()); + } + } + + public void Execute(CheckForFinishedDownloadCommand message) + { + ProcessTrackedDownloads(); + } + + public void Handle(ApplicationStartedEvent message) + { + ProcessTrackedDownloads(); + } + + public void Handle(EpisodeGrabbedEvent message) + { + ProcessTrackedDownloads(); + } + } +} diff --git a/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs deleted file mode 100644 index 0475ceaf2..000000000 --- a/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Download.Events -{ - public class DownloadFailedEvent : IEvent - { - public Int32 SeriesId { get; set; } - public List EpisodeIds { get; set; } - public QualityModel Quality { get; set; } - public String SourceTitle { get; set; } - public String DownloadClient { get; set; } - public String DownloadClientId { get; set; } - public String Message { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs b/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs deleted file mode 100644 index 887e42362..000000000 --- a/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Events -{ - public class EpisodeGrabbedEvent : IEvent - { - public RemoteEpisode Episode { get; private set; } - public String DownloadClient { get; set; } - public String DownloadClientId { get; set; } - - public EpisodeGrabbedEvent(RemoteEpisode episode) - { - Episode = episode; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownload.cs b/src/NzbDrone.Core/Download/FailedDownload.cs deleted file mode 100644 index eead58f05..000000000 --- a/src/NzbDrone.Core/Download/FailedDownload.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download -{ - public class FailedDownload - { - public HistoryItem DownloadClientHistoryItem { get; set; } - public DateTime LastRetry { get; set; } - public Int32 RetryCount { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index e5be96880..cc25cf872 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -14,35 +14,25 @@ namespace NzbDrone.Core.Download public interface IFailedDownloadService { void MarkAsFailed(int historyId); + void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List grabbedHistory, List failedHistory); } - public class FailedDownloadService : IFailedDownloadService, IExecute + public class FailedDownloadService : IFailedDownloadService { - private readonly IProvideDownloadClient _downloadClientProvider; private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly Logger _logger; - private readonly ICached _failedDownloads; - - private static string DOWNLOAD_CLIENT = "downloadClient"; - private static string DOWNLOAD_CLIENT_ID = "downloadClientId"; - - public FailedDownloadService(IProvideDownloadClient downloadClientProvider, - IHistoryService historyService, + public FailedDownloadService(IHistoryService historyService, IEventAggregator eventAggregator, IConfigService configService, - ICacheManager cacheManager, Logger logger) { - _downloadClientProvider = downloadClientProvider; _historyService = historyService; _eventAggregator = eventAggregator; _configService = configService; _logger = logger; - - _failedDownloads = cacheManager.GetCache(GetType()); } public void MarkAsFailed(int historyId) @@ -51,149 +41,92 @@ namespace NzbDrone.Core.Download PublishDownloadFailedEvent(new List { item }, "Manually marked as failed"); } - private void CheckQueue(List grabbedHistory, List failedHistory) + public void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List grabbedHistory, List failedHistory) { - var downloadClient = GetDownloadClient(); - - if (downloadClient == null) + if (!_configService.EnableFailedDownloadHandling) { return; } - var downloadClientQueue = downloadClient.GetQueue().ToList(); - var failedItems = downloadClientQueue.Where(q => q.Title.StartsWith("ENCRYPTED / ")).ToList(); - - if (!failedItems.Any()) + if (trackedDownload.DownloadItem.IsEncrypted && trackedDownload.State == TrackedDownloadState.Downloading) { - _logger.Debug("Yay! No encrypted downloads"); - return; - } + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); - foreach (var failedItem in failedItems) - { - var failedLocal = failedItem; - var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); - - if (!historyItems.Any()) + if (!grabbedItems.Any()) { - _logger.Debug("Unable to find matching history item"); - continue; + _logger.Debug("Download was not grabbed by drone, ignoring."); + return; } - if (failedHistory.Any(h => failedLocal.Id.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)))) + trackedDownload.State = TrackedDownloadState.DownloadFailed; + + var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (failedItems.Any()) { _logger.Debug("Already added to history as failed"); - continue; } - - PublishDownloadFailedEvent(historyItems, "Encrypted download detected"); - - if (_configService.RemoveFailedDownloads) + else { - _logger.Info("Removing encrypted download from queue: {0}", failedItem.Title.Replace("ENCRYPTED / ", "")); - downloadClient.RemoveFromQueue(failedItem.Id); + PublishDownloadFailedEvent(grabbedItems, "Encrypted download detected"); } } - } - private void CheckHistory(List grabbedHistory, List failedHistory) - { - var downloadClient = GetDownloadClient(); - - if (downloadClient == null) + if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed && trackedDownload.State == TrackedDownloadState.Downloading) { - return; - } + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); - var downloadClientHistory = downloadClient.GetHistory(0, 20).ToList(); - var failedItems = downloadClientHistory.Where(h => h.Status == HistoryStatus.Failed).ToList(); - - if (!failedItems.Any()) - { - _logger.Debug("Yay! No failed downloads"); - return; - } - - foreach (var failedItem in failedItems) - { - var failedLocal = failedItem; - var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); - - if (!historyItems.Any()) + if (!grabbedItems.Any()) { - _logger.Debug("Unable to find matching history item"); - continue; + _logger.Debug("Download was not grabbed by drone, ignoring."); + return; } //TODO: Make this more configurable (ignore failure reasons) to support changes and other failures that should be ignored - if (failedLocal.Message.Equals("Unpacking failed, write error or disk is full?", + if (trackedDownload.DownloadItem.Message.Equals("Unpacking failed, write error or disk is full?", StringComparison.InvariantCultureIgnoreCase)) { _logger.Debug("Failed due to lack of disk space, do not blacklist"); - continue; + return; } - if (FailedDownloadForRecentRelease(failedItem, historyItems)) + if (FailedDownloadForRecentRelease(downloadClient, trackedDownload, grabbedItems)) { _logger.Debug("Recent release Failed, do not blacklist"); - continue; + return; } - - if (failedHistory.Any(h => failedLocal.Id.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)))) + + trackedDownload.State = TrackedDownloadState.DownloadFailed; + + var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (failedItems.Any()) { _logger.Debug("Already added to history as failed"); - continue; } - - PublishDownloadFailedEvent(historyItems, failedItem.Message); - - if (_configService.RemoveFailedDownloads) + else { - _logger.Info("Removing failed download from history: {0}", failedItem.Title); - downloadClient.RemoveFromHistory(failedItem.Id); + PublishDownloadFailedEvent(grabbedItems, trackedDownload.DownloadItem.Message); + } + } + + if (_configService.RemoveFailedDownloads && trackedDownload.State == TrackedDownloadState.DownloadFailed) + { + try + { + _logger.Info("Removing failed download from client: {0}", trackedDownload.DownloadItem.Title); + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId); + + trackedDownload.State = TrackedDownloadState.Removed; + } + catch (NotSupportedException) + { + _logger.Debug("Removing item not supported by your download client"); } } } - private List GetHistoryItems(List grabbedHistory, string downloadClientId) - { - return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID))) - .ToList(); - } - - private void PublishDownloadFailedEvent(List historyItems, string message) - { - var historyItem = historyItems.First(); - - var downloadFailedEvent = new DownloadFailedEvent - { - SeriesId = historyItem.SeriesId, - EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), - Quality = historyItem.Quality, - SourceTitle = historyItem.SourceTitle, - DownloadClient = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT), - DownloadClientId = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID), - Message = message - }; - - downloadFailedEvent.Data = downloadFailedEvent.Data.Merge(historyItem.Data); - - _eventAggregator.PublishEvent(downloadFailedEvent); - } - - private IDownloadClient GetDownloadClient() - { - var downloadClient = _downloadClientProvider.GetDownloadClient(); - - if (downloadClient == null) - { - _logger.Debug("No download client is configured"); - } - - return downloadClient; - } - - private bool FailedDownloadForRecentRelease(HistoryItem failedDownloadHistoryItem, List matchingHistoryItems) + private bool FailedDownloadForRecentRelease(IDownloadClient downloadClient, TrackedDownload trackedDownload, List matchingHistoryItems) { double ageHours; @@ -209,31 +142,23 @@ namespace NzbDrone.Core.Download return false; } - var tracked = _failedDownloads.Get(failedDownloadHistoryItem.Id, () => new FailedDownload - { - DownloadClientHistoryItem = failedDownloadHistoryItem, - LastRetry = DateTime.UtcNow - } - ); - - if (tracked.RetryCount >= _configService.BlacklistRetryLimit) + if (trackedDownload.RetryCount >= _configService.BlacklistRetryLimit) { _logger.Debug("Retry limit reached"); return false; } - if (tracked.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow) + if (trackedDownload.RetryCount == 0 || trackedDownload.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow) { _logger.Debug("Retrying failed release"); - tracked.LastRetry = DateTime.UtcNow; - tracked.RetryCount++; + trackedDownload.LastRetry = DateTime.UtcNow; + trackedDownload.RetryCount++; try { - GetDownloadClient().RetryDownload(failedDownloadHistoryItem.Id); + downloadClient.RetryDownload(trackedDownload.DownloadItem.DownloadClientId); } - - catch (NotImplementedException ex) + catch (NotSupportedException ex) { _logger.Debug("Retrying failed downloads is not supported by your download client"); return false; @@ -243,19 +168,30 @@ namespace NzbDrone.Core.Download return true; } - public void Execute(CheckForFailedDownloadCommand message) + private List GetHistoryItems(List grabbedHistory, string downloadClientId) { - if (!_configService.EnableFailedDownloadHandling) + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + private void PublishDownloadFailedEvent(List historyItems, string message) + { + var historyItem = historyItems.First(); + + var downloadFailedEvent = new DownloadFailedEvent { - _logger.Debug("Failed Download Handling is not enabled"); - return; - } + SeriesId = historyItem.SeriesId, + EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), + Quality = historyItem.Quality, + SourceTitle = historyItem.SourceTitle, + DownloadClient = historyItem.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT), + DownloadClientId = historyItem.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID), + Message = message + }; - var grabbedHistory = _historyService.Grabbed(); - var failedHistory = _historyService.Failed(); + downloadFailedEvent.Data = downloadFailedEvent.Data.Merge(historyItem.Data); - CheckQueue(grabbedHistory, failedHistory); - CheckHistory(grabbedHistory, failedHistory); + _eventAggregator.PublishEvent(downloadFailedEvent); } } } diff --git a/src/NzbDrone.Core/Download/HistoryItem.cs b/src/NzbDrone.Core/Download/HistoryItem.cs deleted file mode 100644 index 9475b527d..000000000 --- a/src/NzbDrone.Core/Download/HistoryItem.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download -{ - public class HistoryItem - { - public String Id { get; set; } - public String Title { get; set; } - public String Size { get; set; } - public String Category { get; set; } - public Int32 DownloadTime { get; set; } - public String Storage { get; set; } - public String Message { get; set; } - public HistoryStatus Status { get; set; } - } - - public enum HistoryStatus - { - Completed = 0, - Failed = 1 - } -} diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index d246e9645..b0b3d5fb9 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -6,12 +7,13 @@ namespace NzbDrone.Core.Download { public interface IDownloadClient : IProvider { - string DownloadNzb(RemoteEpisode remoteEpisode); - IEnumerable GetQueue(); - IEnumerable GetHistory(int start = 0, int limit = 0); - void RemoveFromQueue(string id); - void RemoveFromHistory(string id); + DownloadProtocol Protocol { get; } + + string Download(RemoteEpisode remoteEpisode); + IEnumerable GetItems(); + void RemoveItem(string id); void RetryDownload(string id); - void Test(); + + DownloadClientStatus GetStatus(); } } diff --git a/src/NzbDrone.Core/Download/QueueItem.cs b/src/NzbDrone.Core/Download/QueueItem.cs deleted file mode 100644 index 9112680b9..000000000 --- a/src/NzbDrone.Core/Download/QueueItem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download -{ - public class QueueItem - { - public string Id { get; set; } - public decimal Size { get; set; } - public string Title { get; set; } - public decimal Sizeleft { get; set; } - public TimeSpan Timeleft { get; set; } - public String Status { get; set; } - public RemoteEpisode RemoteEpisode { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownload.cs new file mode 100644 index 000000000..9d490c51e --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownload.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Download +{ + public class TrackedDownload + { + public String TrackingId { get; set; } + public Int32 DownloadClient { get; set; } + public DownloadClientItem DownloadItem { get; set; } + public TrackedDownloadState State { get; set; } + public DateTime StartedTracking { get; set; } + public DateTime LastRetry { get; set; } + public Int32 RetryCount { get; set; } + } + + public enum TrackedDownloadState + { + Unknown, + Downloading, + Imported, + DownloadFailed, + Removed + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs new file mode 100644 index 000000000..7567ef095 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs @@ -0,0 +1,34 @@ +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class AppDataLocationCheck : HealthCheckBase + { + private readonly IAppFolderInfo _appFolderInfo; + + public AppDataLocationCheck(IAppFolderInfo appFolderInfo) + { + _appFolderInfo = appFolderInfo; + } + + public override HealthCheck Check() + { + if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || + _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Updating will not be possible to prevent deleting AppData on Update"); + } + + return new HealthCheck(GetType()); + } + + public override bool CheckOnConfigChange + { + get + { + return false; + } + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs index b45ee036f..f86bdf679 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NzbDrone.Core.Download; namespace NzbDrone.Core.HealthCheck.Checks @@ -14,16 +15,19 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClients = _downloadClientProvider.GetDownloadClients(); - if (downloadClient == null) + if (downloadClients.Count() == 0) { return new HealthCheck(GetType(), HealthCheckResult.Warning, "No download client is available"); } try { - downloadClient.GetQueue(); + foreach (var downloadClient in downloadClients) + { + downloadClient.GetItems(); + } } catch (Exception) { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs index b26cf3404..f539b58e1 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.HealthCheck.Checks if (droneFactoryFolder.IsNullOrWhiteSpace()) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Drone factory folder is not configured"); + return new HealthCheck(GetType()); } if (!_diskProvider.FolderExists(droneFactoryFolder)) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs new file mode 100644 index 000000000..1234b8730 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Sabnzbd; +using NzbDrone.Core.Download.Clients.Nzbget; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class ImportMechanismCheck : HealthCheckBase + { + private readonly IConfigService _configService; + private readonly IProvideDownloadClient _provideDownloadClient; + private readonly IDownloadTrackingService _downloadTrackingService; + + public ImportMechanismCheck(IConfigService configService, IProvideDownloadClient provideDownloadClient, IDownloadTrackingService downloadTrackingService) + { + _configService = configService; + _provideDownloadClient = provideDownloadClient; + _downloadTrackingService = downloadTrackingService; + } + + public override HealthCheck Check() + { + var droneFactoryFolder = _configService.DownloadedEpisodesFolder; + var downloadClients = _provideDownloadClient.GetDownloadClients().Select(v => new { downloadClient = v, status = v.GetStatus() }).ToList(); + + var downloadClientIsLocalHost = downloadClients.All(v => v.status.IsLocalhost); + var downloadClientOutputInDroneFactory = !droneFactoryFolder.IsNullOrWhiteSpace() + && downloadClients.Any(v => v.status.OutputRootFolders != null && v.status.OutputRootFolders.Contains(droneFactoryFolder, PathEqualityComparer.Instance)); + + if (!_configService.IsDefined("EnableCompletedDownloadHandling")) + { + // Migration helper logic + if (!downloadClientIsLocalHost) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "Migrating-to-Completed-Download-Handling#Unsupported-download-client-on-different-computer"); + } + + if (downloadClients.All(v => v.downloadClient is Sabnzbd)) + { + // With Sabnzbd we cannot check the category settings. + + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Sabnzbd)", "Migrating-to-Completed-Download-Handling#sabnzbd-enable-completed-download-handling"); + } + else if (downloadClients.All(v => v.downloadClient is Nzbget)) + { + // With Nzbget we can check if the category should be changed. + if (downloadClientOutputInDroneFactory) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget - Conflicting Category)", "Migrating-to-Completed-Download-Handling#nzbget-conflicting-download-client-category"); + } + + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget)", "Migrating-to-Completed-Download-Handling#nzbget-enable-completed-download-handling"); + } + else + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible", "Migrating-to-Completed-Download-Handling"); + } + } + + if (!_configService.EnableCompletedDownloadHandling && droneFactoryFolder.IsNullOrWhiteSpace()) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling or configure Drone factory"); + } + + if (_configService.EnableCompletedDownloadHandling && !droneFactoryFolder.IsNullOrWhiteSpace()) + { + if (_downloadTrackingService.GetCompletedDownloads().Any(v => droneFactoryFolder.PathEquals(v.DownloadItem.OutputPath) || droneFactoryFolder.IsParentPath(v.DownloadItem.OutputPath))) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Completed Download Handling conflict with Drone Factory (Conflicting History Item)", "Migrating-to-Completed-Download-Handling#conflicting-download-client-category"); + } + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs index 0c44b0947..59a79c4cc 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs @@ -28,11 +28,11 @@ namespace NzbDrone.Core.HealthCheck.Checks { if (missingRootFolders.Count == 1) { - return new HealthCheck(GetType(), HealthCheckResult.Error, "Missing root folder: " + missingRootFolders.First()); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Missing root folder: " + missingRootFolders.First(), "#missing-root-folder"); } var message = String.Format("Multiple root folders are missing: {0}", String.Join(" | ", missingRootFolders)); - return new HealthCheck(GetType(), HealthCheckResult.Error, message); + return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#missing-root-folder"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 0eb187b29..704eb2799 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -2,6 +2,7 @@ using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; namespace NzbDrone.Core.HealthCheck.Checks @@ -11,19 +12,22 @@ namespace NzbDrone.Core.HealthCheck.Checks private readonly IDiskProvider _diskProvider; private readonly IAppFolderInfo _appFolderInfo; private readonly ICheckUpdateService _checkUpdateService; + private readonly IConfigFileProvider _configFileProvider; - public UpdateCheck(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, ICheckUpdateService checkUpdateService) + public UpdateCheck(IDiskProvider diskProvider, + IAppFolderInfo appFolderInfo, + ICheckUpdateService checkUpdateService, + IConfigFileProvider configFileProvider) { _diskProvider = diskProvider; _appFolderInfo = appFolderInfo; _checkUpdateService = checkUpdateService; + _configFileProvider = configFileProvider; } - - + public override HealthCheck Check() { - //TODO: Check on mono as well - if (OsInfo.IsWindows) + if (OsInfo.IsWindows || (OsInfo.IsMono && _configFileProvider.UpdateAutomatically)) { try { @@ -33,8 +37,7 @@ namespace NzbDrone.Core.HealthCheck.Checks } catch (Exception) { - return new HealthCheck(GetType(), HealthCheckResult.Error, - "Unable to update, running from write-protected folder"); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to update, running from write-protected folder"); } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs index 183849ecb..31f0a3035 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs @@ -1,13 +1,17 @@ using System; +using System.Text.RegularExpressions; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.HealthCheck { public class HealthCheck : ModelBase { + private static readonly Regex CleanFragmentRegex = new Regex("[^a-z ]", RegexOptions.Compiled); + public Type Source { get; set; } public HealthCheckResult Type { get; set; } public String Message { get; set; } + public Uri WikiUrl { get; set; } public HealthCheck(Type source) { @@ -15,11 +19,25 @@ namespace NzbDrone.Core.HealthCheck Type = HealthCheckResult.Ok; } - public HealthCheck(Type source, HealthCheckResult type, string message) + public HealthCheck(Type source, HealthCheckResult type, String message, String wikiFragment = null) { Source = source; Type = type; Message = message; + WikiUrl = MakeWikiUrl(wikiFragment ?? MakeWikiFragment(message)); + } + + private static String MakeWikiFragment(String message) + { + return "#" + CleanFragmentRegex.Replace(message.ToLower(), String.Empty).Replace(' ', '-'); + } + + private static Uri MakeWikiUrl(String fragment) + { + var rootUri = new Uri("https://github.com/NzbDrone/NzbDrone/wiki/Health-checks"); + var fragmentUri = new Uri(fragment, UriKind.Relative); + + return new Uri(rootUri, fragmentUri); } } diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index dead6df7c..b76a3be12 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.History List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); List Grabbed(); + List Imported(); History MostRecentForEpisode(int episodeId); List FindBySourceTitle(string sourceTitle); } @@ -62,6 +63,11 @@ namespace NzbDrone.Core.History return Query.Where(h => h.EventType == HistoryEventType.Grabbed); } + public List Imported() + { + return Query.Where(h => h.EventType == HistoryEventType.DownloadFolderImported); + } + public History MostRecentForEpisode(int episodeId) { return Query.Where(h => h.EpisodeId == episodeId) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index c95bc233a..e9846031e 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -21,9 +21,11 @@ namespace NzbDrone.Core.History List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); List Grabbed(); + List Imported(); History MostRecentForEpisode(int episodeId); History Get(int id); List FindBySourceTitle(string sourceTitle); + void UpdateHistoryData(Int32 historyId, Dictionary data); } public class HistoryService : IHistoryService, IHandle, IHandle, IHandle @@ -62,6 +64,11 @@ namespace NzbDrone.Core.History return _historyRepository.Grabbed(); } + public List Imported() + { + return _historyRepository.Imported(); + } + public History MostRecentForEpisode(int episodeId) { return _historyRepository.MostRecentForEpisode(episodeId); @@ -95,6 +102,13 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } + public void UpdateHistoryData(Int32 historyId, Dictionary data) + { + var history = _historyRepository.Get(historyId); + history.Data = data; + _historyRepository.Update(history); + } + public void Handle(EpisodeGrabbedEvent message) { foreach (var episode in message.Episode.Episodes) @@ -149,6 +163,8 @@ namespace NzbDrone.Core.History //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); history.Data.Add("DroppedPath", message.EpisodeInfo.Path); history.Data.Add("ImportedPath", message.ImportedEpisode.Path); + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("DownloadClientId", message.DownloadClientId); _historyRepository.Insert(history); } diff --git a/src/NzbDrone.Core/Indexers/DownloadProtocol.cs b/src/NzbDrone.Core/Indexers/DownloadProtocol.cs new file mode 100644 index 000000000..eac150ce6 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/DownloadProtocol.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Indexers +{ + public enum DownloadProtocol + { + Usenet = 1, + Torrent = 2 + } +} diff --git a/src/NzbDrone.Core/Indexers/DownloadProtocols.cs b/src/NzbDrone.Core/Indexers/DownloadProtocols.cs deleted file mode 100644 index 4fff5e07d..000000000 --- a/src/NzbDrone.Core/Indexers/DownloadProtocols.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Indexers -{ - public enum DownloadProtocols - { - Nzb = 0, - Torrent =1 - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs deleted file mode 100644 index f926d911e..000000000 --- a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.Indexers.Eztv -{ - public class Eztv : IndexerBase - { - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Torrent; - } - } - - public override bool SupportsPaging - { - get - { - return false; - } - } - - public override IParseFeed Parser - { - get - { - return new BasicTorrentRssParser(); - } - } - - public override IEnumerable RecentFeed - { - get - { - yield return "http://www.ezrss.it/feed/"; - } - } - - public override IEnumerable GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber) - { - yield return string.Format("http://www.ezrss.it/search/index.php?show_name={0}&season={1}&episode={2}&mode=rss", seriesTitle, seasonNumber, episodeNumber); - } - - public override IEnumerable GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset) - { - yield return string.Format("http://www.ezrss.it/search/index.php?show_name={0}&season={1}&mode=rss", seriesTitle, seasonNumber); - - } - - public override IEnumerable GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date) - { - //EZTV doesn't support searching based on actual episode airdate. they only support release date. - return new string[0]; - } - - public override IEnumerable GetSearchUrls(string query, int offset) - { - return new List(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 141145e9f..fd70a2473 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers { IParseFeed Parser { get; } DownloadProtocol Protocol { get; } + Int32 SupportedPageSize { get; } Boolean SupportsPaging { get; } Boolean SupportsSearching { get; } diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 96fe2b837..bb73432c6 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -34,8 +34,10 @@ namespace NzbDrone.Core.Indexers public abstract DownloadProtocol Protocol { get; } - public abstract bool SupportsPaging { get; } - public virtual bool SupportsSearching { get { return true; } } + public virtual Boolean SupportsFeed { get { return true; } } + public virtual Int32 SupportedPageSize { get { return 0; } } + public bool SupportsPaging { get { return SupportedPageSize > 0; } } + public virtual Boolean SupportsSearching { get { return true; } } protected TSettings Settings { @@ -58,10 +60,4 @@ namespace NzbDrone.Core.Indexers return Definition.Name; } } - - public enum DownloadProtocol - { - Usenet = 1, - Torrent = 2 - } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs index 4a061129e..c1509952e 100644 --- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs +++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs @@ -5,5 +5,7 @@ namespace NzbDrone.Core.Indexers public class IndexerDefinition : ProviderDefinition { public bool Enable { get; set; } + + public DownloadProtocol Protocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 03d0450b7..2b214ed7c 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -31,17 +31,7 @@ namespace NzbDrone.Core.Indexers protected override void InitializeProviders() { - var definitions = _providers.Where(c => c.Protocol == DownloadProtocol.Usenet) - .SelectMany(indexer => indexer.DefaultDefinitions); - var currentProviders = All(); - - var newProviders = definitions.Where(def => currentProviders.All(c => c.Implementation != def.Implementation)).ToList(); - - if (newProviders.Any()) - { - _providerRepository.InsertMany(newProviders.Cast().ToList()); - } } protected override List Active() @@ -59,5 +49,14 @@ namespace NzbDrone.Core.Indexers return base.Create(definition); } + + protected override IndexerDefinition GetProviderCharacteristics(IIndexer provider, IndexerDefinition definition) + { + definition = base.GetProviderCharacteristics(provider, definition); + + definition.Protocol = provider.Protocol; + + return definition; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs index 678a5be1a..154c160a0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -63,11 +63,9 @@ namespace NzbDrone.Core.Indexers _logger.Info("{0} offset {1}. Found {2}", indexer, searchCriteria, result.Count); - if (result.Count > 90 && - offset < 900 && - indexer.SupportsPaging) + if (indexer.SupportsPaging && result.Count >= indexer.SupportedPageSize && offset < 900) { - result.AddRange(Fetch(indexer, searchCriteria, offset + 100)); + result.AddRange(Fetch(indexer, searchCriteria, offset + indexer.SupportedPageSize)); } return result; @@ -152,7 +150,11 @@ namespace NzbDrone.Core.Indexers } } - result.ForEach(c => c.Indexer = indexer.Definition.Name); + result.ForEach(c => + { + c.Indexer = indexer.Definition.Name; + c.DownloadProtocol = indexer.Protocol; + }); return result; } diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index c4c8288a8..9adcd88aa 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -7,6 +7,9 @@ namespace NzbDrone.Core.Indexers.Newznab { public class Newznab : IndexerBase { + public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } + public override Int32 SupportedPageSize { get { return 100; } } + public override IParseFeed Parser { get @@ -72,14 +75,6 @@ namespace NzbDrone.Core.Indexers.Newznab return settings; } - public override bool SupportsPaging - { - get - { - return true; - } - } - public override IEnumerable RecentFeed { get @@ -140,14 +135,6 @@ namespace NzbDrone.Core.Indexers.Newznab return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset)); } - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Usenet; - } - } - private static string NewsnabifyTitle(string title) { return title.Replace("+", "%20"); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs index 85fe2d4e4..cc1919c80 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using NzbDrone.Core.Parser.Model; +using System.Globalization; namespace NzbDrone.Core.Indexers.Newznab { @@ -19,6 +20,21 @@ namespace NzbDrone.Core.Indexers.Newznab return item.Comments().Replace("#comments", ""); } + protected override DateTime GetPublishDate(XElement item) + { + var attributes = item.Elements("attr").ToList(); + var usenetdateElement = attributes.SingleOrDefault(e => e.Attribute("name").Value.Equals("usenetdate", StringComparison.CurrentCultureIgnoreCase)); + + if (usenetdateElement != null) + { + var dateString = usenetdateElement.Attribute("value").Value; + + return XElementExtensions.ParseDate(dateString); + } + + return base.GetPublishDate(item); + } + protected override long GetSize(XElement item) { var attributes = item.Elements("attr").ToList(); diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs index 689138c03..6978213f8 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs @@ -5,13 +5,7 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs { public class Omgwtfnzbs : IndexerBase { - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Usenet; - } - } + public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } public override IParseFeed Parser { @@ -25,7 +19,6 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs { get { - yield return String.Format("http://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user={0}&api={1}&eng=1", Settings.Username, Settings.ApiKey); } @@ -71,13 +64,5 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs { return new List(); } - - public override bool SupportsPaging - { - get - { - return false; - } - } } } diff --git a/src/NzbDrone.Core/Indexers/RssParserBase.cs b/src/NzbDrone.Core/Indexers/RssParserBase.cs index 0988c4f97..8b300c6d5 100644 --- a/src/NzbDrone.Core/Indexers/RssParserBase.cs +++ b/src/NzbDrone.Core/Indexers/RssParserBase.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Indexers try { var reportInfo = ParseFeedItem(item.StripNameSpace(), url); + if (reportInfo != null) { reportInfo.DownloadUrl = GetNzbUrl(item); @@ -69,7 +70,7 @@ namespace NzbDrone.Core.Indexers var reportInfo = CreateNewReleaseInfo(); reportInfo.Title = title; - reportInfo.PublishDate = item.PublishDate(); + reportInfo.PublishDate = GetPublishDate(item); reportInfo.DownloadUrl = GetNzbUrl(item); reportInfo.InfoUrl = GetNzbInfoUrl(item); @@ -92,6 +93,11 @@ namespace NzbDrone.Core.Indexers return item.Title(); } + protected virtual DateTime GetPublishDate(XElement item) + { + return item.PublishDate(); + } + protected virtual string GetNzbUrl(XElement item) { return item.Links().First(); diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs index f61c7ffba..8565ef9b9 100644 --- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs +++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs @@ -6,29 +6,8 @@ namespace NzbDrone.Core.Indexers.Wombles { public class Wombles : IndexerBase { - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Usenet; - } - } - - public override bool SupportsPaging - { - get - { - return false; - } - } - - public override bool SupportsSearching - { - get - { - return false; - } - } + public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } + public override bool SupportsSearching { get { return false; } } public override IParseFeed Parser { diff --git a/src/NzbDrone.Core/Indexers/XElementExtensions.cs b/src/NzbDrone.Core/Indexers/XElementExtensions.cs index 254c9ae6f..fb24e526f 100644 --- a/src/NzbDrone.Core/Indexers/XElementExtensions.cs +++ b/src/NzbDrone.Core/Indexers/XElementExtensions.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Indexers { private static readonly Logger Logger = NzbDroneLogger.GetLogger(); - private static readonly Regex RemoveTimeZoneRegex = new Regex(@"\s[A-Z]{2,4}$", RegexOptions.Compiled); + public static readonly Regex RemoveTimeZoneRegex = new Regex(@"\s[A-Z]{2,4}$", RegexOptions.Compiled); public static string Title(this XElement item) { @@ -35,10 +35,8 @@ namespace NzbDrone.Core.Indexers return res; } - public static DateTime PublishDate(this XElement item) + public static DateTime ParseDate(string dateString) { - string dateString = item.TryGetValue("pubDate"); - try { DateTime result; @@ -56,6 +54,13 @@ namespace NzbDrone.Core.Indexers } } + public static DateTime PublishDate(this XElement item) + { + string dateString = item.TryGetValue("pubDate"); + + return ParseDate(dateString); + } + public static List Links(this XElement item) { var elements = item.Elements("link"); @@ -78,7 +83,7 @@ namespace NzbDrone.Core.Indexers return long.Parse(item.TryGetValue("length")); } - private static string TryGetValue(this XElement item, string elementName, string defaultValue = "") + public static string TryGetValue(this XElement item, string elementName, string defaultValue = "") { var element = item.Element(elementName); diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 459b2ddb5..788e6df0c 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Jobs var defaultTasks = new[] { new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName}, - new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName}, + new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index a4ee5ff99..c8d94c2ed 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -95,7 +95,7 @@ namespace NzbDrone.Core.MediaFiles decisionsStopwatch.Stop(); _logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed); - _importApprovedEpisodes.Import(decisions); + _importApprovedEpisodes.Import(decisions, false); _logger.Info("Completed scanning disk for {0}", series.Title); _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 54d9bb45f..f73e9555c 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -14,10 +14,17 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; namespace NzbDrone.Core.MediaFiles { - public class DownloadedEpisodesImportService : IExecute + public interface IDownloadedEpisodesImportService + { + List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem); + List ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem); + } + + public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService, IExecute { private readonly IDiskProvider _diskProvider; private readonly IDiskScanService _diskScanService; @@ -50,14 +57,58 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } + public List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem) + { + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var series = _parsingService.GetSeries(cleanedUpName); + var quality = QualityParser.ParseQuality(cleanedUpName); + _logger.Debug("{0} folder quality: {1}", cleanedUpName, quality); + + if (series == null) + { + _logger.Debug("Unknown Series {0}", cleanedUpName); + return new List(); + } + + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); + + var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem); + + if (!downloadClientItem.IsReadOnly && importedDecisions.Any() && ShouldDeleteFolder(directoryInfo)) + { + _logger.Debug("Deleting folder after importing valid files"); + _diskProvider.DeleteFolder(directoryInfo.FullName, true); + } + + return importedDecisions; + } + + public List ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem) + { + var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(fileInfo.Name)); + + if (series == null) + { + _logger.Debug("Unknown Series for file: {0}", fileInfo.Name); + return new List(); + } + + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, true, null); + + var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem); + + return importedDecisions; + } + private void ProcessDownloadedEpisodesFolder() { - //TODO: We should also process the download client's category folder var downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; if (String.IsNullOrEmpty(downloadedEpisodesFolder)) { - _logger.Warn("Drone Factory folder is not configured"); + _logger.Trace("Drone Factory folder is not configured"); return; } @@ -100,7 +151,17 @@ namespace NzbDrone.Core.MediaFiles var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); - return ProcessFiles(series, quality, videoFiles); + foreach (var videoFile in videoFiles) + { + if (_diskProvider.IsFileLocked(videoFile)) + { + _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); + return new List(); + } + } + + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); + return _importApprovedEpisodes.Import(decisions, true); } private void ProcessVideoFile(string videoFile) @@ -119,13 +180,8 @@ namespace NzbDrone.Core.MediaFiles return; } - ProcessFiles(series, null, videoFile); - } - - private List ProcessFiles(Series series, QualityModel quality, params string[] videoFiles) - { - var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); - return _importApprovedEpisodes.Import(decisions, true); + var decisions = _importDecisionMaker.GetImportDecisions(new [] { videoFile }.ToList(), series, true, null); + _importApprovedEpisodes.Import(decisions, true); } private void ProcessFolder(string path) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 5e3086835..24f1ab54e 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles { EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series); EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); + EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); } public class EpisodeFileMovingService : IMoveEpisodeFiles @@ -53,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Renaming episode file: {0} to {1}", episodeFile, filePath); - return MoveFile(episodeFile, series, episodes, filePath); + return TransferFile(episodeFile, series, episodes, filePath, false); } public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) @@ -63,10 +64,20 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Moving episode file: {0} to {1}", episodeFile, filePath); - return MoveFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath); + return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, false); } - private EpisodeFile MoveFile(EpisodeFile episodeFile, Series series, List episodes, string destinationFilename) + public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) + { + 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); + + return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, true); + } + + private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List episodes, string destinationFilename, bool copyOnly) { Ensure.That(episodeFile, () => episodeFile).IsNotNull(); Ensure.That(series,() => series).IsNotNull(); @@ -103,8 +114,16 @@ namespace NzbDrone.Core.MediaFiles } } - _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); - _diskProvider.MoveFile(episodeFile.Path, destinationFilename); + if (copyOnly) + { + _logger.Debug("Copying [{0}] > [{1}]", episodeFile.Path, destinationFilename); + _diskProvider.CopyFile(episodeFile.Path, destinationFilename); + } + else + { + _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); + _diskProvider.MoveFile(episodeFile.Path, destinationFilename); + } episodeFile.Path = destinationFilename; _updateEpisodeFileService.ChangeFileDateForFile(episodeFile, series, episodes); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index a62dd7536..598093ff7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -9,13 +9,14 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; namespace NzbDrone.Core.MediaFiles.EpisodeImport { public interface IImportApprovedEpisodes { - List Import(List decisions, bool newDownloads = false); + List Import(List decisions, bool newDownload, DownloadClientItem historyItem = null); } public class ImportApprovedEpisodes : IImportApprovedEpisodes @@ -39,14 +40,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _logger = logger; } - public List Import(List decisions, bool newDownload = false) + public List Import(List decisions, bool newDownload, DownloadClientItem historyItem = null) { 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)) - .ThenByDescending(c => c.LocalEpisode.Size)) - .SelectMany(c => c) - .ToList(); + .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s + .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.QualityProfile)) + .ThenByDescending(c => c.LocalEpisode.Size)) + .SelectMany(c => c) + .ToList(); var imported = new List(); @@ -78,15 +79,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (newDownload) { + bool copyOnly = historyItem != null && historyItem.IsReadOnly; episodeFile.SceneName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath()); - var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode); + var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly); oldFiles = moveResult.OldFiles; } _mediaFileService.Add(episodeFile); imported.Add(importDecision); - _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload)); + if (historyItem != null) + { + _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, historyItem.DownloadClient, historyItem.DownloadClientId)); + } + else + { + _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload)); + } if (newDownload) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs deleted file mode 100644 index 8eb2beaed..000000000 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications -{ - public class NotInUseSpecification : IImportDecisionEngineSpecification - { - private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; - - public NotInUseSpecification(IDiskProvider diskProvider, Logger logger) - { - _diskProvider = diskProvider; - _logger = logger; - } - - public string RejectionReason { get { return "File is in use"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) - { - if (localEpisode.ExistingFile) - { - _logger.Debug("{0} is in series folder, skipping in use check", localEpisode.Path); - return true; - } - - if (_diskProvider.IsFileLocked(localEpisode.Path)) - { - _logger.Debug("{0} is in use"); - return false; - } - - return true; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index 3c52b1bb6..97aa38e93 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -34,19 +34,25 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) { - if (Directory.GetParent(localEpisode.Path).Name.StartsWith(workingFolder)) + DirectoryInfo parent = Directory.GetParent(localEpisode.Path); + while (parent != null) { - if (OsInfo.IsMono) + if (parent.Name.StartsWith(workingFolder)) { - _logger.Debug("{0} is still being unpacked", localEpisode.Path); - return false; + if (OsInfo.IsMono) + { + _logger.Debug("{0} is still being unpacked", localEpisode.Path); + return false; + } + + if (_diskProvider.FileGetLastWriteUtc(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) + { + _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); + return false; + } } - if (_diskProvider.FileGetLastWriteUtc(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) - { - _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); - return false; - } + parent = parent.Parent; } } diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs index d7c1ee6e7..445a1b3a8 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs @@ -9,6 +9,8 @@ namespace NzbDrone.Core.MediaFiles.Events public LocalEpisode EpisodeInfo { get; private set; } public EpisodeFile ImportedEpisode { get; private set; } public Boolean NewDownload { get; set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload) { @@ -16,5 +18,14 @@ namespace NzbDrone.Core.MediaFiles.Events ImportedEpisode = importedEpisode; NewDownload = newDownload; } + + public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadClientId) + { + EpisodeInfo = episodeInfo; + ImportedEpisode = importedEpisode; + NewDownload = newDownload; + DownloadClient = downloadClient; + DownloadClientId = downloadClientId; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index a51121511..750091d94 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.MediaFiles continue; } - if (!DiskProviderBase.IsParent(series.Path, episodeFile.Path)) + if (!series.Path.IsParentPath(episodeFile.Path)) { _logger.Debug("File [{0}] does not belong to this series, removing from db", episodeFile.Path); _mediaFileService.Delete(episodeFile); diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index c3cf4d208..924d60b9c 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.MediaFiles { public interface IUpgradeMediaFiles { - EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); + EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); } public class UpgradeMediaFileService : IUpgradeMediaFiles @@ -32,7 +32,7 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) + public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false) { var moveFileResult = new EpisodeFileMoveResult(); var existingFiles = localEpisode.Episodes @@ -54,7 +54,14 @@ namespace NzbDrone.Core.MediaFiles _mediaFileService.Delete(file, true); } - moveFileResult.EpisodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, localEpisode); + if (copyOnly) + { + moveFileResult.EpisodeFile = _episodeFileMover.CopyEpisodeFile(episodeFile, localEpisode); + } + else + { + moveFileResult.EpisodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, localEpisode); + } return moveFileResult; } diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 52534d5a5..2f3c72ee0 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -77,9 +77,22 @@ namespace NzbDrone.Core.Messaging.Commands _trackCommands.Store(command); - _taskFactory.StartNew(() => ExecuteCommand(command) - , TaskCreationOptions.PreferFairness) - .LogExceptions(); + // TODO: We should use async await (once we get 4.5) or normal Task Continuations on Command processing to prevent blocking the TaskScheduler. + // For now we use TaskCreationOptions 0x10, which is actually .net 4.5 HideScheduler. + // This will detach the scheduler from the thread, causing new Task creating in the command to be executed on the ThreadPool, avoiding a deadlock. + // Please note that the issue only shows itself on mono because since Microsoft .net implementation supports Task inlining on WaitAll. + if (Enum.IsDefined(typeof(TaskCreationOptions), (TaskCreationOptions)0x10)) + { + _taskFactory.StartNew(() => ExecuteCommand(command) + , TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) + .LogExceptions(); + } + else + { + _taskFactory.StartNew(() => ExecuteCommand(command) + , TaskCreationOptions.PreferFairness) + .LogExceptions(); + } return command; } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs index 7031b7ff9..0cc766fa8 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -10,11 +9,8 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; @@ -36,7 +32,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox } private static List ValidCertification = new List { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" }; - private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { @@ -72,7 +68,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox if (!newFilename.PathEquals(existingFilename)) { _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); updatedMetadataFiles.Add(metadataFile); } @@ -93,30 +89,31 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox { SeriesId = series.Id, Consumer = GetType().Name, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + RelativePath = series.Path.GetRelativePath(path) }; //Series and season images are both named folder.jpg, only season ones sit in season folders - if (String.Compare(filename, parentdir.Name, StringComparison.InvariantCultureIgnoreCase) == 0) + if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase)) { var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); + if (seasonMatch.Success) { metadata.Type = MetadataType.SeasonImage; - var seasonNumber = seasonMatch.Groups["season"].Value; - - if (seasonNumber.Contains("specials")) + if (seasonMatch.Groups["specials"].Success) { metadata.SeasonNumber = 0; } + else { - metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); } return metadata; } + else { metadata.Type = MetadataType.SeriesImage; diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs index a8399c0b8..c48f07d01 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -11,11 +9,8 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; @@ -36,7 +31,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv _logger = logger; } - private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { @@ -72,7 +67,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv if (!newFilename.PathEquals(existingFilename)) { _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); updatedMetadataFiles.Add(metadataFile); } @@ -91,11 +86,11 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv { SeriesId = series.Id, Consumer = GetType().Name, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + RelativePath = series.Path.GetRelativePath(path) }; //Series and season images are both named folder.jpg, only season ones sit in season folders - if (String.Compare(filename, "folder.jpg", true) == 0) + if (Path.GetFileName(filename).Equals("folder.jpg", StringComparison.InvariantCultureIgnoreCase)) { var parentdir = Directory.GetParent(path); var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); @@ -103,19 +98,19 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv { metadata.Type = MetadataType.SeasonImage; - var seasonNumber = seasonMatch.Groups["season"].Value; - - if (seasonNumber.Contains("specials")) + if (seasonMatch.Groups["specials"].Success) { metadata.SeasonNumber = 0; } + else { - metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); } return metadata; } + else { metadata.Type = MetadataType.SeriesImage; diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index 20e550e0e..3547fdf52 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc if (!newFilename.PathEquals(existingFilename)) { _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); updatedMetadataFiles.Add(metadataFile); } @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc { SeriesId = series.Id, Consumer = GetType().Name, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + RelativePath = series.Path.GetRelativePath(path) }; if (SeriesImagesRegex.IsMatch(filename)) diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs index ca428d4fc..f6794d6d3 100644 --- a/src/NzbDrone.Core/MetaData/MetadataService.cs +++ b/src/NzbDrone.Core/MetaData/MetadataService.cs @@ -160,7 +160,7 @@ namespace NzbDrone.Core.Metadata _diskProvider.WriteAllText(seriesMetadata.Path, seriesMetadata.Contents); metadata.Hash = hash; - metadata.RelativePath = DiskProviderBase.GetRelativePath(series.Path, seriesMetadata.Path); + metadata.RelativePath = series.Path.GetRelativePath(seriesMetadata.Path); return metadata; } @@ -174,7 +174,7 @@ namespace NzbDrone.Core.Metadata return null; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, episodeMetadata.Path); + var relativePath = series.Path.GetRelativePath(episodeMetadata.Path); var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && c.EpisodeFileId == episodeFile.Id); @@ -226,7 +226,7 @@ namespace NzbDrone.Core.Metadata continue; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + var relativePath = series.Path.GetRelativePath(image.Path); var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage && c.RelativePath == relativePath) ?? @@ -260,7 +260,7 @@ namespace NzbDrone.Core.Metadata continue; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + var relativePath = series.Path.GetRelativePath(image.Path); var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && c.SeasonNumber == season.SeasonNumber && @@ -295,7 +295,7 @@ namespace NzbDrone.Core.Metadata continue; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + var relativePath = series.Path.GetRelativePath(image.Path); var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage && c.EpisodeFileId == episodeFile.Id); @@ -319,7 +319,7 @@ namespace NzbDrone.Core.Metadata EpisodeFileId = episodeFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.EpisodeImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path) + RelativePath = series.Path.GetRelativePath(image.Path) }; DownloadImage(series, image.Url, image.Path); diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index 664778b63..0236a4169 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MetadataSource { private readonly Logger _logger; private static readonly Regex CollapseSpaceRegex = new Regex(@"\s+", RegexOptions.Compiled); - private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!|@)", RegexOptions.Compiled); + private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!|@|\+)", RegexOptions.Compiled); public TraktProxy(Logger logger) { @@ -31,11 +31,43 @@ namespace NzbDrone.Core.MetadataSource { try { - var client = BuildClient("search", "shows"); - var restRequest = new RestRequest(GetSearchTerm(title) + "/30/seasons"); - var response = client.ExecuteAndValidate>(restRequest); + if (title.StartsWith("tvdb:") || title.StartsWith("tvdbid:") || title.StartsWith("slug:")) + { + try + { + var slug = title.Split(':')[1]; - return response.Select(MapSeries).ToList(); + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) + { + return new List(); + } + + var client = BuildClient("show", "summary"); + var restRequest = new RestRequest(GetSearchTerm(slug) + "/extended"); + var response = client.ExecuteAndValidate(restRequest); + + return new List { MapSeries(response) }; + } + catch (RestException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.NotFound) + { + return new List(); + } + + throw; + } + } + else + { + var client = BuildClient("search", "shows"); + var restRequest = new RestRequest(GetSearchTerm(title) + "/30/seasons"); + var response = client.ExecuteAndValidate>(restRequest); + + return response.Select(MapSeries) + .OrderBy(v => title.LevenshteinDistanceClean(v.Title)) + .ToList(); + } } catch (WebException ex) { @@ -170,7 +202,6 @@ namespace NzbDrone.Core.MetadataSource phrase = CollapseSpaceRegex.Replace(phrase, " ").Trim().ToLower(); phrase = phrase.Trim('-'); phrase = HttpUtility.UrlEncode(phrase); - return phrase; } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index 726cac410..d0355835f 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; +using NLog; using NzbDrone.Common; using NzbDrone.Common.Cache; using NzbDrone.Common.Serializer; @@ -19,10 +20,12 @@ namespace NzbDrone.Core.Notifications.Plex public class PlexServerProxy : IPlexServerProxy { private readonly ICached _authCache; + private readonly Logger _logger; - public PlexServerProxy(ICacheManager cacheManager) + public PlexServerProxy(ICacheManager cacheManager, Logger logger) { _authCache = cacheManager.GetCache(GetType(), "authCache"); + _logger = logger; } public List GetTvSections(PlexServerSettings settings) @@ -32,6 +35,9 @@ namespace NzbDrone.Core.Notifications.Plex var response = client.Execute(request); + CheckForError(response.Content); + _logger.Debug("Sections response: {0}", response.Content); + return Json.Deserialize(response.Content) .Directories .Where(d => d.Type == "show") @@ -45,6 +51,9 @@ namespace NzbDrone.Core.Notifications.Plex var client = GetPlexServerClient(settings); var response = client.Execute(request); + + CheckForError(response.Content); + _logger.Debug("Update response: {0}", response.Content); } private String Authenticate(string username, string password) @@ -53,7 +62,9 @@ namespace NzbDrone.Core.Notifications.Plex var client = GetMyPlexClient(username, password); var response = client.Execute(request); + CheckForError(response.Content); + _logger.Debug("Authentication Response: {0}", response.Content); var user = Json.Deserialize(JObject.Parse(response.Content).SelectToken("user").ToString()); @@ -81,7 +92,6 @@ namespace NzbDrone.Core.Notifications.Plex request.AddHeader("X-Plex-Version", "0"); return request; - } private RestClient GetPlexServerClient(PlexServerSettings settings) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index ef39eb353..0f933b570 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -215,7 +215,7 @@ namespace NzbDrone.Core.Notifications.Xbmc postJson.Add(new JProperty("params", parameters)); } - postJson.Add(new JProperty("id", DateTime.Now.Ticks)); + postJson.Add(new JProperty("id", 2)); return postJson; } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 7e3c236e7..ce94da593 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Xbmc var postJson = new JObject(); postJson.Add(new JProperty("jsonrpc", "2.0")); postJson.Add(new JProperty("method", "JSONRPC.Version")); - postJson.Add(new JProperty("id", DateTime.Now.Ticks)); + postJson.Add(new JProperty("id", 1)); var response = _httpProvider.PostCommand(settings.Address, settings.Username, settings.Password, postJson.ToString()); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 389d24a9d..8b70b8ca6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -65,6 +65,10 @@ ..\packages\valueinjecter.2.3.3\lib\net35\Omu.ValueInjecter.dll + + False + ..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll + @@ -90,9 +94,6 @@ ..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll - - ..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll - ..\Libraries\Sqlite\System.Data.SQLite.dll @@ -196,6 +197,7 @@ + @@ -238,10 +240,16 @@ - - + + + + + + + + - + @@ -253,6 +261,7 @@ + @@ -262,22 +271,26 @@ - - - + + + + + + - - + + + @@ -307,10 +320,9 @@ - + - @@ -440,7 +452,6 @@ - @@ -521,13 +532,11 @@ - - + - @@ -700,6 +709,7 @@ + @@ -754,9 +764,7 @@ Always - - - + diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index d2b6201c8..4fb3b2b7a 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -1,4 +1,5 @@ using System; +using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Parser.Model { @@ -10,6 +11,7 @@ namespace NzbDrone.Core.Parser.Model public string InfoUrl { get; set; } public string CommentUrl { get; set; } public String Indexer { get; set; } + public DownloadProtocol DownloadProtocol { get; set; } public DateTime PublishDate { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 8b7987734..f9345db2b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - [SubGroup] Title Absolute Episode Number - new Regex(@"^\[(?.+?)\](?:_|-|\s|\.)?(?.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,}))+", + new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:[ ._-]+(?<absoluteepisode>\d{2,}))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Multi-Part episodes without a title (S01E05.S01E06) @@ -284,18 +284,22 @@ namespace NzbDrone.Core.Parser title = title.TrimEnd("-RP"); - string group; var matches = ReleaseGroupRegex.Matches(title); + if (matches.Count != 0) { - group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; - } - else - { - return defaultReleaseGroup; + var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; + int groupIsNumeric; + + if (Int32.TryParse(group, out groupIsNumeric)) + { + return defaultReleaseGroup; + } + + return group; } - return group; + return defaultReleaseGroup; } public static string RemoveFileExtension(string title) @@ -502,6 +506,9 @@ namespace NzbDrone.Core.Parser if (lowerTitle.Contains("norwegian")) return Language.Norwegian; + if (lowerTitle.Contains("nordic")) + return Language.Norwegian; + if (lowerTitle.Contains("finnish")) return Language.Finnish; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index ce507912e..05f0359ab 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.IndexerSearch.Definitions; @@ -76,7 +77,7 @@ namespace NzbDrone.Core.Parser Episodes = episodes, Path = filename, ParsedEpisodeInfo = parsedEpisodeInfo, - ExistingFile = DiskProviderBase.IsParent(series.Path, filename) + ExistingFile = series.Path.IsParentPath(filename) }; } diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index e8b6e5d5b..fc87bfc39 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -14,18 +14,18 @@ namespace NzbDrone.Core.Parser private static readonly Regex SourceRegex = new Regex(@"\b(?: (?<bluray>BluRay)| - (?<webdl>WEB-DL|WEBDL|WEB\sDL|WEB\-DL|WebRip)| + (?<webdl>WEB[-_. ]DL|WEBDL|WebRip)| (?<hdtv>HDTV)| (?<bdrip>BDRiP)| (?<brrip>BRRip)| (?<dvd>DVD|DVDRip|NTSC|PAL|xvidvd)| - (?<dsr>WS\sDSR|WS_DSR|WS\.DSR|DSR)| + (?<dsr>WS[-_. ]DSR|DSR)| (?<pdtv>PDTV)| (?<sdtv>SDTV) )\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>TrollHD|RawHD)\b", + private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>TrollHD|RawHD|1080i[-_. ]HDTV)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ProperRegex = new Regex(@"\b(?<proper>proper|repack)\b", @@ -37,6 +37,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>Xvid)|(?<divx>divx))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex OtherSourceRegex = new Regex(@"(?<hdtv>HD[-_. ]TV)|(?<sdtv>SD[-_. ]TV)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static QualityModel ParseQuality(string name) { Logger.Debug("Trying to parse quality for {0}", name); @@ -191,6 +193,13 @@ namespace NzbDrone.Core.Parser result.Quality = Quality.Bluray1080p; } + var otherSourceMatch = OtherSourceMatch(normalizedName); + + if (otherSourceMatch != Quality.Unknown) + { + result.Quality = otherSourceMatch; + } + //Based on extension if (result.Quality == Quality.Unknown && !name.ContainsInvalidPathChars()) { @@ -220,6 +229,17 @@ namespace NzbDrone.Core.Parser return Resolution.Unknown; } + + private static Quality OtherSourceMatch(string name) + { + var match = OtherSourceRegex.Match(name); + + if (!match.Success) return Quality.Unknown; + if (match.Groups["sdtv"].Success) return Quality.SDTV; + if (match.Groups["hdtv"].Success) return Quality.HDTV720p; + + return Quality.Unknown; + } } public enum Resolution diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 733ff2301..c8cde78d4 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -2,6 +2,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Queue { @@ -13,7 +14,8 @@ namespace NzbDrone.Core.Queue public Decimal Size { get; set; } public String Title { get; set; } public Decimal Sizeleft { get; set; } - public TimeSpan Timeleft { get; set; } + public TimeSpan? Timeleft { get; set; } public String Status { get; set; } + public RemoteEpisode RemoteEpisode { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index e195f57e4..ead4f083c 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using NLog; using NzbDrone.Core.Download; @@ -12,39 +13,26 @@ namespace NzbDrone.Core.Queue public class QueueService : IQueueService { - private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IDownloadTrackingService _downloadTrackingService; private readonly Logger _logger; - public QueueService(IProvideDownloadClient downloadClientProvider, Logger logger) + public QueueService(IDownloadTrackingService downloadTrackingService, Logger logger) { - _downloadClientProvider = downloadClientProvider; + _downloadTrackingService = downloadTrackingService; _logger = logger; } public List<Queue> GetQueue() { - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var queueItems = _downloadTrackingService.GetQueuedDownloads() + .Select(v => v.DownloadItem) + .OrderBy(v => v.RemainingTime) + .ToList(); - if (downloadClient == null) - { - _logger.Debug("Download client is not configured."); - return new List<Queue>(); - } - - try - { - var queueItems = downloadClient.GetQueue(); - - return MapQueue(queueItems); - } - catch (Exception ex) - { - _logger.Error("Error getting queue from download client: " + downloadClient.ToString(), ex); - return new List<Queue>(); - } + return MapQueue(queueItems); } - private List<Queue> MapQueue(IEnumerable<QueueItem> queueItems) + private List<Queue> MapQueue(IEnumerable<DownloadClientItem> queueItems) { var queued = new List<Queue>(); @@ -53,15 +41,16 @@ namespace NzbDrone.Core.Queue foreach (var episode in queueItem.RemoteEpisode.Episodes) { var queue = new Queue(); - queue.Id = queueItem.Id.GetHashCode() + episode.Id; + queue.Id = queueItem.DownloadClientId.GetHashCode() + episode.Id; queue.Series = queueItem.RemoteEpisode.Series; queue.Episode = episode; queue.Quality = queueItem.RemoteEpisode.ParsedEpisodeInfo.Quality; queue.Title = queueItem.Title; - queue.Size = queueItem.Size; - queue.Sizeleft = queueItem.Sizeleft; - queue.Timeleft = queueItem.Timeleft; - queue.Status = queueItem.Status; + queue.Size = queueItem.TotalSize; + queue.Sizeleft = queueItem.RemainingSize; + queue.Timeleft = queueItem.RemainingTime; + queue.Status = queueItem.Status.ToString(); + queue.RemoteEpisode = queueItem.RemoteEpisode; queued.Add(queue); } } diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs index 54e8315a6..b70b1e06a 100644 --- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace NzbDrone.Core.ThingiProvider { @@ -12,6 +13,7 @@ namespace NzbDrone.Core.ThingiProvider TProviderDefinition Create(TProviderDefinition indexer); void Update(TProviderDefinition indexer); void Delete(int id); - List<TProviderDefinition> Templates(); + IEnumerable<TProviderDefinition> GetDefaultDefinitions(); + IEnumerable<TProviderDefinition> GetPresetDefinitions(TProviderDefinition providerDefinition); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index bdce9edd9..fa9cd32c2 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -38,14 +38,41 @@ namespace NzbDrone.Core.ThingiProvider return _providerRepository.All().ToList(); } - public List<TProviderDefinition> Templates() + public IEnumerable<TProviderDefinition> GetDefaultDefinitions() { - return _providers.Select(p => new TProviderDefinition() + foreach (var provider in _providers) { - ConfigContract = p.ConfigContract.Name, - Implementation = p.GetType().Name, - Settings = (IProviderConfig)Activator.CreateInstance(p.ConfigContract) - }).ToList(); + var definition = provider.DefaultDefinitions + .OfType<TProviderDefinition>() + .FirstOrDefault(v => v.Name == null || v.Name == provider.GetType().Name); + + if (definition == null) + { + definition = new TProviderDefinition() + { + Name = string.Empty, + ConfigContract = provider.ConfigContract.Name, + Implementation = provider.GetType().Name, + Settings = (IProviderConfig)Activator.CreateInstance(provider.ConfigContract) + }; + } + + definition = GetProviderCharacteristics(provider, definition); + + yield return definition; + } + } + + public IEnumerable<TProviderDefinition> GetPresetDefinitions(TProviderDefinition providerDefinition) + { + var provider = _providers.First(v => v.GetType().Name == providerDefinition.Implementation); + + var definitions = provider.DefaultDefinitions + .OfType<TProviderDefinition>() + .Where(v => v.Name != null && v.Name != provider.GetType().Name) + .ToList(); + + return definitions; } public List<TProvider> GetAvailableProviders() @@ -105,6 +132,11 @@ namespace NzbDrone.Core.ThingiProvider return All().Where(c => c.Settings.Validate().IsValid).ToList(); } + protected virtual TProviderDefinition GetProviderCharacteristics(TProvider provider, TProviderDefinition definition) + { + return definition; + } + private void RemoveMissingImplementations() { var storedProvider = _providerRepository.All(); diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 404ac8f12..0e89f3116 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -60,6 +60,8 @@ namespace NzbDrone.Core.Update { try { + EnsureAppDataSafety(); + var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder(); var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName); @@ -128,7 +130,7 @@ namespace NzbDrone.Core.Update _diskProvider.DeleteFolder(_appFolderInfo.GetUpdateClientFolder(), true); _logger.ProgressInfo("Starting update script: {0}", _configFileProvider.UpdateScriptPath); - _processProvider.Start(scriptPath, GetUpdaterArgs(updateSandboxFolder.WrapInQuotes())); + _processProvider.Start(scriptPath, GetUpdaterArgs(updateSandboxFolder)); } private string GetUpdaterArgs(string updateSandboxFolder) @@ -136,7 +138,16 @@ namespace NzbDrone.Core.Update var processId = _processProvider.GetCurrentProcess().Id.ToString(); var executingApplication = _runtimeInfo.ExecutingApplication; - return String.Join(" ", processId, updateSandboxFolder.WrapInQuotes(), executingApplication.WrapInQuotes()); + return String.Join(" ", processId, updateSandboxFolder.TrimEnd(Path.DirectorySeparatorChar).WrapInQuotes(), executingApplication.WrapInQuotes()); + } + + private void EnsureAppDataSafety() + { + if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || + _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) + { + throw new NotSupportedException("Update will cause AppData to be deleted, correct you configuration before proceeding"); + } } public void Execute(ApplicationUpdateCommand message) diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs new file mode 100644 index 000000000..c050b9e82 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs @@ -0,0 +1,25 @@ +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class SeriesAncestorValidator : PropertyValidator + { + private readonly ISeriesService _seriesService; + + public SeriesAncestorValidator(ISeriesService seriesService) + : base("Path is an ancestor of an existing path") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return !_seriesService.GetAllSeries().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config index 36fbe4d0a..f657add80 100644 --- a/src/NzbDrone.Core/packages.config +++ b/src/NzbDrone.Core/packages.config @@ -7,6 +7,6 @@ <package id="Newtonsoft.Json" version="5.0.8" targetFramework="net40" /> <package id="NLog" version="2.1.0" targetFramework="net40" /> <package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" /> - <package id="RestSharp" version="104.3.3" targetFramework="net40" /> + <package id="RestSharp" version="104.4.0" targetFramework="net40" /> <package id="valueinjecter" version="2.3.3" targetFramework="net40" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index fffac3b0a..c51816d43 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -68,6 +68,17 @@ namespace NzbDrone.Integration.Test _runner.Start(); InitRestClients(); + + // Add Wombles + var wombles = Indexers.Post(new Api.Indexers.IndexerResource + { + Enable = true, + ConfigContract = "NullConfig", + Implementation = "Wombles", + Name = "Wombles", + Protocol = Core.Indexers.DownloadProtocol.Usenet, + Fields = new List<Api.ClientSchema.Field>() + }); } private void InitRestClients() diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 3f04a0cc5..837beaf72 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -44,6 +44,10 @@ <Reference Include="nunit.framework"> <HintPath>..\packages\NUnit.2.6.2\lib\nunit.framework.dll</HintPath> </Reference> + <Reference Include="RestSharp, Version=104.4.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> @@ -84,9 +88,6 @@ <Reference Include="Owin"> <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> </Reference> - <Reference Include="RestSharp"> - <HintPath>..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="Client\ClientBase.cs" /> diff --git a/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs b/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs index a97bdf3b8..a4e1e4d40 100644 --- a/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs @@ -10,17 +10,13 @@ namespace NzbDrone.Integration.Test [Test] public void should_only_have_unknown_series_releases() { - var releases = Releases.All(); var indexers = Indexers.All(); - releases.Should().OnlyContain(c => c.Rejections.Contains("Unknown Series")); releases.Should().OnlyContain(c => BeValidRelease(c)); } - - private bool BeValidRelease(ReleaseResource releaseResource) { releaseResource.Age.Should().BeGreaterOrEqualTo(-1); @@ -33,6 +29,5 @@ namespace NzbDrone.Integration.Test return true; } - } } \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/packages.config b/src/NzbDrone.Integration.Test/packages.config index 0013824cf..39bda9dde 100644 --- a/src/NzbDrone.Integration.Test/packages.config +++ b/src/NzbDrone.Integration.Test/packages.config @@ -13,5 +13,5 @@ <package id="NLog" version="2.1.0" targetFramework="net40" /> <package id="NUnit" version="2.6.2" targetFramework="net40" /> <package id="Owin" version="1.0" targetFramework="net40" /> - <package id="RestSharp" version="104.3.3" targetFramework="net40" /> + <package id="RestSharp" version="104.4.0" targetFramework="net40" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 437a9bde4..d9eebe47a 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -23,8 +23,8 @@ namespace NzbDrone.Test.Common.AutoMoq { public readonly MockBehavior DefaultBehavior = MockBehavior.Default; public Type ResolveType; - private IUnityContainer container; - private IDictionary<Type, object> registeredMocks; + private IUnityContainer _container; + private IDictionary<Type, object> _registeredMocks; public AutoMoqer() { @@ -46,7 +46,7 @@ namespace NzbDrone.Test.Common.AutoMoq public virtual T Resolve<T>() { ResolveType = typeof(T); - var result = container.Resolve<T>(); + var result = _container.Resolve<T>(); SetConstant(result); ResolveType = null; return result; @@ -78,13 +78,13 @@ namespace NzbDrone.Test.Common.AutoMoq public virtual void SetMock(Type type, Mock mock) { - if (registeredMocks.ContainsKey(type) == false) - registeredMocks.Add(type, mock); + if (_registeredMocks.ContainsKey(type) == false) + _registeredMocks.Add(type, mock); } public virtual void SetConstant<T>(T instance) { - container.RegisterInstance(instance); + _container.RegisterInstance(instance); SetMock(instance.GetType(), null); } @@ -120,7 +120,7 @@ namespace NzbDrone.Test.Common.AutoMoq public void VerifyAllMocks() { - foreach (var registeredMock in registeredMocks) + foreach (var registeredMock in _registeredMocks) { var mock = registeredMock.Value as Mock; if (mock != null) @@ -132,12 +132,12 @@ namespace NzbDrone.Test.Common.AutoMoq private void SetupAutoMoqer(IUnityContainer container) { - this.container = container; + _container = container; container.RegisterInstance(this); RegisterPlatformLibrary(container); - registeredMocks = new Dictionary<Type, object>(); + _registeredMocks = new Dictionary<Type, object>(); AddTheAutoMockingContainerExtensionToTheContainer(container); } @@ -149,19 +149,19 @@ namespace NzbDrone.Test.Common.AutoMoq private Mock<T> TheRegisteredMockForThisType<T>(Type type) where T : class { - return (Mock<T>)registeredMocks.Where(x => x.Key == type).First().Value; + return (Mock<T>)_registeredMocks.Where(x => x.Key == type).First().Value; } private void CreateANewMockAndRegisterIt<T>(Type type, MockBehavior behavior) where T : class { var mock = new Mock<T>(behavior); - container.RegisterInstance(mock.Object); + _container.RegisterInstance(mock.Object); SetMock(type, mock); } private bool GetMockHasNotBeenCalledForThisType(Type type) { - return registeredMocks.ContainsKey(type) == false; + return _registeredMocks.ContainsKey(type) == false; } private static Type GetTheMockType<T>() where T : class diff --git a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj index e871e8478..017ace6e1 100644 --- a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj @@ -43,6 +43,10 @@ <Reference Include="Moq"> <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> </Reference> + <Reference Include="RestSharp, Version=104.4.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> @@ -68,9 +72,6 @@ <Reference Include="nunit.framework"> <HintPath>..\packages\NUnit.2.6.2\lib\nunit.framework.dll</HintPath> </Reference> - <Reference Include="RestSharp"> - <HintPath>..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="AutoMoq\AutoMoqer.cs" /> diff --git a/src/NzbDrone.Test.Common/packages.config b/src/NzbDrone.Test.Common/packages.config index 28a2e5f1d..5cea9507c 100644 --- a/src/NzbDrone.Test.Common/packages.config +++ b/src/NzbDrone.Test.Common/packages.config @@ -6,6 +6,6 @@ <package id="Newtonsoft.Json" version="5.0.8" targetFramework="net40" /> <package id="NLog" version="2.1.0" targetFramework="net40" /> <package id="NUnit" version="2.6.2" targetFramework="net40" /> - <package id="RestSharp" version="104.3.3" targetFramework="net40" /> + <package id="RestSharp" version="104.4.0" targetFramework="net40" /> <package id="Unity" version="2.1.505.2" targetFramework="net40" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 3691f6fce..0631f9592 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -53,19 +53,7 @@ namespace NzbDrone.Update public void Start(string[] args) { var startupContext = ParseArgs(args); - string targetFolder; - - if (startupContext.ExecutingApplication.IsNullOrWhiteSpace()) - { - var exeFileInfo = new FileInfo(_processProvider.GetProcessById(startupContext.ProcessId).StartPath); - targetFolder = exeFileInfo.Directory.FullName; - } - - else - { - var exeFileInfo = new FileInfo(startupContext.ExecutingApplication); - targetFolder = exeFileInfo.Directory.FullName; - } + var targetFolder = GetInstallationDirectory(startupContext); logger.Info("Starting update process. Target Path:{0}", targetFolder); _installUpdateService.Start(targetFolder, startupContext.ProcessId); @@ -83,29 +71,32 @@ namespace NzbDrone.Update ProcessId = ParseProcessId(args[0]) }; - if (args.Count() == 1) + if (OsInfo.IsMono) { - return startupContext; - } - - else if (args.Count() == 3) - { - startupContext.UpdateLocation = args[1]; - startupContext.ExecutingApplication = args[2]; - } - - else - { - logger.Debug("Arguments:"); - - foreach (var arg in args) + if (args.Count() == 1) { - logger.Debug(" {0}", arg); + return startupContext; } - var message = String.Format("Number of arguments are unexpected, expected: 3, found: {0}", args.Count()); + else if (args.Count() == 3) + { + startupContext.UpdateLocation = args[1]; + startupContext.ExecutingApplication = args[2]; + } - throw new ArgumentOutOfRangeException("args", message); + else + { + logger.Debug("Arguments:"); + + foreach (var arg in args) + { + logger.Debug(" {0}", arg); + } + + var message = String.Format("Number of arguments are unexpected, expected: 3, found: {0}", args.Count()); + + throw new ArgumentOutOfRangeException("args", message); + } } return startupContext; @@ -122,5 +113,26 @@ namespace NzbDrone.Update logger.Debug("NzbDrone process ID: {0}", id); return id; } + + private string GetInstallationDirectory(UpdateStartupContext startupContext) + { + if (startupContext.ExecutingApplication.IsNullOrWhiteSpace()) + { + logger.Debug("Using process ID to find installation directory: {0}", startupContext.ProcessId); + var exeFileInfo = new FileInfo(_processProvider.GetProcessById(startupContext.ProcessId).StartPath); + logger.Debug("Executable location: {0}", exeFileInfo.FullName); + + return exeFileInfo.DirectoryName; + } + + else + { + logger.Debug("Using executing application: {0}", startupContext.ExecutingApplication); + var exeFileInfo = new FileInfo(startupContext.ExecutingApplication); + logger.Debug("Executable location: {0}", exeFileInfo.FullName); + + return exeFileInfo.DirectoryName; + } + } } } diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index 43a879bdb..b6ba578db 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -71,9 +71,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" - ProjectSection(SolutionItems) = preProject - NzbDrone.Common.Test\ServiceFactoryFixture.cs = NzbDrone.Common.Test\ServiceFactoryFixture.cs - EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" EndProject diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html index 2e16ae6e3..1d0822902 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html +++ b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html @@ -8,14 +8,16 @@ <div class="validation-errors"></div> <div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> - <div class="input-group x-path form-group"> - <span class="input-group-addon"> <i class="icon-folder-open"></i></span> - <input class="col-md-9 form-control" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> - <span class="input-group-btn "> - <button class="btn btn-success x-add"> - <i class="icon-ok"/> - </button> - </span> + <div class="form-group"> + <div class="input-group x-path"> + <span class="input-group-addon"> <i class="icon-folder-open"></i></span> + <input class="col-md-9 form-control" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> + <span class="input-group-btn "> + <button class="btn btn-success x-add"> + <i class="icon-ok"/> + </button> + </span> + </div> </div> {{#if items}} diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index ee287bbac..01a3b19fd 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -44,6 +44,11 @@ margin-left: 15px; vertical-align: middle; } + + .year { + font-style : italic; + color : #aaaaaa; + } } .new-series-overview { @@ -130,4 +135,8 @@ li.add-new:hover { overflow: auto; max-height: 300px; } + + .validation-errors { + display: none; + } } diff --git a/src/UI/Cells/EpisodeActionsCellTemplate.html b/src/UI/Cells/EpisodeActionsCellTemplate.html index 77e0ef5cd..a30cf3d5f 100644 --- a/src/UI/Cells/EpisodeActionsCellTemplate.html +++ b/src/UI/Cells/EpisodeActionsCellTemplate.html @@ -1,5 +1,5 @@ <div class="btn-group hidden-xs"> - <button class="btn btn-xs x-automatic-search x-automatic-search-icon" title="Automatic Search"><i class="icon-search"></i></button> + <button class="btn btn-xs x-automatic-search x-automatic-search-icon" title="Automatic Search" data-container="body"><i class="icon-search"></i></button> <button class="btn btn-xs dropdown-toggle" data-toggle="dropdown"> <span class="caret"></span> </button> diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index 167ac8671..c8cd57510 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -50,15 +50,15 @@ define( var title = 'Episode downloaded'; if (quality.proper) { - title += ' [PROPER] - {0}'.format(size); - this.$el.html('<span class="badge badge-info" title="{0}">{1}</span>'.format(title, quality.quality.name)); + title += ' [PROPER]'; } - else { + if (size !== '') { title += ' - {0}'.format(size); - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); } + this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); + return; } diff --git a/src/UI/Cells/SeriesActionsCell.js b/src/UI/Cells/SeriesActionsCell.js index 313f8ef29..066fea0fa 100644 --- a/src/UI/Cells/SeriesActionsCell.js +++ b/src/UI/Cells/SeriesActionsCell.js @@ -3,25 +3,38 @@ define( [ 'vent', - 'Cells/NzbDroneCell' - ], function (vent, NzbDroneCell) { + 'Cells/NzbDroneCell', + 'Commands/CommandController' + ], function (vent, NzbDroneCell, CommandController) { return NzbDroneCell.extend({ className: 'series-actions-cell', + ui: { + refresh: '.x-refresh' + }, + events: { - 'click .x-edit-series' : '_editSeries', - 'click .x-remove-series': '_removeSeries' + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' }, render: function () { this.$el.empty(); this.$el.html( - '<i class="icon-nd-edit x-edit-series" title="" data-original-title="Edit Series"></i> ' + - '<i class="icon-remove x-remove-series hidden-xs" title="" data-original-title="Delete Series"></i>' + '<i class="icon-refresh x-refresh hidden-xs" title="" data-original-title="Update series info and scan disk"></i> ' + + '<i class="icon-nd-edit x-edit" title="" data-original-title="Edit Series"></i>' ); + CommandController.bindToCommand({ + element: this.$el.find('.x-refresh'), + command: { + name : 'refreshSeries', + seriesId : this.model.get('id') + } + }); + this.delegateEvents(); return this; }, @@ -30,8 +43,11 @@ define( vent.trigger(vent.Commands.EditSeriesCommand, {series:this.model}); }, - _removeSeries: function () { - vent.trigger(vent.Commands.DeleteSeriesCommand, {series:this.model}); + _refreshSeries: function () { + CommandController.Execute('refreshSeries', { + name : 'refreshSeries', + seriesId: this.model.id + }); } }); }); diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 4105f32a1..42442e6af 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -137,6 +137,7 @@ td.episode-status-cell, td.quality-cell { .timeleft-cell { cursor : default; width : 80px; + text-align: center; } .queue-status-cell { diff --git a/src/UI/Episode/Search/EpisodeSearchLayout.js b/src/UI/Episode/Search/EpisodeSearchLayout.js index 20767ebd1..a5d400625 100644 --- a/src/UI/Episode/Search/EpisodeSearchLayout.js +++ b/src/UI/Episode/Search/EpisodeSearchLayout.js @@ -8,8 +8,9 @@ define( 'Release/ReleaseCollection', 'Series/SeriesCollection', 'Commands/CommandController', - 'Shared/LoadingView' - ], function (vent, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection,CommandController, LoadingView) { + 'Shared/LoadingView', + 'Episode/Search/NoResultsView' + ], function (vent, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection,CommandController, LoadingView, NoResultsView) { return Marionette.Layout.extend({ template: 'Episode/Search/EpisodeSearchLayoutTemplate', @@ -73,7 +74,14 @@ define( }, _showSearchResults: function () { - this.mainView = new ManualSearchLayout({ collection: this.releaseCollection }); + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + this.mainView = new ManualSearchLayout({ collection: this.releaseCollection }); + } + this._showMainView(); } }); diff --git a/src/UI/Episode/Search/NoResultsView.js b/src/UI/Episode/Search/NoResultsView.js new file mode 100644 index 000000000..54d868438 --- /dev/null +++ b/src/UI/Episode/Search/NoResultsView.js @@ -0,0 +1,10 @@ +'use strict'; + +define( + [ + 'marionette' + ], function (Marionette) { + return Marionette.ItemView.extend({ + template: 'Episode/Search/NoResultsViewTemplate' + }); + }); diff --git a/src/UI/Episode/Search/NoResultsViewTemplate.html b/src/UI/Episode/Search/NoResultsViewTemplate.html new file mode 100644 index 000000000..87201af05 --- /dev/null +++ b/src/UI/Episode/Search/NoResultsViewTemplate.html @@ -0,0 +1 @@ +<div>No results found</div> \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 9618ccf96..7d1276c22 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -69,6 +69,6 @@ define( return this.title; } - return new Handlebars.SafeString('{0} <em>({1})</em>'.format(this.title, this.year)); + return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year)); }); }); diff --git a/src/UI/History/Queue/QueueStatusCell.js b/src/UI/History/Queue/QueueStatusCell.js index 2c01a0246..6a8903deb 100644 --- a/src/UI/History/Queue/QueueStatusCell.js +++ b/src/UI/History/Queue/QueueStatusCell.js @@ -26,6 +26,11 @@ define( title = 'Queued'; } + if (status === 'completed') { + icon = 'icon-inbox'; + title = 'Downloaded'; + } + this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title)); } diff --git a/src/UI/History/Queue/TimeleftCell.js b/src/UI/History/Queue/TimeleftCell.js index 9ead67f63..a4d6e4544 100644 --- a/src/UI/History/Queue/TimeleftCell.js +++ b/src/UI/History/Queue/TimeleftCell.js @@ -2,8 +2,9 @@ define( [ - 'Cells/NzbDroneCell' - ], function (NzbDroneCell) { + 'Cells/NzbDroneCell', + 'filesize' + ], function (NzbDroneCell, fileSize) { return NzbDroneCell.extend({ className: 'timeleft-cell', @@ -14,11 +15,16 @@ define( if (this.cellValue) { var timeleft = this.cellValue.get('timeleft'); - var size = this.cellValue.get('size'); - var sizeleft = this.cellValue.get('sizeleft'); + var totalSize = fileSize(this.cellValue.get('size'), 1, false); + var remainingSize = fileSize(this.cellValue.get('sizeleft'), 1, false); - this.$el.html(timeleft); - this.$el.attr('title', '{0} MB / {1} MB'.format(sizeleft, size)); + if (timeleft === undefined) { + this.$el.html("-"); + } + else { + this.$el.html(timeleft); + } + this.$el.attr('title', '{0} / {1}'.format(remainingSize, totalSize)); } return this; diff --git a/src/UI/Mixins/AsModelBoundView.js b/src/UI/Mixins/AsModelBoundView.js index be70ec65b..0095370e0 100644 --- a/src/UI/Mixins/AsModelBoundView.js +++ b/src/UI/Mixins/AsModelBoundView.js @@ -30,7 +30,7 @@ define( } }; - this.prototype.beforeClose = function () { + this.prototype.onBeforeClose = function () { if (this._modelBinder) { this._modelBinder.unbind(); diff --git a/src/UI/Mixins/AsValidatedView.js b/src/UI/Mixins/AsValidatedView.js index 952c1da69..203efa06b 100644 --- a/src/UI/Mixins/AsValidatedView.js +++ b/src/UI/Mixins/AsValidatedView.js @@ -12,19 +12,18 @@ define( var originalBeforeClose = this.prototype.onBeforeClose; var errorHandler = function (response) { + if (this.model) { + this.model.trigger('validation:failed', response); + } - if (response.status === 400) { - - var view = this; - var validationErrors = JSON.parse(response.responseText); - _.each(validationErrors, function (error) { - view.$el.processServerError(error); - }); + else { + this.trigger('validation:failed', response); } }; - var validatedSync = function (method, model,options) { - this.$el.removeAllErrors(); + var validatedSync = function (method, model, options) { + model.trigger('validation:sync'); + arguments[2].isValidatedCall = true; return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this)); }; @@ -37,8 +36,35 @@ define( } }; + var validationFailed = function (response) { + if (response.status === 400) { + + var view = this; + var validationErrors = JSON.parse(response.responseText); + _.each(validationErrors, function (error) { + view.$el.processServerError(error); + }); + } + }; + this.prototype.onRender = function () { + if (this.model) { + this.listenTo(this.model, 'validation:sync', function () { + this.$el.removeAllErrors(); + }); + + this.listenTo(this.model, 'validation:failed', validationFailed); + } + + else { + this.listenTo(this, 'validation:sync', function () { + this.$el.removeAllErrors(); + }); + + this.listenTo(this, 'validation:failed', validationFailed); + } + Validation.bind(this); this.bindToModelValidation = bindToModel.bind(this); @@ -55,6 +81,10 @@ define( if (this.model) { Validation.unbind(this); + + //If we don't do this the next time the model is used the sync is bound to an old view + this.model.sync = this.model._originalSync; + this.model._originalSync = undefined; } if (originalBeforeClose) { diff --git a/src/UI/Rename/RenamePreviewItemViewTemplate.html b/src/UI/Rename/RenamePreviewItemViewTemplate.html index fb161d465..f0fe50702 100644 --- a/src/UI/Rename/RenamePreviewItemViewTemplate.html +++ b/src/UI/Rename/RenamePreviewItemViewTemplate.html @@ -8,12 +8,12 @@ </div> </label> </div> - <div class="col-md-9"> + <div class="col-md-11"> <div class="row"> - <div class="col-md-9 file-path"><i class="icon-nd-existing" title="Existing path" /> {{existingPath}}</div> + <div class="col-md-12 file-path"><i class="icon-nd-existing" title="Existing path" /> {{existingPath}}</div> </div> <div class="row"> - <div class="col-md-9 file-path"><i class="icon-nd-suggested" title="Suggested path" /> {{newPath}}</div> + <div class="col-md-12 file-path"><i class="icon-nd-suggested" title="Suggested path" /> {{newPath}}</div> </div> </div> </div> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js index 1c4970a65..729723ab1 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js @@ -3,26 +3,10 @@ define( [ 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ + 'marionette', + 'Series/Index/SeriesIndexItemView' + ], function (vent, Marionette, SeriesIndexItemView) { + return SeriesIndexItemView.extend({ template: 'Series/Index/Overview/SeriesOverviewItemViewTemplate', - - ui: { - 'progressbar': '.progress .bar' - }, - - events: { - 'click .x-edit' : 'editSeries', - 'click .x-remove': 'removeSeries' - }, - - editSeries: function () { - vent.trigger(vent.Commands.EditSeriesCommand, {series: this.model}); - }, - - removeSeries: function () { - vent.trigger(vent.Commands.DeleteSeriesCommand, {series: this.model}); - } }); }); diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html index a43ee6b6e..3b9ddf3c1 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html @@ -14,8 +14,8 @@ </div> <div class="col-md-2 col-xs-2"> <div class="pull-right series-overview-list-actions"> + <i class="icon-refresh x-refresh" title="Update series info and scan disk"/> <i class="icon-nd-edit x-edit" title="Edit Series"/> - <i class="icon-remove x-remove" title="Delete Series"/> </div> </div> </div> diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemView.js b/src/UI/Series/Index/Posters/SeriesPostersItemView.js index 997c1c9e1..e72cb6a4b 100644 --- a/src/UI/Series/Index/Posters/SeriesPostersItemView.js +++ b/src/UI/Series/Index/Posters/SeriesPostersItemView.js @@ -3,37 +3,25 @@ define( [ 'vent', - 'marionette' - ], function (vent, Marionette) { + 'marionette', + 'Series/Index/SeriesIndexItemView' + ], function (vent, Marionette, SeriesIndexItemView) { - return Marionette.ItemView.extend({ + return SeriesIndexItemView.extend({ tagName : 'li', template: 'Series/Index/Posters/SeriesPostersItemViewTemplate', + initialize: function () { + this.events['mouseenter .x-series-poster'] = 'posterHoverAction'; + this.events['mouseleave .x-series-poster'] = 'posterHoverAction'; - ui: { - 'progressbar': '.progress .bar', - 'controls' : '.series-controls' - }, - - events: { - 'click .x-edit' : 'editSeries', - 'click .x-remove' : 'removeSeries', - 'mouseenter .x-series-poster': 'posterHoverAction', - 'mouseleave .x-series-poster': 'posterHoverAction' - }, - - - editSeries: function () { - vent.trigger(vent.Commands.EditSeriesCommand, {series:this.model}); - }, - - removeSeries: function () { - vent.trigger(vent.Commands.DeleteSeriesCommand, {series:this.model}); + this.ui.controls = '.x-series-controls'; + this.ui.title = '.x-title'; }, posterHoverAction: function () { this.ui.controls.slideToggle(); + this.ui.title.slideToggle(); } }); }); diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html index bf3de36b8..f1b82fc52 100644 --- a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html +++ b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html @@ -1,9 +1,9 @@ <div class="series-posters-item"> <div class="center"> <div class="series-poster-container x-series-poster"> - <div class="series-controls"> + <div class="series-controls x-series-controls"> + <i class="icon-refresh x-refresh" title="Refresh Series"/> <i class="icon-nd-edit x-edit" title="Edit Series"/> - <i class="icon-remove x-remove" title="Delete Series"/> </div> {{#unless_eq status compare="continuing"}} <div class="ended-banner">Ended</div> @@ -12,6 +12,9 @@ <img class="series-poster" src="{{poster}}" {{defaultImg}}> <div class="center title">{{title}}</div> </a> + <div class="hidden-title x-title"> + {{title}} + </div> </div> </div> diff --git a/src/UI/Series/Index/SeriesIndexItemView.js b/src/UI/Series/Index/SeriesIndexItemView.js new file mode 100644 index 000000000..9513ac588 --- /dev/null +++ b/src/UI/Series/Index/SeriesIndexItemView.js @@ -0,0 +1,41 @@ +'use strict'; + +define( + [ + 'vent', + 'marionette', + 'Commands/CommandController' + ], function (vent, Marionette, CommandController) { + return Marionette.ItemView.extend({ + + ui: { + refresh : '.x-refresh' + }, + + events: { + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' + }, + + onRender: function () { + CommandController.bindToCommand({ + element: this.ui.refresh, + command: { + name : 'refreshSeries', + seriesId : this.model.get('id') + } + }); + }, + + _editSeries: function () { + vent.trigger(vent.Commands.EditSeriesCommand, {series: this.model}); + }, + + _refreshSeries: function () { + CommandController.Execute('refreshSeries', { + name : 'refreshSeries', + seriesId: this.model.id + }); + } + }); + }); diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index e6b32703c..1420fe73e 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -188,7 +188,22 @@ background-color : #eeeeee; width : 100%; text-align : right; - padding-right : 20px; + padding-right : 10px; + .opacity(0.8); + display : none; + + i { + .clickable(); + } + } + + .hidden-title { + position : absolute;; + bottom : 0px; + overflow : hidden; + background-color : #eeeeee; + width : 100%; + text-align : center; .opacity(0.8); display : none; } @@ -279,7 +294,7 @@ text-transform : none; i { - .clickable; + .clickable(); font-size : 24px; padding-left : 5px; } @@ -340,6 +355,10 @@ .series-overview-list-actions { min-width: 56px; max-width: 56px; + + i { + .clickable(); + } } //Editor diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js index e6f557dc1..48fd1cbee 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js @@ -1,23 +1,14 @@ 'use strict'; define([ - 'marionette', + 'Settings/ThingyAddCollectionView', + 'Settings/ThingyHeaderGroupView', 'Settings/DownloadClient/Add/DownloadClientAddItemView' -], function (Marionette, AddItemView) { +], function (ThingyAddCollectionView, ThingyHeaderGroupView, AddItemView) { - return Marionette.CompositeView.extend({ - itemView : AddItemView, + return ThingyAddCollectionView.extend({ + itemView : ThingyHeaderGroupView.extend({ itemView: AddItemView }), itemViewContainer: '.add-download-client .items', - template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate', - - itemViewOptions: function () { - return { - downloadClientCollection: this.downloadClientCollection - }; - }, - - initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; - } + template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate' }); }); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js index beab52273..aafbc33f3 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js @@ -1,35 +1,55 @@ 'use strict'; define([ + 'underscore', + 'jquery', 'AppLayout', 'marionette', 'Settings/DownloadClient/Edit/DownloadClientEditView' -], function (AppLayout, Marionette, EditView) { +], function (_, $, AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', - tagName : 'li', + template : 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', events: { - 'click': '_add' + 'click .x-preset': '_addPreset', + 'click' : '_add' }, initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; + this.targetCollection = options.targetCollection; + }, + + _addPreset: function (e) { + + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + + var presetData = _.where(this.model.get('presets'), {name: presetName})[0]; + + this.model.set(presetData); + + this.model.set({ + id : undefined, + enable : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); }, _add: function (e) { - if (this.$(e.target).hasClass('icon-info-sign')) { + if ($(e.target).closest('.btn,.btn-group').length !== 0) { return; } this.model.set({ id : undefined, - name : this.model.get('implementationName'), enable : true }); - var editView = new EditView({ model: this.model, downloadClientCollection: this.downloadClientCollection }); + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); AppLayout.modalRegion.show(editView); } }); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html index f892a4d01..a2c0295be 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html @@ -1,6 +1,27 @@ <div class="add-thingy"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} + <div> + {{implementation}} + </div> + <div class="pull-right"> + {{#if_gt presets.length compare=0}} + <div class="btn-group"> + <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> + Presets + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {{#each presets}} + <li class="x-preset" data-id="{{name}}"> + <a>{{name}}</a> + </li> + {{/each}} + </ul> + </div> + {{/if_gt}} + {{#if infoLink}} + <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> + <i class="icon-info-sign"/> + </a> + {{/if}} + </div> </div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js b/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js new file mode 100644 index 000000000..f37861d72 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js @@ -0,0 +1,36 @@ +'use strict'; + +define([ + 'underscore', + 'AppLayout', + 'backbone', + 'Settings/DownloadClient/DownloadClientCollection', + 'Settings/DownloadClient/Add/DownloadClientAddCollectionView' +], function (_, AppLayout, Backbone, SchemaCollection, AddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var groupedSchemaCollection = new Backbone.Collection(); + + schemaCollection.on('sync', function() { + + var groups = schemaCollection.groupBy(function(model, iterator) { return model.get('protocol'); }); + + var modelCollection = _.map(groups, function(values, key, list) { + return { 'header': key, collection: values }; + }); + + groupedSchemaCollection.reset(modelCollection); + }); + + var view = new AddCollectionView({ collection: groupedSchemaCollection, targetCollection: collection }); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/Add/SchemaModal.js b/src/UI/Settings/DownloadClient/Add/SchemaModal.js deleted file mode 100644 index dac0dca63..000000000 --- a/src/UI/Settings/DownloadClient/Add/SchemaModal.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; -define([ - 'AppLayout', - 'Settings/DownloadClient/DownloadClientCollection', - 'Settings/DownloadClient/Add/DownloadClientAddCollectionView' -], function (AppLayout, DownloadClientCollection, DownloadClientAddCollectionView) { - return ({ - - open: function (collection) { - var schemaCollection = new DownloadClientCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - - var view = new DownloadClientAddCollectionView({ collection: schemaCollection, downloadClientCollection: collection}); - AppLayout.modalRegion.show(view); - } - }); -}); diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js index 502d57e7f..2c8f951db 100644 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js +++ b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js @@ -1,23 +1,23 @@ 'use strict'; -define( - [ - 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', - events: { - 'click .x-confirm-delete': '_delete' - }, +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', - _delete: function () { - this.model.destroy({ - wait : true, - success: function () { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } - }); + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollection.js b/src/UI/Settings/DownloadClient/DownloadClientCollection.js index 6166da3e4..6e32ea832 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollection.js +++ b/src/UI/Settings/DownloadClient/DownloadClientCollection.js @@ -1,12 +1,31 @@ 'use strict'; -define( - [ - 'backbone', - 'Settings/DownloadClient/DownloadClientModel' - ], function (Backbone, DownloadClientModel) { - return Backbone.Collection.extend({ - model: DownloadClientModel, - url : window.NzbDrone.ApiRoot + '/downloadclient' - }); +define([ + 'backbone', + 'Settings/DownloadClient/DownloadClientModel' +], function (Backbone, DownloadClientModel) { + + return Backbone.Collection.extend({ + model: DownloadClientModel, + url : window.NzbDrone.ApiRoot + '/downloadclient', + + comparator : function(left, right, collection) { + + var result = 0; + + if (left.get('protocol')) { + result = -left.get('protocol').localeCompare(right.get('protocol')); + } + + if (result === 0 && left.get('name')) { + result = left.get('name').localeCompare(right.get('name')); + } + + if (result === 0) { + result = left.get('implementation').localeCompare(right.get('implementation')); + } + + return result; + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js index 4a11cb167..505d1ebfd 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js @@ -1,31 +1,29 @@ 'use strict'; -define( - [ - 'underscore', - 'AppLayout', - 'marionette', - 'Settings/DownloadClient/DownloadClientItemView', - 'Settings/DownloadClient/Add/SchemaModal' - ], function (_, AppLayout, Marionette, DownloadClientItemView, SchemaModal) { - return Marionette.CompositeView.extend({ - itemView : DownloadClientItemView, - itemViewContainer: '#x-download-clients', - template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', - ui: { - 'addCard': '.x-add-card' - }, +define([ + 'marionette', + 'Settings/DownloadClient/DownloadClientItemView', + 'Settings/DownloadClient/Add/DownloadClientSchemaModal' +], function (Marionette, ItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer: '.download-client-list', + template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', - events: { - 'click .x-add-card': '_openSchemaModal' - }, + ui: { + 'addCard': '.x-add-card' + }, - appendHtml: function (collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, + events: { + 'click .x-add-card': '_openSchemaModal' + }, - _openSchemaModal: function () { - SchemaModal.open(this.collection); - } - }); + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html index be4c04f09..a5ebbecef 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html @@ -2,7 +2,7 @@ <legend>Download Clients</legend> <div class="row"> <div class="col-md-12"> - <ul id="x-download-clients" class="download-client-list thingies"> + <ul class="download-client-list thingies"> <li> <div class="download-client-item thingy add-card x-add-card"> <span class="center well"> diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemView.js b/src/UI/Settings/DownloadClient/DownloadClientItemView.js index ae552f53c..0d8bf3315 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientItemView.js +++ b/src/UI/Settings/DownloadClient/DownloadClientItemView.js @@ -1,27 +1,26 @@ 'use strict'; -define( - [ - 'AppLayout', - 'marionette', - 'Settings/DownloadClient/Edit/DownloadClientEditView' - ], function (AppLayout, Marionette, EditView) { +define([ + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Edit/DownloadClientEditView' +], function (AppLayout, Marionette, EditView) { - return Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/DownloadClientItemViewTemplate', - tagName : 'li', + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/DownloadClientItemViewTemplate', + tagName : 'li', - events: { - 'click' : '_edit' - }, + events: { + 'click' : '_edit' + }, - initialize: function () { - this.listenTo(this.model, 'sync', this.render); - }, + initialize: function () { + this.listenTo(this.model, 'sync', this.render); + }, - _edit: function () { - var view = new EditView({ model: this.model, downloadClientCollection: this.model.collection }); - AppLayout.modalRegion.show(view); - } - }); + _edit: function () { + var view = new EditView({ model: this.model, targetCollection: this.model.collection}); + AppLayout.modalRegion.show(view); + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayout.js b/src/UI/Settings/DownloadClient/DownloadClientLayout.js index e632371dc..e9510f160 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayout.js +++ b/src/UI/Settings/DownloadClient/DownloadClientLayout.js @@ -1,32 +1,31 @@ 'use strict'; -define( - [ - 'marionette', - 'Settings/DownloadClient/DownloadClientCollection', - 'Settings/DownloadClient/DownloadClientCollectionView', - 'Settings/DownloadClient/Options/DownloadClientOptionsView', - 'Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView' - ], function (Marionette, DownloadClientCollection, DownloadClientCollectionView, DownloadClientOptionsView, FailedDownloadHandlingView) { +define([ + 'marionette', + 'Settings/DownloadClient/DownloadClientCollection', + 'Settings/DownloadClient/DownloadClientCollectionView', + 'Settings/DownloadClient/DroneFactory/DroneFactoryView', + 'Settings/DownloadClient/DownloadHandling/DownloadHandlingView' +], function (Marionette, DownloadClientCollection, CollectionView, DroneFactoryView, DownloadHandlingView) { - return Marionette.Layout.extend({ - template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', + return Marionette.Layout.extend({ + template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', - regions: { - downloadClients : '#x-download-clients-region', - downloadClientOptions : '#x-download-client-options-region', - failedDownloadHandling : '#x-failed-download-handling-region' - }, + regions: { + downloadClients : '#x-download-clients-region', + downloadHandling : '#x-download-handling-region', + droneFactory : '#x-dronefactory-region' + }, - initialize: function () { - this.downloadClientCollection = new DownloadClientCollection(); - this.downloadClientCollection.fetch(); - }, + initialize: function () { + this.downloadClientsCollection = new DownloadClientCollection(); + this.downloadClientsCollection.fetch(); + }, - onShow: function () { - this.downloadClients.show(new DownloadClientCollectionView({ collection: this.downloadClientCollection })); - this.downloadClientOptions.show(new DownloadClientOptionsView({ model: this.model })); - this.failedDownloadHandling.show(new FailedDownloadHandlingView({ model: this.model })); - } - }); - }); \ No newline at end of file + onShow: function () { + this.downloadClients.show(new CollectionView({ collection: this.downloadClientsCollection })); + this.downloadHandling.show(new DownloadHandlingView({ model: this.model })); + this.droneFactory.show(new DroneFactoryView({ model: this.model })); + } + }); +}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html index 365590417..7450b08d3 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html @@ -1,6 +1,6 @@ <div id="x-download-clients-region"></div> <div class="form-horizontal"> - <div id="x-download-client-options-region"></div> - <div id="x-failed-download-handling-region"></div> -</div> + <div id="x-download-handling-region"></div> + <div id="x-dronefactory-region"></div> +</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientModel.js b/src/UI/Settings/DownloadClient/DownloadClientModel.js index 5e08858af..3702cf7dc 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientModel.js +++ b/src/UI/Settings/DownloadClient/DownloadClientModel.js @@ -1,10 +1,9 @@ 'use strict'; -define( - [ - 'backbone.deepmodel' - ], function (DeepModel) { - return DeepModel.DeepModel.extend({ - }); +define([ + 'backbone.deepmodel' +], function (DeepModel) { + return DeepModel.DeepModel.extend({ + }); - +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js index 8a3b066b3..ab2a40f89 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js +++ b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js @@ -1,11 +1,11 @@ 'use strict'; -define( - [ - 'Settings/SettingsModelBase' - ], function (SettingsModelBase) { - return SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/downloadclient', - successMessage: 'Download client settings saved', - errorMessage : 'Failed to save download client settings' - }); + +define([ + 'Settings/SettingsModelBase' +], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/downloadclient', + successMessage: 'Download client settings saved', + errorMessage : 'Failed to save download client settings' }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js new file mode 100644 index 000000000..d88d628ef --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js @@ -0,0 +1,60 @@ +'use strict'; +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (Marionette, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate', + + ui: { + completedDownloadHandlingCheckbox : '.x-completed-download-handling', + completedDownloadOptions : '.x-completed-download-options', + failedDownloadHandlingCheckbox : '.x-failed-download-handling', + failedDownloadOptions : '.x-failed-download-options' + }, + + events: { + 'change .x-completed-download-handling' : '_setCompletedDownloadOptionsVisibility', + 'change .x-failed-download-handling' : '_setFailedDownloadOptionsVisibility' + }, + + onRender: function () { + if (!this.ui.completedDownloadHandlingCheckbox.prop('checked')) { + this.ui.completedDownloadOptions.hide(); + } + if (!this.ui.failedDownloadHandlingCheckbox.prop('checked')) { + this.ui.failedDownloadOptions.hide(); + } + }, + + _setCompletedDownloadOptionsVisibility: function () { + var checked = this.ui.completedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.completedDownloadOptions.slideDown(); + } + + else { + this.ui.completedDownloadOptions.slideUp(); + } + }, + + _setFailedDownloadOptionsVisibility: function () { + var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.failedDownloadOptions.slideDown(); + } + + else { + this.ui.failedDownloadOptions.slideUp(); + } + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html similarity index 63% rename from src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html rename to src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html index 0bf3acc39..d32b8f72f 100644 --- a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html @@ -1,6 +1,56 @@ -<fieldset class="advanced-setting"> - <legend>Failed Download Handling</legend> +<fieldset> + <legend>Completed Download Handling</legend> + <div class="form-group"> + <label class="col-sm-3 control-label">Enable</label> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enableCompletedDownloadHandling" class="x-completed-download-handling"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Import completed downloads in download client history"/> + <i class="icon-nd-form-warning" title="Download client history items that are stored in the drone factory will be ignored. Configure the Drone Factory for a different path"/> + </span> + </div> + </div> + </div> + + <div class="x-completed-download-options advanced-setting"> + <div class="form-group"> + <label class="col-sm-3 control-label">Remove</label> + + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="removeCompletedDownloads"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Remove imported downloads from download client history"/> + </span> + </div> + </div> + </div> + </div> +</fieldset> + +<fieldset class="advanced-setting"> + <legend>Failed Download Handling</legend> + <div class="form-group"> <label class="col-sm-3 control-label">Enable</label> @@ -40,7 +90,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release when a download fails?"/> + <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release"/> </span> </div> </div> @@ -62,7 +112,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically remove failed downloads from history and encrypted downloads from queue?"/> + <i class="icon-nd-form-info" title="Remove failed downloads from download client history"/> </span> </div> </div> diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js similarity index 86% rename from src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js rename to src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js index 444bed1d8..100b9b46c 100644 --- a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js +++ b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js @@ -8,7 +8,7 @@ define( ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate', + template: 'Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate', ui: { droneFactory : '.x-path' diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.html similarity index 86% rename from src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html rename to src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.html index f5feb5a57..afd61ea24 100644 --- a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.html @@ -1,10 +1,10 @@ <fieldset> - <legend>Options</legend> + <legend>Drone Factory Options</legend> <div class="form-group"> <label class="col-sm-3 control-label">Drone Factory</label> <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-nd-form-info" title="The folder where your download client downloads TV shows to (Completed Download Directory)"/> + <i class="icon-nd-form-info" title="Optional folder to periodically scan for available imports"/> <i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i> </div> diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js index 6f75aaf8f..98fa8eea0 100644 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js +++ b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js @@ -1,97 +1,96 @@ 'use strict'; -define( - [ - 'vent', - 'AppLayout', - 'marionette', - 'Settings/DownloadClient/Delete/DownloadClientDeleteView', - 'Commands/CommandController', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView', - 'underscore', - 'Form/FormBuilder', - 'Mixins/AutoComplete', - 'bootstrap' - ], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { +define([ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Delete/DownloadClientDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder', + 'Mixins/AutoComplete', + 'bootstrap' +], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { - var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', - ui: { - path : '.x-path', - modalBody : '.modal-body' - }, + ui: { + path : '.x-path', + modalBody : '.modal-body' + }, - events: { - 'click .x-save' : '_save', - 'click .x-save-and-add': '_saveAndAdd', - 'click .x-delete' : '_delete', - 'click .x-back' : '_back', - 'click .x-test' : '_test' - }, + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-test' : '_test' + }, - initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; - }, + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, - onShow: function () { - //Hack to deal with modals not overflowing - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.autoComplete('/directories'); - }, - - _save: function () { - var self = this; - var promise = this.model.save(); - - if (promise) { - promise.done(function () { - self.downloadClientCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - } - }, - - _saveAndAdd: function () { - var self = this; - var promise = this.model.save(); - - if (promise) { - promise.done(function () { - self.notificationCollection.add(self.model, { merge: true }); - - require('Settings/DownloadClient/Add/SchemaModal').open(self.downloadClientCollection); - }); - } - }, - - _delete: function () { - var view = new DeleteView({ model: this.model }); - AppLayout.modalRegion.show(view); - }, - - _back: function () { - require('Settings/DownloadClient/Add/SchemaModal').open(this.downloadClientCollection); - }, - - _test: function () { - var testCommand = 'test{0}'.format(this.model.get('implementation')); - var properties = {}; - - _.each(this.model.get('fields'), function (field) { - properties[field.name] = field.value; - }); - - CommandController.Execute(testCommand, properties); + onShow: function () { + //Hack to deal with modals not overflowing + if (this.ui.path.length > 0) { + this.ui.modalBody.addClass('modal-overflow'); } - }); - AsModelBoundView.call(view); - AsValidatedView.call(view); + this.ui.path.autoComplete('/directories'); + }, - return view; + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + + require('Settings/DownloadClient/Add/DownloadClientSchemaModal').open(self.targetCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + require('Settings/DownloadClient/Add/DownloadClientSchemaModal').open(this.targetCollection); + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + } }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; +}); diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js deleted file mode 100644 index 9af62d5dc..000000000 --- a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView' - ], function (Marionette, AsModelBoundView, AsValidatedView) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate', - - ui: { - failedDownloadHandlingCheckbox: '.x-failed-download-handling', - failedDownloadOptions : '.x-failed-download-options' - }, - - events: { - 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' - }, - - _setFailedDownloadOptionsVisibility: function () { - var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); - if (checked) { - this.ui.failedDownloadOptions.slideDown(); - } - - else { - this.ui.failedDownloadOptions.slideUp(); - } - } - }); - - AsModelBoundView.call(view); - AsValidatedView.call(view); - - return view; - }); diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less index bd8da872e..9b9f30f10 100644 --- a/src/UI/Settings/DownloadClient/downloadclient.less +++ b/src/UI/Settings/DownloadClient/downloadclient.less @@ -27,7 +27,7 @@ } .add-download-client { - li { + li.add-thingy-item { width: 33%; } } \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js b/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js new file mode 100644 index 000000000..35edef28d --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js @@ -0,0 +1,14 @@ +'use strict'; + +define([ + 'Settings/ThingyAddCollectionView', + 'Settings/ThingyHeaderGroupView', + 'Settings/Indexers/Add/IndexerAddItemView' +], function (ThingyAddCollectionView, ThingyHeaderGroupView, AddItemView) { + + return ThingyAddCollectionView.extend({ + itemView : ThingyHeaderGroupView.extend({ itemView: AddItemView }), + itemViewContainer: '.add-indexer .items', + template : 'Settings/Indexers/Add/IndexerAddCollectionViewTemplate' + }); +}); diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html new file mode 100644 index 000000000..95d3ceb9a --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html @@ -0,0 +1,16 @@ +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add Indexer</h3> + </div> + <div class="modal-body"> + <div class="add-indexer add-thingies"> + <ul class="items"></ul> + </div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> + </div> +</div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js new file mode 100644 index 000000000..c28f40bfb --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js @@ -0,0 +1,56 @@ +'use strict'; + +define([ + 'underscore', + 'jquery', + 'AppLayout', + 'marionette', + 'Settings/Indexers/Edit/IndexerEditView' +], function (_, $, AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template : 'Settings/Indexers/Add/IndexerAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', + + events: { + 'click .x-preset': '_addPreset', + 'click' : '_add' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + _addPreset: function (e) { + + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + + var presetData = _.where(this.model.get('presets'), {name: presetName})[0]; + + this.model.set(presetData); + + this.model.set({ + id : undefined, + enable : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + }, + + _add: function (e) { + if ($(e.target).closest('.btn,.btn-group').length !== 0) { + return; + } + + this.model.set({ + id : undefined, + enable : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + } + }); +}); diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html new file mode 100644 index 000000000..a2c0295be --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html @@ -0,0 +1,27 @@ +<div class="add-thingy"> + <div> + {{implementation}} + </div> + <div class="pull-right"> + {{#if_gt presets.length compare=0}} + <div class="btn-group"> + <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> + Presets + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {{#each presets}} + <li class="x-preset" data-id="{{name}}"> + <a>{{name}}</a> + </li> + {{/each}} + </ul> + </div> + {{/if_gt}} + {{#if infoLink}} + <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> + <i class="icon-info-sign"/> + </a> + {{/if}} + </div> +</div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js b/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js new file mode 100644 index 000000000..f702481b7 --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js @@ -0,0 +1,36 @@ +'use strict'; + +define([ + 'underscore', + 'AppLayout', + 'backbone', + 'Settings/Indexers/IndexerCollection', + 'Settings/Indexers/Add/IndexerAddCollectionView' +], function (_, AppLayout, Backbone, SchemaCollection, AddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var groupedSchemaCollection = new Backbone.Collection(); + + schemaCollection.on('sync', function() { + + var groups = schemaCollection.groupBy(function(model, iterator) { return model.get('protocol'); }); + + var modelCollection = _.map(groups, function(values, key, list) { + return { 'header': key, collection: values }; + }); + + groupedSchemaCollection.reset(modelCollection); + }); + + var view = new AddCollectionView({ collection: groupedSchemaCollection, targetCollection: collection }); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/Indexers/Collection.js b/src/UI/Settings/Indexers/Collection.js deleted file mode 100644 index fc20e436a..000000000 --- a/src/UI/Settings/Indexers/Collection.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Settings/Indexers/Model', - ], function (Backbone, IndexerModel) { - return Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/indexer', - model: IndexerModel - }); - }); diff --git a/src/UI/Settings/Indexers/CollectionView.js b/src/UI/Settings/Indexers/CollectionView.js deleted file mode 100644 index 662dd5298..000000000 --- a/src/UI/Settings/Indexers/CollectionView.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; -define( - [ - 'AppLayout', - 'marionette', - 'Settings/Indexers/ItemView', - 'Settings/Indexers/EditView', - 'Settings/Indexers/Collection', - 'underscore' - ], function (AppLayout, Marionette, IndexerItemView, IndexerEditView, IndexerCollection, _) { - return Marionette.CompositeView.extend({ - itemView : IndexerItemView, - itemViewContainer: '#x-indexers', - template : 'Settings/Indexers/CollectionTemplate', - - ui: { - 'addCard': '.x-add-card' - }, - - events: { - 'click .x-add-card': '_openSchemaModal' - }, - - appendHtml: function (collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal: function () { - var self = this; - var schemaCollection = new IndexerCollection(); - var originalUrl = schemaCollection.url; - - schemaCollection.url = schemaCollection.url + '/schema'; - - schemaCollection.fetch({ - success: function (collection) { - collection.url = originalUrl; - var model = _.first(collection.models); - - model.set({ - id : undefined, - name : '', - enable: true - }); - - var view = new IndexerEditView({ model: model, indexerCollection: self.collection}); - AppLayout.modalRegion.show(view); - } - }); - } - }); - }); diff --git a/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js b/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js new file mode 100644 index 000000000..adcb5236f --- /dev/null +++ b/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/Indexers/Delete/IndexerDeleteViewTemplate', + + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } + }); +}); diff --git a/src/UI/Settings/Indexers/DeleteViewTemplate.html b/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.html similarity index 100% rename from src/UI/Settings/Indexers/DeleteViewTemplate.html rename to src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.html diff --git a/src/UI/Settings/Indexers/DeleteView.js b/src/UI/Settings/Indexers/DeleteView.js deleted file mode 100644 index b230684eb..000000000 --- a/src/UI/Settings/Indexers/DeleteView.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; -define( - [ - 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ - template: 'Settings/Indexers/DeleteViewTemplate', - - events: { - 'click .x-confirm-delete': '_removeNotification' - }, - - _removeNotification: function () { - this.model.destroy({ - wait : true, - success: function () { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } - }); - }); diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js new file mode 100644 index 000000000..4a77072d7 --- /dev/null +++ b/src/UI/Settings/Indexers/Edit/IndexerEditView.js @@ -0,0 +1,100 @@ +'use strict'; + +define([ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/Indexers/Delete/IndexerDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder', + 'Mixins/AutoComplete', + 'bootstrap' +], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Indexers/Edit/IndexerEditViewTemplate', + + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-close' : '_close', + 'click .x-test' : '_test' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + + require('Settings/Indexers/Add/IndexerSchemaModal').open(self.targetCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + + require('Settings/Indexers/Add/IndexerSchemaModal').open(this.targetCollection); + }, + + _close: function () { + + if (this.model.isNew()) { + this.model.destroy(); + } + + else { + this.model.fetch(); + } + + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; +}); diff --git a/src/UI/Settings/Indexers/EditTemplate.html b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html similarity index 75% rename from src/UI/Settings/Indexers/EditTemplate.html rename to src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html index 7e7eee4e0..75ec1d284 100644 --- a/src/UI/Settings/Indexers/EditTemplate.html +++ b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html @@ -1,14 +1,14 @@ <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close x-cancel"aria-hidden="true">×</button> + <button type="button" class="close x-close" aria-hidden="true">×</button> {{#if id}} - <h3>Edit</h3> + <h3>Edit - {{implementation}}</h3> {{else}} - <h3>Add Newznab</h3> + <h3>Add - {{implementation}}</h3> {{/if}} </div> - <div class="modal-body"> + <div class="modal-body indexer-modal"> <div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Name</label> @@ -41,12 +41,14 @@ </div> <div class="modal-footer"> {{#if id}} - <button class="btn btn-danger pull-left x-remove">delete</button> + <button class="btn btn-danger pull-left x-delete">delete</button> + {{else}} + <button class="btn pull-left x-back">back</button> {{/if}} - <span class="x-activity"></span> - - <button class="btn x-cancel">cancel</button> + <!-- Testing is currently not yet supported for indexers, but leaving the infrastructure for later --> + <!-- <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> --> + <button class="btn x-close">cancel</button> <div class="btn-group"> <button class="btn btn-primary x-save">save</button> diff --git a/src/UI/Settings/Indexers/EditView.js b/src/UI/Settings/Indexers/EditView.js deleted file mode 100644 index dca99a2ac..000000000 --- a/src/UI/Settings/Indexers/EditView.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -define( - [ - 'vent', - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView', - 'underscore' - ], function (vent, Marionette, AsModelBoundView, AsValidatedView, _) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Indexers/EditTemplate', - - ui: { - activity: '.x-activity' - }, - - events: { - 'click .x-save' : '_save', - 'click .x-save-and-add': '_saveAndAdd', - 'click .x-cancel' : '_cancel' - }, - - initialize: function (options) { - this.indexerCollection = options.indexerCollection; - }, - - _save: function () { - this.ui.activity.html('<i class="icon-nd-spinner"></i>'); - - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.indexerCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - - promise.fail(function () { - self.ui.activity.empty(); - }); - } - }, - - _saveAndAdd: function () { - this.ui.activity.html('<i class="icon-nd-spinner"></i>'); - - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.indexerCollection.add(self.model, { merge: true }); - - self.model.set({ - id : undefined, - name : '', - enable: false - }); - - _.each(self.model.get('fields'), function (value, key, list) { - self.model.set('fields.' + key + '.value', ''); - }); - }); - - promise.fail(function () { - self.ui.activity.empty(); - }); - } - }, - - _cancel: function () { - if (this.model.isNew()) { - this.model.destroy(); - vent.trigger(vent.Commands.CloseModalCommand); - } - } - }); - - AsModelBoundView.call(view); - AsValidatedView.call(view); - - return view; - }); diff --git a/src/UI/Settings/Indexers/IndexerCollection.js b/src/UI/Settings/Indexers/IndexerCollection.js new file mode 100644 index 000000000..4a3f2492e --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerCollection.js @@ -0,0 +1,31 @@ +'use strict'; + +define([ + 'backbone', + 'Settings/Indexers/IndexerModel' +], function (Backbone, IndexerModel) { + + return Backbone.Collection.extend({ + model: IndexerModel, + url : window.NzbDrone.ApiRoot + '/indexer', + + comparator : function(left, right, collection) { + + var result = 0; + + if (left.get('protocol')) { + result = -left.get('protocol').localeCompare(right.get('protocol')); + } + + if (result === 0 && left.get('name')) { + result = left.get('name').localeCompare(right.get('name')); + } + + if (result === 0) { + result = left.get('implementation').localeCompare(right.get('implementation')); + } + + return result; + } + }); +}); diff --git a/src/UI/Settings/Indexers/IndexerCollectionView.js b/src/UI/Settings/Indexers/IndexerCollectionView.js new file mode 100644 index 000000000..7bfd67322 --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerCollectionView.js @@ -0,0 +1,29 @@ +'use strict'; + +define([ + 'marionette', + 'Settings/Indexers/IndexerItemView', + 'Settings/Indexers/Add/IndexerSchemaModal' +], function (Marionette, ItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer: '.indexer-list', + template : 'Settings/Indexers/IndexerCollectionViewTemplate', + + ui: { + 'addCard': '.x-add-card' + }, + + events: { + 'click .x-add-card': '_openSchemaModal' + }, + + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } + }); +}); diff --git a/src/UI/Settings/Indexers/CollectionTemplate.html b/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.html similarity index 57% rename from src/UI/Settings/Indexers/CollectionTemplate.html rename to src/UI/Settings/Indexers/IndexerCollectionViewTemplate.html index 657ee83d7..09e4e129b 100644 --- a/src/UI/Settings/Indexers/CollectionTemplate.html +++ b/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.html @@ -2,11 +2,11 @@ <legend>Indexers</legend> <div class="row"> <div class="col-md-12"> - <ul id="x-indexers" class="indexer-list thingies"> + <ul class="indexer-list thingies"> <li> - <div class="indexer-settings-item add-card x-add-card"> + <div class="indexer-item thingy add-card x-add-card"> <span class="center well"> - <i class="icon-plus" title="Add Newznab"/> + <i class="icon-plus" title="Add Indexer"/> </span> </div> </li> diff --git a/src/UI/Settings/Indexers/IndexerItemView.js b/src/UI/Settings/Indexers/IndexerItemView.js new file mode 100644 index 000000000..a85a73b21 --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerItemView.js @@ -0,0 +1,26 @@ +'use strict'; + +define([ + 'AppLayout', + 'marionette', + 'Settings/Indexers/Edit/IndexerEditView' +], function (AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template: 'Settings/Indexers/IndexerItemViewTemplate', + tagName : 'li', + + events: { + 'click' : '_edit' + }, + + initialize: function () { + this.listenTo(this.model, 'sync', this.render); + }, + + _edit: function () { + var view = new EditView({ model: this.model, targetCollection: this.model.collection }); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/Indexers/IndexerItemViewTemplate.html b/src/UI/Settings/Indexers/IndexerItemViewTemplate.html new file mode 100644 index 000000000..d1b3cf807 --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerItemViewTemplate.html @@ -0,0 +1,13 @@ +<div class="indexer-item thingy" title="Click to edit"> + <div> + <h3>{{name}}</h3> + </div> + + <div class="settings"> + {{#if enable}} + <span class="label label-success">Enabled</span> + {{else}} + <span class="label label-default">Not Enabled</span> + {{/if}} + </div> +</div> diff --git a/src/UI/Settings/Indexers/IndexerLayout.js b/src/UI/Settings/Indexers/IndexerLayout.js index eca151eb1..9f3b4c209 100644 --- a/src/UI/Settings/Indexers/IndexerLayout.js +++ b/src/UI/Settings/Indexers/IndexerLayout.js @@ -1,28 +1,28 @@ 'use strict'; -define( - [ - 'marionette', - 'Settings/Indexers/CollectionView', - 'Settings/Indexers/Options/IndexerOptionsView' - ], function (Marionette, CollectionView, OptionsView) { - return Marionette.Layout.extend({ - template: 'Settings/Indexers/IndexerLayoutTemplate', +define([ + 'marionette', + 'Settings/Indexers/IndexerCollection', + 'Settings/Indexers/IndexerCollectionView', + 'Settings/Indexers/Options/IndexerOptionsView' +], function (Marionette, IndexerCollection, CollectionView, OptionsView) { - regions: { - indexersRegion : '#indexers-collection', - indexerOptions : '#indexer-options' - }, + return Marionette.Layout.extend({ + template: 'Settings/Indexers/IndexerLayoutTemplate', - initialize: function (options) { - this.settings = options.settings; - this.indexersCollection = options.indexersCollection; - }, + regions: { + indexers : '#x-indexers-region', + indexerOptions : '#x-indexer-options-region' + }, - onShow: function () { - this.indexersRegion.show(new CollectionView({ collection: this.indexersCollection })); - this.indexerOptions.show(new OptionsView({ model: this.settings })); - } - }); + initialize: function (options) { + this.indexersCollection = new IndexerCollection(); + this.indexersCollection.fetch(); + }, + + onShow: function () { + this.indexers.show(new CollectionView({ collection: this.indexersCollection })); + this.indexerOptions.show(new OptionsView({ model: this.model })); + } }); - +}); diff --git a/src/UI/Settings/Indexers/IndexerLayoutTemplate.html b/src/UI/Settings/Indexers/IndexerLayoutTemplate.html index a0c6402a8..91bfbbdef 100644 --- a/src/UI/Settings/Indexers/IndexerLayoutTemplate.html +++ b/src/UI/Settings/Indexers/IndexerLayoutTemplate.html @@ -1,5 +1,4 @@ -<div id="indexers-collection"></div> - +<div id="x-indexers-region"></div> <div class="form-horizontal"> - <div id="indexer-options"></div> + <div id="x-indexer-options-region"></div> </div> diff --git a/src/UI/Settings/Indexers/IndexerModel.js b/src/UI/Settings/Indexers/IndexerModel.js new file mode 100644 index 000000000..3702cf7dc --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerModel.js @@ -0,0 +1,9 @@ +'use strict'; + +define([ + 'backbone.deepmodel' +], function (DeepModel) { + return DeepModel.DeepModel.extend({ + + }); +}); diff --git a/src/UI/Settings/Indexers/IndexerSettingsModel.js b/src/UI/Settings/Indexers/IndexerSettingsModel.js index 34ede06ee..ce3a654ea 100644 --- a/src/UI/Settings/Indexers/IndexerSettingsModel.js +++ b/src/UI/Settings/Indexers/IndexerSettingsModel.js @@ -1,11 +1,11 @@ 'use strict'; -define( - [ - 'Settings/SettingsModelBase' - ], function (SettingsModelBase) { - return SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/indexer', - successMessage: 'Indexer settings saved', - errorMessage : 'Failed to save indexer settings' - }); + +define([ + 'Settings/SettingsModelBase' +], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/indexer', + successMessage: 'Indexer settings saved', + errorMessage : 'Failed to save indexer settings' }); +}); diff --git a/src/UI/Settings/Indexers/ItemTemplate.html b/src/UI/Settings/Indexers/ItemTemplate.html deleted file mode 100644 index 4f38978ad..000000000 --- a/src/UI/Settings/Indexers/ItemTemplate.html +++ /dev/null @@ -1,37 +0,0 @@ -<div class="indexer-settings-item thingy"> - <div> - <h3>{{name}}</h3> - {{#if_eq implementation compare="Newznab"}} - <span class="btn-group pull-right"> - <button class="btn btn-xs btn-icon-only x-delete"> - <i class="icon-nd-delete"/> - </button> - </span> - {{/if_eq}} - </div> - - <div class="form-group"> - <label class="control-label">Enable</label> - - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - {{formBuilder}} - - {{#if_eq name compare="WomblesIndex"}} - <div class="alert"> - <i class="icon-nd-warning"></i> - Does not support searching - </div> - {{/if_eq}} -</div> diff --git a/src/UI/Settings/Indexers/ItemView.js b/src/UI/Settings/Indexers/ItemView.js deleted file mode 100644 index 23ab0d00b..000000000 --- a/src/UI/Settings/Indexers/ItemView.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -define( - [ - 'AppLayout', - 'marionette', - 'Settings/Indexers/DeleteView', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView' - ], function (AppLayout, Marionette, DeleteView, AsModelBoundView, AsValidatedView) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Indexers/ItemTemplate', - tagName : 'li', - - events: { - 'click .x-delete': '_deleteIndexer' - }, - - _deleteIndexer: function () { - var view = new DeleteView({ model: this.model}); - AppLayout.modalRegion.show(view); - } - }); - - AsModelBoundView.call(view); - return AsValidatedView.call(view); - - }); diff --git a/src/UI/Settings/Indexers/Model.js b/src/UI/Settings/Indexers/Model.js deleted file mode 100644 index ecfee73b8..000000000 --- a/src/UI/Settings/Indexers/Model.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; -define([ - 'Settings/SettingsModelBase'], function (ModelBase) { - return ModelBase.extend({ - - baseInitialize: ModelBase.prototype.initialize, - - initialize: function () { - var name = this.get('name'); - - if (name) { - this.successMessage = 'Saved indexer: ' + name; - this.errorMessage = 'Couldn\'t save indexer: ' + name; - } - - else { - this.successMessage = 'Saved indexer'; - this.errorMessage = 'Couldn\'t save indexer'; - } - - this.baseInitialize.call(this); - } - }); -}); diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less index 281e70e90..3fed3ef5f 100644 --- a/src/UI/Settings/Indexers/indexers.less +++ b/src/UI/Settings/Indexers/indexers.less @@ -1,29 +1,33 @@ -.indexer-settings-item { +@import "../../Shared/Styles/clickable.less"; - width: 220px; - height: 295px; +.indexer-list { + li { + display: inline-block; + vertical-align: top; + } +} + +.indexer-item { + + .clickable; + + width: 290px; + height: 90px; padding: 10px 15px; - h3 { - width: 175px; - overflow: visible; - } - &.add-card { - margin-top: 10px; - margin-left: 10px; - .center { - margin-top: 90px; + margin-top: -3px; } } +} - /* Super hack to keep using form builder, this should be dead when we do proper modals for editing */ - .col-sm-1, .col-sm-3, .col-sm-5 { - display : block; - width : 100%; - padding: 0px; - float: none; - position: inherit; +.modal-overflow { + overflow-y: visible; +} + +.add-indexer { + li.add-thingy-item { + width: 33%; } } \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html index 05a416998..1126a78f6 100644 --- a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html +++ b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html @@ -2,5 +2,5 @@ <div id="episode-naming"></div> <div id="sorting"></div> <div id="file-management"></div> - <div id="permissions"></div> + {{#if_mono}}<div id="permissions"></div>{{/if_mono}} </div> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html index fd8ddcc69..704863c1c 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html @@ -1,5 +1,4 @@ -{{#if_mono}} -<fieldset class="advanced-setting"> +<fieldset class="advanced-setting"> <legend>Permissions</legend> <div class="form-group"> @@ -73,4 +72,3 @@ </div> </div> </fieldset> -{{/if_mono}} diff --git a/src/UI/Settings/Metadata/MetadataItemView.js b/src/UI/Settings/Metadata/MetadataItemView.js index 3fa9f43f4..95f6bdd4a 100644 --- a/src/UI/Settings/Metadata/MetadataItemView.js +++ b/src/UI/Settings/Metadata/MetadataItemView.js @@ -13,7 +13,7 @@ define( tagName : 'li', events: { - 'click .x-edit' : '_edit' + 'click' : '_edit' }, initialize: function () { diff --git a/src/UI/Settings/Metadata/MetadataItemViewTemplate.html b/src/UI/Settings/Metadata/MetadataItemViewTemplate.html index f461ea4fb..544985ff0 100644 --- a/src/UI/Settings/Metadata/MetadataItemViewTemplate.html +++ b/src/UI/Settings/Metadata/MetadataItemViewTemplate.html @@ -1,9 +1,6 @@ -<div class="metadata-item"> +<div class="metadata-item" title="Click to edit"> <div> <h3>{{name}}</h3> - <span class="btn-group pull-right"> - <button class="btn btn-xs btn-icon-only x-edit"><i class="icon-nd-edit"/></button> - </span> </div> <div class="settings"> diff --git a/src/UI/Settings/Metadata/metadata.less b/src/UI/Settings/Metadata/metadata.less index b90674017..566114a39 100644 --- a/src/UI/Settings/Metadata/metadata.less +++ b/src/UI/Settings/Metadata/metadata.less @@ -10,6 +10,7 @@ .metadata-item { .card; + .clickable; width: 200px; height: 230px; @@ -18,7 +19,7 @@ h3 { margin-top: 0px; display: inline-block; - width: 140px; + width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js b/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js new file mode 100644 index 000000000..713e05c2c --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js @@ -0,0 +1,13 @@ +'use strict'; + +define([ + 'Settings/ThingyAddCollectionView', + 'Settings/Notifications/Add/NotificationAddItemView' +], function (ThingyAddCollectionView, AddItemView) { + + return ThingyAddCollectionView.extend({ + itemView : AddItemView, + itemViewContainer: '.add-notifications .items', + template : 'Settings/Notifications/Add/NotificationAddCollectionViewTemplate' + }); +}); diff --git a/src/UI/Settings/Notifications/AddTemplate.html b/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.html similarity index 100% rename from src/UI/Settings/Notifications/AddTemplate.html rename to src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.html diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js new file mode 100644 index 000000000..500f7bca8 --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js @@ -0,0 +1,60 @@ +'use strict'; + +define([ + 'underscore', + 'jquery', + 'AppLayout', + 'marionette', + 'Settings/Notifications/Edit/NotificationEditView' +], function (_, $, AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template : 'Settings/Notifications/Add/NotificationAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', + + events: { + 'click .x-preset': '_addPreset', + 'click' : '_add' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + _addPreset: function (e) { + + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + + var presetData = _.where(this.model.get('presets'), {name: presetName})[0]; + + this.model.set(presetData); + + this.model.set({ + id : undefined, + onGrab : true, + onDownload : true, + onUpgrade : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + }, + + _add: function (e) { + if ($(e.target).closest('.btn,.btn-group').length !== 0) { + return; + } + + this.model.set({ + id : undefined, + onGrab : true, + onDownload : true, + onUpgrade : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + } + }); +}); diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html new file mode 100644 index 000000000..a2c0295be --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html @@ -0,0 +1,27 @@ +<div class="add-thingy"> + <div> + {{implementation}} + </div> + <div class="pull-right"> + {{#if_gt presets.length compare=0}} + <div class="btn-group"> + <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> + Presets + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {{#each presets}} + <li class="x-preset" data-id="{{name}}"> + <a>{{name}}</a> + </li> + {{/each}} + </ul> + </div> + {{/if_gt}} + {{#if infoLink}} + <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> + <i class="icon-info-sign"/> + </a> + {{/if}} + </div> +</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js b/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js new file mode 100644 index 000000000..f931ac924 --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js @@ -0,0 +1,21 @@ +'use strict'; + +define([ + 'AppLayout', + 'Settings/Notifications/NotificationCollection', + 'Settings/Notifications/Add/NotificationAddCollectionView' +], function (AppLayout, SchemaCollection, AddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var view = new AddCollectionView({ collection: schemaCollection, targetCollection: collection}); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/Notifications/AddItemTemplate.html b/src/UI/Settings/Notifications/AddItemTemplate.html deleted file mode 100644 index f892a4d01..000000000 --- a/src/UI/Settings/Notifications/AddItemTemplate.html +++ /dev/null @@ -1,6 +0,0 @@ -<div class="add-thingy"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/AddItemView.js b/src/UI/Settings/Notifications/AddItemView.js deleted file mode 100644 index 2c031b3e0..000000000 --- a/src/UI/Settings/Notifications/AddItemView.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -define([ - 'AppLayout', - 'marionette', - 'Settings/Notifications/NotificationEditView' -], function (AppLayout, Marionette, EditView) { - - return Marionette.ItemView.extend({ - template: 'Settings/Notifications/AddItemTemplate', - tagName : 'li', - - events: { - 'click': 'addNotification' - }, - - initialize: function (options) { - this.notificationCollection = options.notificationCollection; - }, - - addNotification: function (e) { - if (this.$(e.target).hasClass('icon-info-sign')) { - return; - } - - this.model.set({ - id : undefined, - name : this.model.get('implementationName'), - onGrab : true, - onDownload : true, - onUpgrade : true - }); - - var editView = new EditView({ model: this.model, notificationCollection: this.notificationCollection }); - AppLayout.modalRegion.show(editView); - } - }); -}); diff --git a/src/UI/Settings/Notifications/AddView.js b/src/UI/Settings/Notifications/AddView.js deleted file mode 100644 index 17e1064d2..000000000 --- a/src/UI/Settings/Notifications/AddView.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -define([ - 'marionette', - 'Settings/Notifications/AddItemView' -], function (Marionette, AddItemView) { - - return Marionette.CompositeView.extend({ - itemView : AddItemView, - itemViewContainer: '.add-notifications .items', - template : 'Settings/Notifications/AddTemplate', - - itemViewOptions: function () { - return { - notificationCollection: this.notificationCollection - }; - }, - - initialize: function (options) { - this.notificationCollection = options.notificationCollection; - } - }); -}); diff --git a/src/UI/Settings/Notifications/Collection.js b/src/UI/Settings/Notifications/Collection.js deleted file mode 100644 index a045020ab..000000000 --- a/src/UI/Settings/Notifications/Collection.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Settings/Notifications/Model' - ], function (Backbone, NotificationModel) { - return Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/notification', - model: NotificationModel - }); - }); diff --git a/src/UI/Settings/Notifications/CollectionTemplate.html b/src/UI/Settings/Notifications/CollectionTemplate.html deleted file mode 100644 index b9cfad00d..000000000 --- a/src/UI/Settings/Notifications/CollectionTemplate.html +++ /dev/null @@ -1,13 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <ul class="notifications thingies"> - <li> - <div class="notification-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-plus" title="Add Connection"/> - </span> - </div> - </li> - </ul> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/CollectionView.js b/src/UI/Settings/Notifications/CollectionView.js deleted file mode 100644 index efae31c1b..000000000 --- a/src/UI/Settings/Notifications/CollectionView.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; -define([ - 'marionette', - 'Settings/Notifications/NotificationsItemView', - 'Settings/Notifications/SchemaModal' -], function (Marionette, NotificationItemView, SchemaModal) { - return Marionette.CompositeView.extend({ - itemView : NotificationItemView, - itemViewContainer: '.notifications', - template : 'Settings/Notifications/CollectionTemplate', - - ui: { - 'addCard': '.x-add-card' - }, - - events: { - 'click .x-add-card': '_openSchemaModal' - }, - - appendHtml: function(collectionView, itemView, index){ - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal: function () { - SchemaModal.open(this.collection); - } - }); -}); diff --git a/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js b/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js new file mode 100644 index 000000000..858fcf85e --- /dev/null +++ b/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/Notifications/Delete/NotificationDeleteViewTemplate', + + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } + }); +}); diff --git a/src/UI/Settings/Notifications/DeleteTemplate.html b/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.html similarity index 100% rename from src/UI/Settings/Notifications/DeleteTemplate.html rename to src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.html diff --git a/src/UI/Settings/Notifications/DeleteView.js b/src/UI/Settings/Notifications/DeleteView.js deleted file mode 100644 index 24a03f776..000000000 --- a/src/UI/Settings/Notifications/DeleteView.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; -define( - [ - 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ - template: 'Settings/Notifications/DeleteTemplate', - - events: { - 'click .x-confirm-delete': '_removeNotification' - }, - - _removeNotification: function () { - this.model.destroy({ - wait : true, - success: function () { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } - }); - }); diff --git a/src/UI/Settings/Notifications/Edit/NotificationEditView.js b/src/UI/Settings/Notifications/Edit/NotificationEditView.js new file mode 100644 index 000000000..0855d5482 --- /dev/null +++ b/src/UI/Settings/Notifications/Edit/NotificationEditView.js @@ -0,0 +1,113 @@ +'use strict'; + +define([ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/Notifications/Delete/NotificationDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder' +], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Notifications/Edit/NotificationEditViewTemplate', + + ui: { + onDownloadToggle: '.x-on-download', + onUpgradeSection: '.x-on-upgrade' + }, + + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-cancel' : '_cancel', + 'click .x-test' : '_test', + 'change .x-on-download': '_onDownloadChanged' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + onRender: function () { + this._onDownloadChanged(); + }, + + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + + require('Settings/Notifications/Add/NotificationSchemaModal').open(self.targetCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + + require('Settings/Notifications/Add/NotificationSchemaModal').open(this.targetCollection); + }, + + _cancel: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + }, + + _onDownloadChanged: function () { + var checked = this.ui.onDownloadToggle.prop('checked'); + + if (checked) { + this.ui.onUpgradeSection.show(); + } + + else { + this.ui.onUpgradeSection.hide(); + } + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; +}); diff --git a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html b/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.html similarity index 94% rename from src/UI/Settings/Notifications/NotificationEditViewTemplate.html rename to src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.html index a68594879..79a4f4ba7 100644 --- a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html +++ b/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.html @@ -1,14 +1,14 @@ <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close x-cancel" aria-hidden="true">×</button> + <button type="button" class="close x-cancel" data-dismiss="modal" aria-hidden="true">×</button> {{#if id}} <h3>Edit - {{implementation}}</h3> {{else}} <h3>Add - {{implementation}}</h3> {{/if}} </div> - <div class="modal-body"> + <div class="modal-body notification-modal"> <div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Name</label> @@ -95,7 +95,7 @@ {{/if}} <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> - <button class="btn x-cancel">cancel</button> + <button class="btn x-cancel" data-dismiss="modal">cancel</button> <div class="btn-group"> <button class="btn btn-primary x-save">save</button> diff --git a/src/UI/Settings/Notifications/NotificationCollection.js b/src/UI/Settings/Notifications/NotificationCollection.js new file mode 100644 index 000000000..9a729c937 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationCollection.js @@ -0,0 +1,13 @@ +'use strict'; + +define([ + 'backbone', + 'Settings/Notifications/NotificationModel' +], function (Backbone, NotificationModel) { + + return Backbone.Collection.extend({ + model: NotificationModel, + url : window.NzbDrone.ApiRoot + '/notification' + + }); +}); diff --git a/src/UI/Settings/Notifications/NotificationCollectionView.js b/src/UI/Settings/Notifications/NotificationCollectionView.js new file mode 100644 index 000000000..ee74ef0c4 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationCollectionView.js @@ -0,0 +1,29 @@ +'use strict'; + +define([ + 'marionette', + 'Settings/Notifications/NotificationItemView', + 'Settings/Notifications/Add/NotificationSchemaModal' +], function (Marionette, ItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer: '.notification-list', + template : 'Settings/Notifications/NotificationCollectionViewTemplate', + + ui: { + 'addCard': '.x-add-card' + }, + + events: { + 'click .x-add-card': '_openSchemaModal' + }, + + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } + }); +}); diff --git a/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html b/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html new file mode 100644 index 000000000..512f1b422 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html @@ -0,0 +1,16 @@ +<fieldset> + <legend>Connections</legend> + <div class="row"> + <div class="col-md-12"> + <ul class="notification-list thingies"> + <li> + <div class="notification-item thingy add-card x-add-card"> + <span class="center well"> + <i class="icon-plus" title="Add Connection"/> + </span> + </div> + </li> + </ul> + </div> + </div> +</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationEditView.js b/src/UI/Settings/Notifications/NotificationEditView.js deleted file mode 100644 index a11c507f3..000000000 --- a/src/UI/Settings/Notifications/NotificationEditView.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -define( - [ - 'vent', - 'AppLayout', - 'marionette', - 'Settings/Notifications/DeleteView', - 'Commands/CommandController', - 'Mixins/AsModelBoundView', - 'underscore', - 'Form/FormBuilder' - - ], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, _) { - - var model = Marionette.ItemView.extend({ - template: 'Settings/Notifications/NotificationEditViewTemplate', - - ui: { - onDownloadToggle: '.x-on-download', - onUpgradeSection: '.x-on-upgrade' - }, - - events: { - 'click .x-save' : '_saveClient', - 'click .x-save-and-add': '_saveAndAddNotification', - 'click .x-delete' : '_deleteNotification', - 'click .x-back' : '_back', - 'click .x-test' : '_test', - 'click .x-cancel' : '_cancel', - 'change .x-on-download': '_onDownloadChanged' - }, - - initialize: function (options) { - this.notificationCollection = options.notificationCollection; - }, - - onRender: function () { - this._onDownloadChanged(); - }, - - _saveClient: function () { - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.notificationCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - } - }, - - _saveAndAddNotification: function () { - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.notificationCollection.add(self.model, { merge: true }); - - require('Settings/Notifications/SchemaModal').open(self.notificationCollection); - }); - } - }, - - _cancel: function () { - if (this.model.isNew()) { - this.model.destroy(); - } - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _deleteNotification: function () { - var view = new DeleteView({ model: this.model }); - AppLayout.modalRegion.show(view); - }, - - _back: function () { - if (this.model.isNew()) { - this.model.destroy(); - } - - require('Settings/Notifications/SchemaModal').open(this.notificationCollection); - }, - - _test: function () { - var testCommand = 'test{0}'.format(this.model.get('implementation')); - var properties = {}; - - _.each(this.model.get('fields'), function (field) { - properties[field.name] = field.value; - }); - - CommandController.Execute(testCommand, properties); - }, - - _onDownloadChanged: function () { - var checked = this.ui.onDownloadToggle.prop('checked'); - - if (checked) { - this.ui.onUpgradeSection.show(); - } - - else { - this.ui.onUpgradeSection.hide(); - } - } - }); - - return AsModelBoundView.call(model); - }); diff --git a/src/UI/Settings/Notifications/NotificationsItemView.js b/src/UI/Settings/Notifications/NotificationItemView.js similarity index 65% rename from src/UI/Settings/Notifications/NotificationsItemView.js rename to src/UI/Settings/Notifications/NotificationItemView.js index 3cde28bf9..c91a00adb 100644 --- a/src/UI/Settings/Notifications/NotificationsItemView.js +++ b/src/UI/Settings/Notifications/NotificationItemView.js @@ -3,8 +3,7 @@ define([ 'AppLayout', 'marionette', - 'Settings/Notifications/NotificationEditView' - + 'Settings/Notifications/Edit/NotificationEditView' ], function (AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ @@ -12,15 +11,15 @@ define([ tagName : 'li', events: { - 'click' : '_editNotification' + 'click' : '_edit' }, initialize: function () { this.listenTo(this.model, 'sync', this.render); }, - _editNotification: function () { - var view = new EditView({ model: this.model, notificationCollection: this.model.collection}); + _edit: function () { + var view = new EditView({ model: this.model, targetCollection: this.model.collection}); AppLayout.modalRegion.show(view); } }); diff --git a/src/UI/Settings/Notifications/Model.js b/src/UI/Settings/Notifications/NotificationModel.js similarity index 74% rename from src/UI/Settings/Notifications/Model.js rename to src/UI/Settings/Notifications/NotificationModel.js index b384b0c3d..9eb8e5552 100644 --- a/src/UI/Settings/Notifications/Model.js +++ b/src/UI/Settings/Notifications/NotificationModel.js @@ -1,6 +1,7 @@ 'use strict'; define([ - 'Settings/SettingsModelBase'], function (ModelBase) { + 'Settings/SettingsModelBase' +], function (ModelBase) { return ModelBase.extend({ successMessage: 'Notification Saved', diff --git a/src/UI/Settings/Notifications/SchemaModal.js b/src/UI/Settings/Notifications/SchemaModal.js deleted file mode 100644 index 923072ec4..000000000 --- a/src/UI/Settings/Notifications/SchemaModal.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; -define([ - 'AppLayout', - 'Settings/Notifications/Collection', - 'Settings/Notifications/AddView' -], function (AppLayout, NotificationCollection, AddSelectionNotificationView) { - return ({ - - open: function (collection) { - var schemaCollection = new NotificationCollection(); - var orginalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = orginalUrl; - - var view = new AddSelectionNotificationView({ collection: schemaCollection, notificationCollection: collection}); - AppLayout.modalRegion.show(view); - } - }); -}); diff --git a/src/UI/Settings/Notifications/notifications.less b/src/UI/Settings/Notifications/notifications.less index 3a20ab1bf..01fd71567 100644 --- a/src/UI/Settings/Notifications/notifications.less +++ b/src/UI/Settings/Notifications/notifications.less @@ -25,7 +25,7 @@ } .add-notifications { - li { + li.add-thingy-item { width: 40%; } } \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js index 003d6520d..d5a3f9d1a 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -9,7 +9,7 @@ define( ], function (Marionette, AsModelBoundView, fileSize) { var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Definition/QualityDefinitionTemplate', + template: 'Settings/Quality/Definition/QualityDefinitionViewTemplate', className: 'row', ui: { @@ -61,6 +61,14 @@ define( } { + if (maxSize === 0) + { + this.ui.thirtyMinuteMaxSize.html('Unlimited'); + this.ui.sixtyMinuteMaxSize.html('Unlimited'); + + return; + } + var maxBytes = maxSize * 1024 * 1024; var maxThirty = fileSize(maxBytes * 30, 1, false); var maxSixty = fileSize(maxBytes * 60, 1, false); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionViewTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html rename to src/UI/Settings/Quality/Definition/QualityDefinitionViewTemplate.html diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index c8066d71b..eddb56ecc 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -12,12 +12,12 @@ define( 'Settings/MediaManagement/MediaManagementSettingsModel', 'Settings/Quality/QualityLayout', 'Settings/Indexers/IndexerLayout', - 'Settings/Indexers/Collection', + 'Settings/Indexers/IndexerCollection', 'Settings/Indexers/IndexerSettingsModel', 'Settings/DownloadClient/DownloadClientLayout', 'Settings/DownloadClient/DownloadClientSettingsModel', - 'Settings/Notifications/CollectionView', - 'Settings/Notifications/Collection', + 'Settings/Notifications/NotificationCollectionView', + 'Settings/Notifications/NotificationCollection', 'Settings/Metadata/MetadataLayout', 'Settings/General/GeneralView', 'Shared/LoadingView', @@ -93,7 +93,6 @@ define( this.mediaManagementSettings = new MediaManagementSettingsModel(); this.namingSettings = new NamingModel(); this.indexerSettings = new IndexerSettingsModel(); - this.indexerCollection = new IndexerCollection(); this.downloadClientSettings = new DownloadClientSettingsModel(); this.notificationCollection = new NotificationCollection(); this.generalSettings = new GeneralSettingsModel(); @@ -102,7 +101,6 @@ define( this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), - this.indexerCollection.fetch(), this.downloadClientSettings.fetch(), this.notificationCollection.fetch(), this.generalSettings.fetch() @@ -112,7 +110,7 @@ define( self.loading.$el.hide(); self.mediaManagement.show(new MediaManagementLayout({ settings: self.mediaManagementSettings, namingSettings: self.namingSettings })); self.quality.show(new QualityLayout()); - self.indexers.show(new IndexerLayout({ settings: self.indexerSettings, indexersCollection: self.indexerCollection })); + self.indexers.show(new IndexerLayout({ model: self.indexerSettings })); self.downloadClient.show(new DownloadClientLayout({ model: self.downloadClientSettings })); self.notifications.show(new NotificationCollectionView({ collection: self.notificationCollection })); self.metadata.show(new MetadataLayout()); diff --git a/src/UI/Settings/ThingyAddCollectionView.js b/src/UI/Settings/ThingyAddCollectionView.js new file mode 100644 index 000000000..eb0a56e60 --- /dev/null +++ b/src/UI/Settings/ThingyAddCollectionView.js @@ -0,0 +1,18 @@ +'use strict'; + +define([ + 'marionette' +], function (Marionette) { + + return Marionette.CompositeView.extend({ + itemViewOptions : function () { + return { + targetCollection: this.targetCollection || this.options.targetCollection + }; + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + } + }); +}); diff --git a/src/UI/Settings/ThingyHeaderGroupView.js b/src/UI/Settings/ThingyHeaderGroupView.js new file mode 100644 index 000000000..aec24a3a1 --- /dev/null +++ b/src/UI/Settings/ThingyHeaderGroupView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'backbone', + 'marionette' +], function (Backbone, Marionette) { + + return Marionette.CompositeView.extend({ + itemViewContainer: '.item-list', + template: 'Settings/ThingyHeaderGroupViewTemplate', + tagName : 'div', + + itemViewOptions: function () { + return { + targetCollection: this.targetCollection || this.options.targetCollection + }; + }, + + initialize: function () { + this.collection = new Backbone.Collection(this.model.get('collection')); + } + }); +}); diff --git a/src/UI/Settings/ThingyHeaderGroupViewTemplate.html b/src/UI/Settings/ThingyHeaderGroupViewTemplate.html new file mode 100644 index 000000000..c3c233e52 --- /dev/null +++ b/src/UI/Settings/ThingyHeaderGroupViewTemplate.html @@ -0,0 +1,2 @@ +<legend>{{header}}</legend> +<ul class="item-list" /> \ No newline at end of file diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less index a9ccce814..e2348b8c0 100644 --- a/src/UI/Settings/thingy.less +++ b/src/UI/Settings/thingy.less @@ -7,30 +7,23 @@ font-size: 24px; font-weight: lighter; text-align: center; - - a { - font-size: 16px; - color: #595959; - - i { - .clickable; - } - } - - a:hover { - text-decoration: none; - } + height: 85px; } .add-thingies { text-align: center; - .items { + legend { + text-align: left; + text-transform: capitalize; + } + + ul.items { list-style-type: none; margin: 0px; padding: 0px; - li { + li.add-thingy-item { display: inline-block; vertical-align: top; } diff --git a/src/UI/Shared/ControlPanel/ControlPanelController.js b/src/UI/Shared/ControlPanel/ControlPanelController.js index 4e2a1100c..d09a34a4c 100644 --- a/src/UI/Shared/ControlPanel/ControlPanelController.js +++ b/src/UI/Shared/ControlPanel/ControlPanelController.js @@ -9,15 +9,15 @@ define( return Marionette.AppRouter.extend({ initialize: function () { - vent.on(vent.Commands.OpenControlPanelCommand, this._openControlPanel, this); - vent.on(vent.Commands.CloseControlPanelCommand, this._closeControlPanel, this); + vent.on(vent.Commands.OpenControlPanelCommand, this._openModal, this); + vent.on(vent.Commands.CloseControlPanelCommand, this._closeModal, this); }, - _openControlPanel: function (view) { + _openModal: function (view) { AppLayout.controlPanelRegion.show(view); }, - _closeControlPanel: function () { + _closeModal: function () { AppLayout.controlPanelRegion.closePanel(); } }); diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js index 51e93cf5a..aaf51b1e1 100644 --- a/src/UI/Shared/FormatHelpers.js +++ b/src/UI/Shared/FormatHelpers.js @@ -10,6 +10,11 @@ define( bytes: function (sourceSize) { var size = Number(sourceSize); + + if (isNaN(size)) { + return ''; + } + return Filesize(size, { base: 2, round: 1 }); }, diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index 9fcedea9a..177092de3 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -15,8 +15,8 @@ define( return Marionette.AppRouter.extend({ initialize: function () { - vent.on(vent.Commands.OpenModalCommand, this._openControlPanel, this); - vent.on(vent.Commands.CloseModalCommand, this._closeControlPanel, this); + vent.on(vent.Commands.OpenModalCommand, this._openModal, this); + vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); @@ -25,12 +25,12 @@ define( vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); }, - _openControlPanel: function (view) { + _openModal: function (view) { AppLayout.modalRegion.show(view); }, - _closeControlPanel: function () { - AppLayout.modalRegion.closePanel(); + _closeModal: function () { + AppLayout.modalRegion.closeModal(); }, _editSeries: function (options) { diff --git a/src/UI/Shared/Modal/ModalRegion.js b/src/UI/Shared/Modal/ModalRegion.js index 5aef6748e..a39f73fe4 100644 --- a/src/UI/Shared/Modal/ModalRegion.js +++ b/src/UI/Shared/Modal/ModalRegion.js @@ -11,7 +11,7 @@ define( constructor: function () { Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showPanel, this); + this.on('show', this.showModal, this); }, getEl: function (selector) { @@ -20,7 +20,7 @@ define( return $el; }, - showPanel: function () { + showModal: function () { this.$el.addClass('modal fade'); //need tab index so close on escape works @@ -32,7 +32,7 @@ define( 'backdrop': 'static'}); }, - closePanel: function () { + closeModal: function () { $(this.el).modal('hide'); this.reset(); } diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index fe67f68cf..a72863d5a 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -34,6 +34,7 @@ define( if (this.model.get('tooltip')) { this.$el.attr('title', this.model.get('tooltip')); + this.$el.attr('data-container', 'body'); } }, diff --git a/src/UI/System/Info/Health/HealthLayout.js b/src/UI/System/Info/Health/HealthLayout.js index 2a0a44461..935087741 100644 --- a/src/UI/System/Info/Health/HealthLayout.js +++ b/src/UI/System/Info/Health/HealthLayout.js @@ -5,8 +5,9 @@ define( 'backgrid', 'Health/HealthCollection', 'System/Info/Health/HealthCell', + 'System/Info/Health/HealthWikiCell', 'System/Info/Health/HealthOkView' - ], function (Marionette, Backgrid, HealthCollection, HealthCell, HealthOkView) { + ], function (Marionette, Backgrid, HealthCollection, HealthCell, HealthWikiCell, HealthOkView) { return Marionette.Layout.extend({ template: 'System/Info/Health/HealthLayoutTemplate', @@ -19,12 +20,20 @@ define( { name: 'type', label: '', - cell: HealthCell + cell: HealthCell, + sortable: false }, { name: 'message', label: 'Message', - cell: 'string' + cell: 'string', + sortable: false + }, + { + name: 'wikiUrl', + label: '', + cell: HealthWikiCell, + sortable: false } ], diff --git a/src/UI/System/Info/Health/HealthWikiCell.js b/src/UI/System/Info/Health/HealthWikiCell.js new file mode 100644 index 000000000..e6efd8c22 --- /dev/null +++ b/src/UI/System/Info/Health/HealthWikiCell.js @@ -0,0 +1,29 @@ +'use strict'; +define( + [ + 'jquery', + 'backgrid' + ], function ($, Backgrid) { + return Backgrid.UriCell.extend({ + + className: 'wiki-link-cell', + + title: 'Read the Wiki for more information', + + text: 'Wiki', + + render: function () { + this.$el.empty(); + var rawValue = this.model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, this.model); + this.$el.append($("<a>", { + tabIndex: -1, + href: rawValue, + title: this.title || formattedValue, + target: this.target + }).text(this.text)); + this.delegateEvents(); + return this; + } + }); + }); diff --git a/src/UI/System/Update/UpdateItemViewTemplate.html b/src/UI/System/Update/UpdateItemViewTemplate.html index 95b56de51..408ec669b 100644 --- a/src/UI/System/Update/UpdateItemViewTemplate.html +++ b/src/UI/System/Update/UpdateItemViewTemplate.html @@ -3,12 +3,14 @@ <legend>{{version}} <span class="date"> - {{ShortDate releaseDate}} - {{#if installed}}<i class="icon-ok" title="Installed"></i>{{/if}} - - {{#if isUpgrade}} - <span class="label label-default install-update x-install-update">Install</span> - {{/if}} </span> + {{#if installed}} + <span class="update-installed"><i class="icon-ok" title="Installed"></i></span> + {{/if}} + + {{#if isUpgrade}} + <span class="label label-default install-update x-install-update">Install</span> + {{/if}} </legend> {{#with changes}} diff --git a/src/UI/System/Update/update.less b/src/UI/System/Update/update.less index 48c138f40..6d4e8e944 100644 --- a/src/UI/System/Update/update.less +++ b/src/UI/System/Update/update.less @@ -4,11 +4,21 @@ margin-bottom: 30px; legend { - margin-bottom: 5px; - line-height: 30px; + cursor : default; + margin-bottom : 5px; + line-height : 30px; .date { - font-size: 16px; + font-size : 16px; + } + + .install-update { + .clickable(); + margin-left : 10px; + } + + .update-installed { + margin-left : 10px; } } @@ -25,11 +35,6 @@ font-size: 13px; } - .install-update { - .clickable(); - margin-left: 10px; - } - a { color: white; text-decoration: none; diff --git a/src/UI/jQuery/RouteBinder.js b/src/UI/jQuery/RouteBinder.js index f4b541102..e3e85c068 100644 --- a/src/UI/jQuery/RouteBinder.js +++ b/src/UI/jQuery/RouteBinder.js @@ -48,13 +48,15 @@ define( throw 'couldn\'t find route target'; } - if (!href.startsWith('http')) { var relativeHref = href.replace(StatusModel.get('urlBase'), ''); Backbone.history.navigate(relativeHref, { trigger: true }); } - + else if (href.contains('#')) { + //Open in new tab without dereferer (since it doesn't support fragments) + window.open(href, '_blank'); + } else { //Open in new tab window.open('http://www.dereferer.org/?' + encodeURI(href), '_blank'); diff --git a/src/UI/jQuery/jquery.validation.js b/src/UI/jQuery/jquery.validation.js index 1b180c588..210f94d69 100644 --- a/src/UI/jQuery/jquery.validation.js +++ b/src/UI/jQuery/jquery.validation.js @@ -9,14 +9,13 @@ define( var validationName = error.propertyName.toLowerCase(); this.find('.validation-errors') - .addClass('alert alert-error') + .addClass('alert alert-danger') .append('<div><i class="icon-exclamation-sign"></i>' + error.errorMessage + '</div>'); var input = this.find('[name]').filter(function () { return this.name.toLowerCase() === validationName; }); - if (input.length === 0) { input = this.find('[validation-name]').filter(function () { return $(this).attr('validation-name').toLowerCase() === validationName; @@ -58,12 +57,15 @@ define( }; $.fn.addFormError = function (error) { - this.find('.form-group').parent().prepend('<div class="alert alert-error validation-error">' + error.errorMessage + '</div>'); + var t1 = this.find('.form-horizontal'); + var t2 = this.find('.form-horizontal').parent(); + + this.prepend('<div class="alert alert-danger validation-error">' + error.errorMessage + '</div>'); }; $.fn.removeAllErrors = function () { this.find('.error').removeClass('error'); - this.find('.validation-errors').removeClass('alert').removeClass('alert-error').html(''); + this.find('.validation-errors').removeClass('alert').removeClass('alert-danger').html(''); this.find('.validation-error').remove(); return this.find('.help-inline.error-message').remove(); };