New: Added global Remote Path mapping table to replace individual Local Category Path settings.
This commit is contained in:
parent
8281063698
commit
525f1aa9dd
|
@ -96,6 +96,8 @@
|
|||
<Compile Include="ClientSchema\SelectOption.cs" />
|
||||
<Compile Include="Commands\CommandModule.cs" />
|
||||
<Compile Include="Commands\CommandResource.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingModule.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />
|
||||
<Compile Include="Config\UiConfigModule.cs" />
|
||||
<Compile Include="Config\UiConfigResource.cs" />
|
||||
<Compile Include="Config\DownloadClientConfigModule.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<RemotePathMappingResource>
|
||||
{
|
||||
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<RemotePathMappingResource>();
|
||||
}
|
||||
|
||||
private int CreateMapping(RemotePathMappingResource rootFolderResource)
|
||||
{
|
||||
return GetNewId<RemotePathMapping>(_remotePathMappingService.Add, rootFolderResource);
|
||||
}
|
||||
|
||||
private List<RemotePathMappingResource> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), null))
|
||||
.Returns(CreateRemoteEpisode());
|
||||
|
||||
.Returns(() => CreateRemoteEpisode());
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||
|
||||
Mocker.GetMock<IRemotePathMappingService>()
|
||||
.Setup(v => v.RemapRemoteToLocal(It.IsAny<String>(), It.IsAny<String>()))
|
||||
.Returns<String, String>((h,r) => r);
|
||||
}
|
||||
|
||||
protected virtual RemoteEpisode CreateRemoteEpisode()
|
||||
|
|
|
@ -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<INzbgetProxy>()
|
||||
|
@ -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<IRemotePathMappingService>()
|
||||
.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<IRemotePathMappingService>()
|
||||
.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);
|
||||
|
|
|
@ -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<ISabnzbdProxy>()
|
||||
|
@ -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<IRemotePathMappingService>()
|
||||
.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<IRemotePathMappingService>()
|
||||
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv"))
|
||||
.Returns(@"O:\mymount".AsOsAgnostic());
|
||||
|
||||
GivenQueue(null);
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@
|
|||
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
|
||||
<Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" />
|
||||
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.cs" />
|
||||
<Compile Include="OrganizerTests\CleanFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileServiceTests\FilterFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileTableCleanupServiceFixture.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<RemotePathMappingService>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(m => m.FolderExists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IRemotePathMappingRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(new List<RemotePathMapping>());
|
||||
}
|
||||
|
||||
private void GivenMapping()
|
||||
{
|
||||
var mappings = Builder<RemotePathMapping>.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<IRemotePathMappingRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(mappings);
|
||||
}
|
||||
|
||||
private void WithNonExistingFolder()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(m => m.FolderExists(It.IsAny<string>()))
|
||||
.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<IRemotePathMappingRepository>().Verify(c => c.Insert(mapping), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_remove_mapping()
|
||||
{
|
||||
Subject.Remove(1);
|
||||
Mocker.GetMock<IRemotePathMappingRepository>().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<InvalidOperationException>(() => 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<PendingRelease>().RegisterModel("PendingReleases")
|
||||
.Ignore(e => e.RemoteEpisode);
|
||||
|
||||
Mapper.Entity<RemotePathMapping>().RegisterModel("RemotePathMappings");
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
|
|
|
@ -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<DownloadClientItem> GetItems()
|
||||
{
|
||||
Dictionary<String, String> 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<String> { category.DestDir };
|
||||
}
|
||||
else
|
||||
{
|
||||
status.OutputRootFolders = new List<String> { Settings.TvCategoryLocalPath };
|
||||
}
|
||||
status.OutputRootFolders = new List<String> { _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<DownloadClientItem> 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<String> { category.FullPath };
|
||||
}
|
||||
else
|
||||
{
|
||||
status.OutputRootFolders = new List<String> { Settings.TvCategoryLocalPath };
|
||||
}
|
||||
status.OutputRootFolders = new List<String> { _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ValidationFailure>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -223,6 +223,7 @@
|
|||
<Compile Include="Datastore\Migration\057_convert_episode_file_path_to_relative.cs" />
|
||||
<Compile Include="Datastore\Migration\058_drop_epsiode_file_path.cs" />
|
||||
<Compile Include="Datastore\Migration\059_add_enable_options_to_indexers.cs" />
|
||||
<Compile Include="Datastore\Migration\063_add_remotepathmappings.cs" />
|
||||
<Compile Include="Datastore\Migration\061_clear_bad_scene_names.cs" />
|
||||
<Compile Include="Datastore\Migration\060_remove_enable_from_indexers.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
|
@ -560,6 +561,9 @@
|
|||
<Compile Include="MetadataSource\Trakt\TraktException.cs" />
|
||||
<Compile Include="MetadataSource\TraktProxy.cs" />
|
||||
<Compile Include="MetadataSource\Tvdb\TvdbProxy.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingService.cs" />
|
||||
<Compile Include="Notifications\DownloadMessage.cs" />
|
||||
<Compile Include="Notifications\Email\Email.cs">
|
||||
<SubType>Code</SubType>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.RemotePathMappings
|
||||
{
|
||||
public interface IRemotePathMappingRepository : IBasicRepository<RemotePathMapping>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class RemotePathMappingRepository : BasicRepository<RemotePathMapping>, IRemotePathMappingRepository
|
||||
{
|
||||
|
||||
public RemotePathMappingRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool PublishModelEvents
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<RemotePathMapping> 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<List<RemotePathMapping>> _cache;
|
||||
|
||||
public RemotePathMappingService(IDownloadClientRepository downloadClientRepository,
|
||||
IRemotePathMappingRepository remotePathMappingRepository,
|
||||
IDiskProvider diskProvider,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_downloadClientRepository = downloadClientRepository;
|
||||
_remotePathMappingRepository = remotePathMappingRepository;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
|
||||
_cache = cacheManager.GetCache<List<RemotePathMapping>>(GetType());
|
||||
}
|
||||
|
||||
public List<RemotePathMapping> 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<RemotePathMapping> 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('\\', '/') + "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }));
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
<div id="x-download-clients-region"></div>
|
||||
<div class="form-horizontal">
|
||||
|
||||
<div id="x-download-handling-region"></div>
|
||||
<div id="x-dronefactory-region"></div>
|
||||
<div id="x-remotepath-mapping-region"></div>
|
||||
</div>
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
<fieldset class="advanced-setting">
|
||||
<legend>Remote Path Mappings</legend>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div id="remotepath-mapping-list">
|
||||
<div class="remotepath-header x-header hidden-xs">
|
||||
<div class="row">
|
||||
<span class="col-sm-2">Host</span>
|
||||
<span class="col-sm-5">Remote Path</span>
|
||||
<span class="col-sm-4">Local Path</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rows x-rows">
|
||||
</div>
|
||||
<div class="remotepath-footer">
|
||||
<div class="pull-right">
|
||||
<span class="add-remotepath-mapping">
|
||||
<i class="icon-nd-add x-add" title="Add new mapping" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>Delete Mapping</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete the mapping for '{{localPath}}'?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal">cancel</button>
|
||||
<button class="btn btn-danger x-confirm-delete">delete</button>
|
||||
</div>
|
||||
</div>
|
|
@ -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;
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
{{#if id}}
|
||||
<h3>Edit Mapping</h3>
|
||||
{{else}}
|
||||
<h3>Add Mapping</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-body remotepath-mapping-modal">
|
||||
<div class="form-horizontal">
|
||||
<div>
|
||||
<p>Use this feature if you have a remotely running Download Client. NzbDrone will use the information provided to translate the paths provided by the Download Client API to something NzbDrone can access and import.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Host</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-3 help-inline">
|
||||
<i class="icon-nd-form-info" title="Host you specified for the remote Download Client." />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-sm-pull-1">
|
||||
<input type="text" name="host" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Remote Path</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="Root path to the directory that the Download Client accesses." />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="text" name="remotePath" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Local Path</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="Path that NzbDrone should use to access the same directory remotely." />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="text" name="localPath" class="form-control x-path"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{#if id}}
|
||||
<button class="btn btn-danger pull-left x-delete">delete</button>
|
||||
{{/if}}
|
||||
|
||||
<button class="btn" data-dismiss="modal">cancel</button>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary x-save">save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
<span class="col-sm-2">
|
||||
<div>{{host}}</div>
|
||||
</span>
|
||||
<span class="col-sm-5">
|
||||
<div>{{remotePath}}</div>
|
||||
</span>
|
||||
<span class="col-sm-4">
|
||||
<div>{{localPath}}</div>
|
||||
</span>
|
||||
<span class="col-sm-1">
|
||||
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit Mapping"></i></div>
|
||||
</span>
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'backbone.deepmodel'
|
||||
], function ($, DeepModel) {
|
||||
return DeepModel.DeepModel.extend({
|
||||
|
||||
});
|
||||
});
|
|
@ -31,3 +31,30 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ define(
|
|||
return Marionette.CompositeView.extend({
|
||||
template: 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate',
|
||||
|
||||
itemViewContainer: ".x-rows",
|
||||
itemViewContainer: '.x-rows',
|
||||
|
||||
itemView: QualityDefinitionView
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue