From 257cdf938212fbb2d0485a65e3c5a731d337f70b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 5 Jul 2014 16:21:44 +0200 Subject: [PATCH] New: It is now possible to use Completed Download Handling with remote download clients by specifying the local mount in settings. --- .../DiskProviderTests/FreeSpaceFixtureBase.cs | 8 --- .../NzbgetTests/NzbgetFixture.cs | 57 ++++++++++++++++-- .../SabnzbdTests/SabnzbdFixture.cs | 58 ++++++++++++++++++- .../Download/Clients/Nzbget/Nzbget.cs | 39 +++++++++++-- .../Download/Clients/Nzbget/NzbgetSettings.cs | 13 ++++- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 39 +++++++++++-- .../Clients/Sabnzbd/SabnzbdSettings.cs | 14 +++-- .../Download/DownloadClientBase.cs | 22 ++++++- 8 files changed, 218 insertions(+), 32 deletions(-) diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs index 098456276..d59cb37e5 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs @@ -26,14 +26,6 @@ namespace NzbDrone.Common.Test.DiskProviderTests Subject.GetAvailableSpace(Path.Combine(path, "invalidFolder")).Should().NotBe(0); } - [Test] - public void should_get_free_space_for_drive_that_doesnt_exist() - { - WindowsOnly(); - - Assert.Throws(() => Subject.GetAvailableSpace("J:\\").Should().NotBe(0)); - } - [Test] public void should_be_able_to_check_space_on_ramdrive() { diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs index 0dffee397..15e90536a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -1,17 +1,18 @@ using System; using System.IO; using System.Linq; +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Core.Tv; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Parser.Model; +using NzbDrone.Test.Common; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using System.Collections.Generic; namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests { @@ -28,7 +29,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new NzbgetSettings { - Host = "192.168.5.55", + Host = "127.0.0.1", Port = 2222, Username = "admin", Password = "pass", @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests FileSizeLo = 1000, Category = "tv", Name = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", - DestDir = "somedirectory", + DestDir = "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", Parameters = new List { new NzbgetParameter { Name = "drone", Value = "id" } }, ParStatus = "SUCCESS", UnpackStatus = "NONE", @@ -81,6 +82,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests { DownloadRate = 7000000 }); + + var configItems = new Dictionary(); + configItems.Add("Category1.Name", "tv"); + configItems.Add("Category1.DestDir", @"/remote/mount/tv"); + + Mocker.GetMock() + .Setup(v => v.GetConfig(It.IsAny())) + .Returns(configItems); + } + + protected void WithMountPoint(String mountPath) + { + (Subject.Definition.Settings as NzbgetSettings).TvCategoryLocalPath = mountPath; } protected void WithFailedDownload() @@ -223,5 +237,40 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests items.Should().BeEmpty(); } + + [Test] + public void should_return_status_with_outputdir() + { + var result = Subject.GetStatus(); + + result.IsLocalhost.Should().BeTrue(); + result.OutputRootFolders.Should().NotBeNull(); + result.OutputRootFolders.First().Should().Be(@"/remote/mount/tv"); + } + + [Test] + public void should_return_status_with_mounted_outputdir() + { + WithMountPoint(@"O:\mymount".AsOsAgnostic()); + + var result = Subject.GetStatus(); + + result.IsLocalhost.Should().BeTrue(); + result.OutputRootFolders.Should().NotBeNull(); + result.OutputRootFolders.First().Should().Be(@"O:\mymount".AsOsAgnostic()); + } + + [Test] + public void should_remap_storage_if_mounted() + { + WithMountPoint(@"O:\mymount".AsOsAgnostic()); + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 3fd83618e..455b9d4b7 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new SabnzbdSettings { - Host = "192.168.5.55", + Host = "127.0.0.1", Port = 2222, ApiKey = "5c770e3197e4fe763423ee7c392c25d1", Username = "admin", @@ -82,10 +82,29 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests Category = "tv", Id = "sabnzbd_nzb12345", Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", - Storage = "somedirectory" + Storage = "/remote/mount/vv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE" } } }; + + Mocker.GetMock() + .Setup(s => s.GetConfig(It.IsAny())) + .Returns(new SabnzbdConfig + { + Misc = new SabnzbdConfigMisc + { + complete_dir = "/remote/mount/" + }, + Categories = new List + { + new SabnzbdCategory { Name = "tv", Dir = "vv" } + } + }); + } + + protected void WithMountPoint(String mountPath) + { + (Subject.Definition.Settings as SabnzbdSettings).TvCategoryLocalPath = mountPath; } protected void WithFailedDownload() @@ -269,6 +288,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests result.OutputPath.Should().Be(@"C:\sorted\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); } + [Test] + public void should_remap_storage_if_mounted() + { + WithMountPoint(@"O:\mymount".AsOsAgnostic()); + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + } + [Test] public void should_not_blow_up_if_storage_is_drive_root() { @@ -281,5 +313,27 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests result.OutputPath.Should().Be(@"C:\".AsOsAgnostic()); } + + [Test] + public void should_return_status_with_outputdir() + { + var result = Subject.GetStatus(); + + result.IsLocalhost.Should().BeTrue(); + result.OutputRootFolders.Should().NotBeNull(); + result.OutputRootFolders.First().Should().Be(@"/remote/mount/vv"); + } + + [Test] + public void should_return_status_with_mounted_outputdir() + { + WithMountPoint(@"O:\mymount".AsOsAgnostic()); + + var result = Subject.GetStatus(); + + result.IsLocalhost.Should().BeTrue(); + result.OutputRootFolders.Should().NotBeNull(); + result.OutputRootFolders.First().Should().Be(@"O:\mymount".AsOsAgnostic()); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 49983269d..4df64d48a 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -189,14 +189,36 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public override IEnumerable GetItems() { + Dictionary config = null; + NzbgetCategory category = null; + try + { + if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) + { + config = _proxy.GetConfig(Settings); + category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); + } + } + catch (DownloadClientException ex) + { + _logger.ErrorException(ex.Message, ex); + yield break; + } + foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) { - if (downloadClientItem.Category != Settings.TvCategory) continue; + if (downloadClientItem.Category == Settings.TvCategory) + { + if (category != null) + { + RemapStorage(downloadClientItem, category.DestDir, Settings.TvCategoryLocalPath); + } - downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); - if (downloadClientItem.RemoteEpisode == null) continue; + downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); + if (downloadClientItem.RemoteEpisode == null) continue; - yield return downloadClientItem; + yield return downloadClientItem; + } } } @@ -223,7 +245,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget if (category != null) { - status.OutputRootFolders = new List { category.DestDir }; + if (Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) + { + status.OutputRootFolders = new List { category.DestDir }; + } + else + { + status.OutputRootFolders = new List { Settings.TvCategoryLocalPath }; + } } return status; diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 57f79bfb4..83d72e2e4 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -3,6 +3,7 @@ using FluentValidation; using FluentValidation.Results; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Core.Download.Clients.Nzbget { @@ -14,6 +15,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget RuleFor(c => c.Port).GreaterThan(0); RuleFor(c => c.Username).NotEmpty().When(c => !String.IsNullOrWhiteSpace(c.Password)); RuleFor(c => c.Password).NotEmpty().When(c => !String.IsNullOrWhiteSpace(c.Username)); + + RuleFor(c => c.TvCategory).NotEmpty().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath)); + RuleFor(c => c.TvCategoryLocalPath).IsValidPath().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath)); } } @@ -45,13 +49,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)] public String TvCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(5, Label = "Category Local Path", Type = FieldType.Textbox, Advanced = true, HelpText = "Local path to the category output dir. Useful if Nzbget runs on another computer.")] + public String TvCategoryLocalPath { get; set; } + + [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] public Int32 RecentTvPriority { get; set; } - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] public Int32 OlderTvPriority { get; set; } - [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)] public Boolean UseSsl { get; set; } public ValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 3e5a1e812..420deced7 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -182,14 +182,36 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public override IEnumerable GetItems() { + SabnzbdConfig config = null; + SabnzbdCategory category = null; + try + { + if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) + { + config = _proxy.GetConfig(Settings); + category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); + } + } + catch (DownloadClientException ex) + { + _logger.ErrorException(ex.Message, ex); + yield break; + } + foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) { - if (downloadClientItem.Category != Settings.TvCategory) continue; + if (downloadClientItem.Category == Settings.TvCategory) + { + if (category != null) + { + RemapStorage(downloadClientItem, category.FullPath, Settings.TvCategoryLocalPath); + } - downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); - if (downloadClientItem.RemoteEpisode == null) continue; + downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); + if (downloadClientItem.RemoteEpisode == null) continue; - yield return downloadClientItem; + yield return downloadClientItem; + } } } @@ -268,7 +290,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (category != null) { - status.OutputRootFolders = new List { category.FullPath }; + if (Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) + { + status.OutputRootFolders = new List { category.FullPath }; + } + else + { + status.OutputRootFolders = new List { Settings.TvCategoryLocalPath }; + } } return status; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 749cd2ed1..75d6e918b 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -3,6 +3,7 @@ using FluentValidation; using FluentValidation.Results; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Core.Download.Clients.Sabnzbd { @@ -21,10 +22,12 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd .WithMessage("Username is required when API key is not configured") .When(c => String.IsNullOrWhiteSpace(c.ApiKey)); - RuleFor(c => c.Password).NotEmpty() .WithMessage("Password is required when API key is not configured") .When(c => String.IsNullOrWhiteSpace(c.ApiKey)); + + RuleFor(c => c.TvCategory).NotEmpty().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath)); + RuleFor(c => c.TvCategoryLocalPath).IsValidPath().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath)); } } @@ -59,13 +62,16 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)] public String TvCategory { get; set; } - [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(6, Label = "Category Local Path", Type = FieldType.Textbox, Advanced = true, HelpText = "Local path to the category output dir. Useful if Sabnzbd runs on another computer.")] + public String TvCategoryLocalPath { get; set; } + + [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] public Int32 RecentTvPriority { get; set; } - [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] public Int32 OlderTvPriority { get; set; } - [FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)] public Boolean UseSsl { get; set; } public ValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index cf609060a..7ff6d4c13 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,6 +1,8 @@ using System; +using System.IO; +using System.Linq; using System.Collections.Generic; -using FluentValidation.Results; +using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; @@ -8,6 +10,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Configuration; using NLog; +using FluentValidation.Results; namespace NzbDrone.Core.Download { @@ -81,6 +84,23 @@ namespace NzbDrone.Core.Download return remoteEpisode; } + protected void RemapStorage(DownloadClientItem downloadClientItem, String remotePath, String localPath) + { + if (downloadClientItem.OutputPath.IsNullOrWhiteSpace() || localPath.IsNullOrWhiteSpace()) + { + return; + } + + remotePath = remotePath.TrimEnd('/', '\\'); + localPath = localPath.TrimEnd('/', '\\'); + + if (downloadClientItem.OutputPath.StartsWith(remotePath)) + { + downloadClientItem.OutputPath = localPath + downloadClientItem.OutputPath.Substring(remotePath.Length); + downloadClientItem.OutputPath = downloadClientItem.OutputPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + } + protected ValidationFailure TestFolder(String folder, String propertyName, Boolean mustBeWritable = true) { if (!_diskProvider.FolderExists(folder))