diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 33f3953e8..bdba4dfab 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -96,6 +96,8 @@ + + diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs new file mode 100644 index 000000000..11d694f1c --- /dev/null +++ b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Api.Mapping; +using NzbDrone.Common; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation.Paths; +using Omu.ValueInjecter; + +namespace NzbDrone.Api.Config +{ + public class RemotePathMappingModule : NzbDroneRestModule + { + private readonly IRemotePathMappingService _remotePathMappingService; + + public RemotePathMappingModule(IConfigService configService, IRemotePathMappingService remotePathMappingService, PathExistsValidator pathExistsValidator) + { + _remotePathMappingService = remotePathMappingService; + + GetResourceAll = GetMappings; + GetResourceById = GetMappingById; + CreateResource = CreateMapping; + DeleteResource = DeleteMapping; + UpdateResource = UpdateMapping; + + SharedValidator.RuleFor(c => c.Host) + .NotEmpty(); + + // We cannot use IsValidPath here, because it's a remote path, possibly other OS. + SharedValidator.RuleFor(c => c.RemotePath) + .NotEmpty(); + + SharedValidator.RuleFor(c => c.LocalPath) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(pathExistsValidator); + } + + private RemotePathMappingResource GetMappingById(int id) + { + return _remotePathMappingService.Get(id).InjectTo(); + } + + private int CreateMapping(RemotePathMappingResource rootFolderResource) + { + return GetNewId(_remotePathMappingService.Add, rootFolderResource); + } + + private List GetMappings() + { + return ToListResource(_remotePathMappingService.All); + } + + private void DeleteMapping(int id) + { + _remotePathMappingService.Remove(id); + } + + private void UpdateMapping(RemotePathMappingResource resource) + { + var mapping = _remotePathMappingService.Get(resource.Id); + + mapping.InjectFrom(resource); + + _remotePathMappingService.Update(mapping); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs new file mode 100644 index 000000000..85a4fb5ac --- /dev/null +++ b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs @@ -0,0 +1,12 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Config +{ + public class RemotePathMappingResource : RestResource + { + public String Host { get; set; } + public String RemotePath { get; set; } + public String LocalPath { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 8113a5012..96f965b2f 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -11,6 +11,8 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; using NzbDrone.Core.Download; using NzbDrone.Core.Configuration; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Test.Download.DownloadClientTests { @@ -29,13 +31,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests Mocker.GetMock() .Setup(s => s.Map(It.IsAny(), It.IsAny(), null)) - .Returns(CreateRemoteEpisode()); - + .Returns(() => CreateRemoteEpisode()); Mocker.GetMock() .Setup(s => s.Get(It.IsAny())) .Returns(r => new HttpResponse(r, new HttpHeader(), new Byte[0])); + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal(It.IsAny(), It.IsAny())) + .Returns((h,r) => r); } protected virtual RemoteEpisode CreateRemoteEpisode() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs index 08d269339..0105f26b1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Parser.Model; using NzbDrone.Test.Common; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests { @@ -92,11 +93,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests .Returns(configItems); } - protected void GivenMountPoint(String mountPath) - { - (Subject.Definition.Settings as NzbgetSettings).TvCategoryLocalPath = mountPath; - } - protected void GivenFailedDownload() { Mocker.GetMock() @@ -251,7 +247,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests [Test] public void should_return_status_with_mounted_outputdir() { - GivenMountPoint(@"O:\mymount".AsOsAgnostic()); + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv")) + .Returns(@"O:\mymount".AsOsAgnostic()); var result = Subject.GetStatus(); @@ -263,7 +261,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests [Test] public void should_remap_storage_if_mounted() { - GivenMountPoint(@"O:\mymount".AsOsAgnostic()); + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE")) + .Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); GivenQueue(null); GivenHistory(_completed); diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index c12623df0..025caa91b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests { @@ -106,11 +107,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests .Returns(_config); } - protected void GivenMountPoint(String mountPath) - { - (Subject.Definition.Settings as SabnzbdSettings).TvCategoryLocalPath = mountPath; - } - protected void GivenFailedDownload() { Mocker.GetMock() @@ -303,7 +299,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests [Test] public void should_remap_storage_if_mounted() { - GivenMountPoint(@"O:\mymount".AsOsAgnostic()); + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE")) + .Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); GivenQueue(null); GivenHistory(_completed); @@ -361,7 +359,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests [Test] public void should_return_status_with_mounted_outputdir() { - GivenMountPoint(@"O:\mymount".AsOsAgnostic()); + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv")) + .Returns(@"O:\mymount".AsOsAgnostic()); GivenQueue(null); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index d9fd547e2..7165062a6 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -197,6 +197,7 @@ + diff --git a/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs b/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs new file mode 100644 index 000000000..36ff6d577 --- /dev/null +++ b/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; +using FizzWare.NBuilder; + +namespace NzbDrone.Core.Test.RemotePathMappingsTests +{ + [TestFixture] + public class RemotePathMappingServiceFixture : CoreTest + { + [SetUp] + public void Setup() + { + Mocker.GetMock() + .Setup(m => m.FolderExists(It.IsAny())) + .Returns(true); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(new List()); + } + + private void GivenMapping() + { + var mappings = Builder.CreateListOfSize(1) + .All() + .With(v => v.Host = "my-server.localdomain") + .With(v => v.RemotePath = "/mnt/storage/") + .With(v => v.LocalPath = @"D:\mountedstorage\".AsOsAgnostic()) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(mappings); + } + + private void WithNonExistingFolder() + { + Mocker.GetMock() + .Setup(m => m.FolderExists(It.IsAny())) + .Returns(false); + } + + [TestCase("my-first-server.localdomain", "/mnt/storage", @"D:\storage1")] + [TestCase("my-server.localdomain", "/mnt/storage2", @"D:\storage2")] + public void should_be_able_to_add_new_mapping(String host, String remotePath, String localPath) + { + GivenMapping(); + + localPath = localPath.AsOsAgnostic(); + + var mapping = new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath }; + + Subject.Add(mapping); + + Mocker.GetMock().Verify(c => c.Insert(mapping), Times.Once()); + } + + [Test] + public void should_be_able_to_remove_mapping() + { + Subject.Remove(1); + Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); + } + + [TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage")] + [TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage2")] + public void adding_duplicated_mapping_should_throw(String host, String remotePath, String localPath) + { + localPath = localPath.AsOsAgnostic(); + + GivenMapping(); + + var mapping = new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath }; + + Assert.Throws(() => Subject.Add(mapping)); + } + + [TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")] + [TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")] + [TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")] + public void should_remap_remote_to_local(String host, String remotePath, String expectedLocalPath) + { + expectedLocalPath = expectedLocalPath.AsOsAgnostic(); + + GivenMapping(); + + var result = Subject.RemapRemoteToLocal(host, remotePath); + + result.Should().Be(expectedLocalPath); + } + + [TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")] + [TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage")] + [TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")] + [TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")] + public void should_remap_local_to_remote(String host, String expectedRemotePath, String localPath) + { + localPath = localPath.AsOsAgnostic(); + + GivenMapping(); + + var result = Subject.RemapLocalToRemote(host, localPath); + + result.Should().Be(expectedRemotePath); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/063_add_remotepathmappings.cs b/src/NzbDrone.Core/Datastore/Migration/063_add_remotepathmappings.cs new file mode 100644 index 000000000..2f8c6b755 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/063_add_remotepathmappings.cs @@ -0,0 +1,17 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(63)] + public class add_remotepathmappings : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("RemotePathMappings") + .WithColumn("Host").AsString() + .WithColumn("RemotePath").AsString() + .WithColumn("LocalPath").AsString(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 85836aa12..e7a4be08f 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -16,6 +16,7 @@ using NzbDrone.Core.Jobs; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Metadata; using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Notifications; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -87,6 +88,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("PendingReleases") .Ignore(e => e.RemoteEpisode); + + Mapper.Entity().RegisterModel("RemotePathMappings"); } private static void RegisterMappers() diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 5ecb18beb..c8bbc97dd 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Download.Clients.Nzbget { @@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, + IRemotePathMappingService remotePathMappingService, Logger logger) - : base(httpClient, configService, diskProvider, parsingService, logger) + : base(httpClient, configService, diskProvider, parsingService, remotePathMappingService, logger) { _proxy = proxy; } @@ -145,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); historyItem.Title = item.Name; historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); - historyItem.OutputPath = item.DestDir; + historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, item.DestDir); historyItem.Category = item.Category; 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; @@ -172,31 +174,12 @@ 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; - } + MigrateLocalCategoryPath(); foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) { if (downloadClientItem.Category == Settings.TvCategory) { - if (category != null) - { - RemapStorage(downloadClientItem, category.DestDir, Settings.TvCategoryLocalPath); - } - downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); if (downloadClientItem.RemoteEpisode == null) continue; @@ -230,14 +213,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget if (category != null) { - if (Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) - { - status.OutputRootFolders = new List { category.DestDir }; - } - else - { - status.OutputRootFolders = new List { Settings.TvCategoryLocalPath }; - } + status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.DestDir) }; } return status; @@ -279,11 +255,6 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { failures.AddIfNotNull(TestConnection()); failures.AddIfNotNull(TestCategory()); - - if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) - { - failures.AddIfNotNull(TestFolder(Settings.TvCategoryLocalPath, "TvCategoryLocalPath")); - } } private ValidationFailure TestConnection() @@ -333,5 +304,35 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return result; } + + // TODO: Remove around January 2015, this code moves the settings to the RemotePathMappingService. + private void MigrateLocalCategoryPath() + { + if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) + { + try + { + _logger.Debug("Has legacy TvCategoryLocalPath, trying to migrate to RemotePathMapping list."); + + var config = _proxy.GetConfig(Settings); + var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); + + if (category != null) + { + var localPath = Settings.TvCategoryLocalPath; + Settings.TvCategoryLocalPath = null; + + _remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.DestDir, localPath); + + _logger.Info("Discovered Local Category Path for {0}, the setting was automatically moved to the Remote Path Mapping table.", Definition.Name); + } + } + catch (DownloadClientException ex) + { + _logger.ErrorException("Unable to migrate local category path", ex); + throw; + } + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 83d72e2e4..e6370a3d9 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -49,16 +49,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)] public String TvCategory { get; set; } - [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.")] + // TODO: Remove around January 2015, this setting was superceded by the RemotePathMappingService, but has to remain for a while to properly migrate. 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")] + [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")] public Int32 RecentTvPriority { get; set; } - [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(6, 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(8, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)] public Boolean UseSsl { get; set; } public ValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index cc6d92c96..5c19f2e70 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -22,8 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, + IRemotePathMappingService remotePathMappingService, Logger logger) - : base(configService, diskProvider, parsingService, logger) + : base(configService, diskProvider, parsingService, remotePathMappingService, logger) { _httpClient = httpClient; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 7dbd1a7a1..f94bf72b4 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Download.Clients.Sabnzbd { @@ -25,8 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, + IRemotePathMappingService remotePathMappingService, Logger logger) - : base(httpClient, configService, diskProvider, parsingService, logger) + : base(httpClient, configService, diskProvider, parsingService, remotePathMappingService, logger) { _proxy = proxy; } @@ -147,11 +149,13 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd historyItem.Status = DownloadItemStatus.Downloading; } - if (!sabHistoryItem.Storage.IsNullOrWhiteSpace()) - { - historyItem.OutputPath = sabHistoryItem.Storage; + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, sabHistoryItem.Storage); - var parent = sabHistoryItem.Storage.GetParentPath(); + if (!outputPath.IsNullOrWhiteSpace()) + { + historyItem.OutputPath = outputPath; + + var parent = outputPath.GetParentPath(); while (parent != null) { if (Path.GetFileName(parent) == sabHistoryItem.Title) @@ -170,31 +174,12 @@ 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; - } + MigrateLocalCategoryPath(); foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) { if (downloadClientItem.Category == Settings.TvCategory) { - if (category != null) - { - RemapStorage(downloadClientItem, category.FullPath, Settings.TvCategoryLocalPath); - } - downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); if (downloadClientItem.RemoteEpisode == null) continue; @@ -323,14 +308,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (category != null) { - if (Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) - { - status.OutputRootFolders = new List { category.FullPath }; - } - else - { - status.OutputRootFolders = new List { Settings.TvCategoryLocalPath }; - } + status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; } return status; @@ -342,12 +320,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd failures.AddIfNotNull(TestAuthentication()); failures.AddIfNotNull(TestGlobalConfig()); failures.AddIfNotNull(TestCategory()); - - if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) - { - failures.AddIfNotNull(TestFolder(Settings.TvCategoryLocalPath, "TvCategoryLocalPath")); - failures.AddIfNotNull(TestCategoryLocalPath()); - } } private ValidationFailure TestConnection() @@ -447,14 +419,34 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return null; } - private ValidationFailure TestCategoryLocalPath() + private void MigrateLocalCategoryPath() { - if (Settings.Host == "127.0.0.1" || Settings.Host == "localhost") + // TODO: Remove around January 2015, this code moves the settings to the RemotePathMappingService. + if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace()) { - return new ValidationFailure("TvCategoryLocalPath", "Do not set when SABnzbd is running on the same system as NzbDrone"); - } + try + { + _logger.Debug("Has legacy TvCategoryLocalPath, trying to migrate to RemotePathMapping list."); - return null; + var config = _proxy.GetConfig(Settings); + var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); + + if (category != null) + { + var localPath = Settings.TvCategoryLocalPath; + Settings.TvCategoryLocalPath = null; + + _remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.FullPath, localPath); + + _logger.Info("Discovered Local Category Path for {0}, the setting was automatically moved to the Remote Path Mapping table.", Definition.Name); + } + } + catch (DownloadClientException ex) + { + _logger.ErrorException("Unable to migrate local category path", ex); + throw; + } + } } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 75d6e918b..932d0e00b 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -25,9 +25,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd 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)); } } @@ -62,16 +59,16 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)] public String TvCategory { get; set; } - [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.")] + // TODO: Remove around January 2015, this setting was superceded by the RemotePathMappingService, but has to remain for a while to properly migrate. 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")] + [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")] public Int32 RecentTvPriority { get; set; } - [FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(7, 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(9, 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/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index 2a7f20b80..584dde186 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.MediaFiles; using NLog; using Omu.ValueInjecter; using FluentValidation.Results; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Download.Clients.UsenetBlackhole { @@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, + IRemotePathMappingService remotePathMappingService, Logger logger) - : base(configService, diskProvider, parsingService, logger) + : base(configService, diskProvider, parsingService, remotePathMappingService, logger) { _diskScanService = diskScanService; _httpClient = httpClient; diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index e3b07e4dc..3a7f6a6ab 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Configuration; using NLog; using FluentValidation.Results; using NzbDrone.Core.Validation; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Download { @@ -21,6 +22,7 @@ namespace NzbDrone.Core.Download protected readonly IConfigService _configService; protected readonly IDiskProvider _diskProvider; protected readonly IParsingService _parsingService; + protected readonly IRemotePathMappingService _remotePathMappingService; protected readonly Logger _logger; public Type ConfigContract @@ -49,11 +51,12 @@ namespace NzbDrone.Core.Download } } - protected DownloadClientBase(IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, Logger logger) + protected DownloadClientBase(IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, IRemotePathMappingService remotePathMappingService, Logger logger) { _configService = configService; _diskProvider = diskProvider; _parsingService = parsingService; + _remotePathMappingService = remotePathMappingService; _logger = logger; } @@ -84,23 +87,6 @@ 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); - } - } - public ValidationResult Test() { var failures = new List(); diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index eec17ec23..0a684a398 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -15,6 +15,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Configuration; using NLog; +using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Core.Download { @@ -24,11 +25,12 @@ namespace NzbDrone.Core.Download protected readonly IHttpClient _httpClient; protected UsenetClientBase(IHttpClient httpClient, - IConfigService configService, - IDiskProvider diskProvider, - IParsingService parsingService, - Logger logger) - : base(configService, diskProvider, parsingService, logger) + IConfigService configService, + IDiskProvider diskProvider, + IParsingService parsingService, + IRemotePathMappingService remotePathMappingService, + Logger logger) + : base(configService, diskProvider, parsingService, remotePathMappingService, logger) { _httpClient = httpClient; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 728c53c9a..249a3b4b1 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -223,6 +223,7 @@ + @@ -560,6 +561,9 @@ + + + Code diff --git a/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs b/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs new file mode 100644 index 000000000..17b2c8692 --- /dev/null +++ b/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + + +namespace NzbDrone.Core.RemotePathMappings +{ + public class RemotePathMapping : ModelBase + { + public String Host { get; set; } + public String RemotePath { get; set; } + public String LocalPath { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingRepository.cs b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingRepository.cs new file mode 100644 index 000000000..3ebe7ba84 --- /dev/null +++ b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingRepository.cs @@ -0,0 +1,27 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.RemotePathMappings +{ + public interface IRemotePathMappingRepository : IBasicRepository + { + + } + + public class RemotePathMappingRepository : BasicRepository, IRemotePathMappingRepository + { + + public RemotePathMappingRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + protected override bool PublishModelEvents + { + get + { + return true; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs new file mode 100644 index 000000000..d993220e2 --- /dev/null +++ b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs @@ -0,0 +1,222 @@ +using System.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Tv; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Download; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.RemotePathMappings +{ + public interface IRemotePathMappingService + { + List All(); + RemotePathMapping Add(RemotePathMapping mapping); + void Remove(int id); + RemotePathMapping Get(int id); + RemotePathMapping Update(RemotePathMapping mapping); + + String RemapRemoteToLocal(String host, String remotePath); + String RemapLocalToRemote(String host, String localPath); + + // TODO: Remove around January 2015. Used to migrate legacy Local Category Path settings. + void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, String remotePath, String localPath); + } + + public class RemotePathMappingService : IRemotePathMappingService + { + // TODO: Remove DownloadClientRepository reference around January 2015. Used to migrate legacy Local Category Path settings. + private readonly IDownloadClientRepository _downloadClientRepository; + private readonly IRemotePathMappingRepository _remotePathMappingRepository; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + private readonly ICached> _cache; + + public RemotePathMappingService(IDownloadClientRepository downloadClientRepository, + IRemotePathMappingRepository remotePathMappingRepository, + IDiskProvider diskProvider, + ICacheManager cacheManager, + Logger logger) + { + _downloadClientRepository = downloadClientRepository; + _remotePathMappingRepository = remotePathMappingRepository; + _diskProvider = diskProvider; + _logger = logger; + + _cache = cacheManager.GetCache>(GetType()); + } + + public List All() + { + return _cache.Get("all", () => _remotePathMappingRepository.All().ToList(), TimeSpan.FromSeconds(10)); + } + + public RemotePathMapping Add(RemotePathMapping mapping) + { + mapping.LocalPath = CleanPath(mapping.LocalPath); + mapping.RemotePath = CleanPath(mapping.RemotePath); + + var all = All(); + + ValidateMapping(all, mapping); + + var result = _remotePathMappingRepository.Insert(mapping); + + _cache.Clear(); + + return result; + } + + public void Remove(int id) + { + _remotePathMappingRepository.Delete(id); + + _cache.Clear(); + } + + public RemotePathMapping Get(int id) + { + return _remotePathMappingRepository.Get(id); + } + + public RemotePathMapping Update(RemotePathMapping mapping) + { + var existing = All().Where(v => v.Id != mapping.Id).ToList(); + + ValidateMapping(existing, mapping); + + var result = _remotePathMappingRepository.Update(mapping); + + _cache.Clear(); + + return result; + } + + private void ValidateMapping(List existing, RemotePathMapping mapping) + { + if (mapping.Host.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Invalid Host"); + } + + if (mapping.RemotePath.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Invalid RemotePath"); + } + + if (mapping.LocalPath.IsNullOrWhiteSpace() || !Path.IsPathRooted(mapping.LocalPath)) + { + throw new ArgumentException("Invalid LocalPath"); + } + + if (!_diskProvider.FolderExists(mapping.LocalPath)) + { + throw new DirectoryNotFoundException("Can't add mount point directory that doesn't exist."); + } + + if (existing.Exists(r => r.Host == mapping.Host && r.RemotePath == mapping.RemotePath)) + { + throw new InvalidOperationException("RemotePath already mounted."); + } + } + + public String RemapRemoteToLocal(String host, String remotePath) + { + if (remotePath.IsNullOrWhiteSpace()) + { + return remotePath; + } + + var cleanRemotePath = CleanPath(remotePath); + + foreach (var mapping in All()) + { + if (host == mapping.Host && cleanRemotePath.StartsWith(mapping.RemotePath)) + { + var localPath = mapping.LocalPath + cleanRemotePath.Substring(mapping.RemotePath.Length); + + localPath = localPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + if (!remotePath.EndsWith("/") && !remotePath.EndsWith("\\")) + { + localPath = localPath.TrimEnd('/', '\\'); + } + + return localPath; + } + } + + return remotePath; + } + + public String RemapLocalToRemote(String host, String localPath) + { + if (localPath.IsNullOrWhiteSpace()) + { + return localPath; + } + + var cleanLocalPath = CleanPath(localPath); + + foreach (var mapping in All()) + { + if (host != mapping.Host) continue; + + if (cleanLocalPath.StartsWith(mapping.LocalPath)) + { + var remotePath = mapping.RemotePath + cleanLocalPath.Substring(mapping.LocalPath.Length); + + remotePath = remotePath.Replace(Path.DirectorySeparatorChar, mapping.RemotePath.Contains('\\') ? '\\' : '/'); + + if (!localPath.EndsWith("/") && !localPath.EndsWith("\\")) + { + remotePath = remotePath.TrimEnd('/', '\\'); + } + + return remotePath; + } + } + + return localPath; + } + + // TODO: Remove around January 2015. Used to migrate legacy Local Category Path settings. + public void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, String remotePath, String localPath) + { + _logger.Debug("Migrating local category path for Host {0}/{1} to {2}", host, remotePath, localPath); + + var existingMappings = All().Where(v => v.Host == host).ToList(); + + remotePath = CleanPath(remotePath); + localPath = CleanPath(localPath); + + if (!existingMappings.Any(v => v.LocalPath == localPath && v.RemotePath == remotePath)) + { + Add(new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath }); + } + + var downloadClient = _downloadClientRepository.Get(downloadClientId); + downloadClient.Settings = newSettings; + _downloadClientRepository.Update(downloadClient); + } + + private static String CleanPath(String path) + { + if (path.Contains('\\')) + { + return path.TrimEnd('\\', '/') + "\\"; + } + else + { + return path.TrimEnd('\\', '/') + "/"; + } + } + } +} \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayout.js b/src/UI/Settings/DownloadClient/DownloadClientLayout.js index e9510f160..b88cfe504 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayout.js +++ b/src/UI/Settings/DownloadClient/DownloadClientLayout.js @@ -4,9 +4,11 @@ define([ 'marionette', 'Settings/DownloadClient/DownloadClientCollection', 'Settings/DownloadClient/DownloadClientCollectionView', + 'Settings/DownloadClient/DownloadHandling/DownloadHandlingView', 'Settings/DownloadClient/DroneFactory/DroneFactoryView', - 'Settings/DownloadClient/DownloadHandling/DownloadHandlingView' -], function (Marionette, DownloadClientCollection, CollectionView, DroneFactoryView, DownloadHandlingView) { + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView' +], function (Marionette, DownloadClientCollection, DownloadClientCollectionView, DownloadHandlingView, DroneFactoryView, RemotePathMappingCollection, RemotePathMappingCollectionView) { return Marionette.Layout.extend({ template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', @@ -14,18 +16,22 @@ define([ regions: { downloadClients : '#x-download-clients-region', downloadHandling : '#x-download-handling-region', - droneFactory : '#x-dronefactory-region' + droneFactory : '#x-dronefactory-region', + remotePathMappings : '#x-remotepath-mapping-region' }, initialize: function () { this.downloadClientsCollection = new DownloadClientCollection(); this.downloadClientsCollection.fetch(); + this.remotePathMappingCollection = new RemotePathMappingCollection(); + this.remotePathMappingCollection.fetch(); }, onShow: function () { - this.downloadClients.show(new CollectionView({ collection: this.downloadClientsCollection })); + this.downloadClients.show(new DownloadClientCollectionView({ collection: this.downloadClientsCollection })); this.downloadHandling.show(new DownloadHandlingView({ model: this.model })); this.droneFactory.show(new DroneFactoryView({ model: this.model })); + this.remotePathMappings.show(new RemotePathMappingCollectionView({ collection: this.remotePathMappingCollection })); } }); }); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs index 7450b08d3..7f3d4ffca 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs +++ b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs @@ -1,6 +1,6 @@ 
-
+
diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js new file mode 100644 index 000000000..4369a8563 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js @@ -0,0 +1,11 @@ +'use strict'; +define([ + 'backbone', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel' +], function (Backbone, RemotePathMappingModel) { + + return Backbone.Collection.extend({ + model : RemotePathMappingModel, + url : window.NzbDrone.ApiRoot + '/remotePathMapping' + }); +}); diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js new file mode 100644 index 000000000..98c9ba2ce --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js @@ -0,0 +1,28 @@ +'use strict'; +define([ + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel', + 'bootstrap' +], function (AppLayout, Marionette, RemotePathMappingItemView, EditView, RemotePathMappingModel) { + + return Marionette.CompositeView.extend({ + template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate', + itemViewContainer : '.x-rows', + itemView : RemotePathMappingItemView, + + events: { + 'click .x-add' : '_addMapping' + }, + + _addMapping: function() { + var model = new RemotePathMappingModel(); + model.collection = this.collection; + + var view = new EditView({ model: model, targetCollection: this.collection}); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs new file mode 100644 index 000000000..846dee402 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs @@ -0,0 +1,24 @@ +
+ Remote Path Mappings + +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js new file mode 100644 index 000000000..36e252f56 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate', + + 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/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs new file mode 100644 index 000000000..ad288fbf6 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs @@ -0,0 +1,13 @@ + diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js new file mode 100644 index 000000000..6016083c9 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js @@ -0,0 +1,51 @@ +'use strict'; + +define([ + 'underscore', + 'vent', + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'Mixins/AsEditModalView', + 'Mixins/AutoComplete', + 'bootstrap' +], function (_, vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, AsEditModalView) { + + var view = Marionette.ItemView.extend({ + template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate', + + ui : { + path : '.x-path', + modalBody : '.modal-body' + }, + + _deleteView: DeleteView, + + 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'); + }, + + _onAfterSave : function () { + this.targetCollection.add(this.model, { merge : true }); + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + AsEditModalView.call(view); + + return view; +}); diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs new file mode 100644 index 000000000..4d755ac8b --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs @@ -0,0 +1,63 @@ + \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js new file mode 100644 index 000000000..9a98d046e --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js @@ -0,0 +1,26 @@ +'use strict'; + +define([ + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView' +], function (AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate', + className : 'row', + + events: { + 'click .x-edit' : '_editMapping' + }, + + initialize: function () { + this.listenTo(this.model, 'sync', this.render); + }, + + _editMapping: function() { + var view = new EditView({ model: this.model, targetCollection: this.model.collection}); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs new file mode 100644 index 000000000..2aecc5417 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs @@ -0,0 +1,12 @@ + +
{{host}}
+
+ +
{{remotePath}}
+
+ +
{{localPath}}
+
+ +
+
\ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js new file mode 100644 index 000000000..2eec974a4 --- /dev/null +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js @@ -0,0 +1,10 @@ +'use strict'; + +define([ + 'jquery', + 'backbone.deepmodel' +], function ($, DeepModel) { + return DeepModel.DeepModel.extend({ + + }); +}); diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less index 9b9f30f10..65c88e939 100644 --- a/src/UI/Settings/DownloadClient/downloadclient.less +++ b/src/UI/Settings/DownloadClient/downloadclient.less @@ -30,4 +30,31 @@ li.add-thingy-item { width: 33%; } +} + +.add-remotepath-mapping { + cursor: pointer; + font-size: 14px; + text-align: center; + display: inline-block; + padding: 2px 6px; + + i { + cursor: pointer; + } +} + +#remotepath-mapping-list { + + .remotepath-header .row { + font-weight: bold; + line-height: 40px; + } + + .rows .row { + line-height : 30px; + border-top : 1px solid #ddd; + vertical-align : middle; + padding : 5px; + } } \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js index 48588bcbf..64638e676 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js @@ -10,7 +10,7 @@ define( return Marionette.CompositeView.extend({ template: 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate', - itemViewContainer: ".x-rows", + itemViewContainer: '.x-rows', itemView: QualityDefinitionView });