From d00285540d3a1b9436544ac2c69be420f390e4f5 Mon Sep 17 00:00:00 2001 From: jtpavlock Date: Sun, 9 Jun 2019 12:54:53 -0500 Subject: [PATCH] New: Ability to set a post-import label in Deluge, rTorrent, qBittorrent, and uTorrent --- .../Download/Clients/Deluge/Deluge.cs | 61 ++++++++++---- .../Download/Clients/Deluge/DelugeProxy.cs | 4 +- .../Download/Clients/Deluge/DelugeSettings.cs | 12 ++- .../Clients/QBittorrent/QBittorrent.cs | 83 +++++++++++++++---- .../Clients/QBittorrent/QBittorrentLabel.cs | 10 +++ .../QBittorrent/QBittorrentProxySelector.cs | 2 + .../Clients/QBittorrent/QBittorrentProxyV1.cs | 15 +++- .../Clients/QBittorrent/QBittorrentProxyV2.cs | 15 +++- .../QBittorrent/QBittorrentSettings.cs | 14 +++- .../Download/Clients/rTorrent/RTorrent.cs | 22 ++++- .../rTorrent/RTorrentDirectoryValidator.cs | 4 +- .../Clients/rTorrent/RTorrentProxy.cs | 39 ++++++--- .../Clients/rTorrent/RTorrentSettings.cs | 13 +-- .../Download/Clients/uTorrent/UTorrent.cs | 28 ++++++- .../Clients/uTorrent/UTorrentProxy.cs | 15 ++++ .../Clients/uTorrent/UTorrentSettings.cs | 9 +- .../Download/DownloadClientBase.cs | 5 ++ .../Download/DownloadEventHub.cs | 36 ++++++-- src/NzbDrone.Core/Download/IDownloadClient.cs | 1 + 19 files changed, 311 insertions(+), 77 deletions(-) create mode 100644 src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 655e30ab8..1067af9cd 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -31,6 +31,24 @@ namespace NzbDrone.Core.Download.Clients.Deluge _proxy = proxy; } + public override void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + // set post-import category + if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && + Settings.MusicImportedCategory != Settings.MusicCategory) + { + try + { + _proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MusicImportedCategory, Settings); + } + catch (DownloadClientUnavailableException) + { + _logger.Warn("Failed to set torrent post-import label \"{0}\" for {1} in Deluge. Does the label exist?", + Settings.MusicImportedCategory, downloadClientItem.Title); + } + } + } + protected override string AddFromMagnetLink(RemoteAlbum remoteAlbum, string hash, string magnetLink) { var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings); @@ -40,13 +58,13 @@ namespace NzbDrone.Core.Download.Clients.Deluge throw new DownloadClientException("Deluge failed to add magnet " + magnetLink); } - if (!Settings.MusicCategory.IsNullOrWhiteSpace()) - { - _proxy.SetLabel(actualHash, Settings.MusicCategory, Settings); - } - _proxy.SetTorrentSeedingConfiguration(actualHash, remoteAlbum.SeedConfiguration, Settings); + if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(actualHash, Settings.MusicCategory, Settings); + } + var isRecentAlbum = remoteAlbum.IsRecentAlbum(); if (isRecentAlbum && Settings.RecentTvPriority == (int)DelugePriority.First || @@ -69,9 +87,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge _proxy.SetTorrentSeedingConfiguration(actualHash, remoteAlbum.SeedConfiguration, Settings); - if (!Settings.MusicCategory.IsNullOrWhiteSpace()) + if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) { - _proxy.SetLabel(actualHash, Settings.MusicCategory, Settings); + _proxy.SetTorrentLabel(actualHash, Settings.MusicCategory, Settings); } var isRecentAlbum = remoteAlbum.IsRecentAlbum(); @@ -91,7 +109,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge { IEnumerable torrents; - if (!Settings.MusicCategory.IsNullOrWhiteSpace()) + if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) { torrents = _proxy.GetTorrentsByLabel(Settings.MusicCategory, Settings); } @@ -117,6 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge item.OutputPath = outputPath + torrent.Name; item.RemainingSize = torrent.Size - torrent.BytesDownloaded; item.SeedRatio = torrent.Ratio; + try { item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta); @@ -190,7 +209,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge { status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }; } - + return status; } @@ -226,12 +245,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge case WebExceptionStatus.ConnectionClosed: return new NzbDroneValidationFailure("UseSsl", "Verify SSL settings") { - DetailedDescription = "Please verify your SSL configuration on both Deluge and NzbDrone." + DetailedDescription = "Please verify your SSL configuration on both Deluge and Lidarr." }; case WebExceptionStatus.SecureChannelFailure: return new NzbDroneValidationFailure("UseSsl", "Unable to connect through SSL") { - DetailedDescription = "Drone is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both drone and Deluge to not use SSL." + DetailedDescription = "Lidarr is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both drone and Deluge to not use SSL." }; default: return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); @@ -248,7 +267,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge private ValidationFailure TestCategory() { - if (Settings.MusicCategory.IsNullOrWhiteSpace()) + if (Settings.MusicCategory.IsNullOrWhiteSpace() && Settings.MusicImportedCategory.IsNullOrWhiteSpace()) { return null; } @@ -265,7 +284,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge var labels = _proxy.GetAvailableLabels(Settings); - if (!labels.Contains(Settings.MusicCategory)) + if (Settings.MusicCategory.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.MusicCategory)) { _proxy.AddLabel(Settings.MusicCategory, Settings); labels = _proxy.GetAvailableLabels(Settings); @@ -274,7 +293,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge { return new NzbDroneValidationFailure("MusicCategory", "Configuration of label failed") { - DetailedDescription = "Lidarr as unable to add the label to Deluge." + DetailedDescription = "Lidarr was unable to add the label to Deluge." + }; + } + } + + if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.MusicImportedCategory)) + { + _proxy.AddLabel(Settings.MusicImportedCategory, Settings); + labels = _proxy.GetAvailableLabels(Settings); + + if (!labels.Contains(Settings.MusicImportedCategory)) + { + return new NzbDroneValidationFailure("MusicImportedCategory", "Configuration of label failed") + { + DetailedDescription = "Lidarr was unable to add the label to Deluge." }; } } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 45f6727f1..580096373 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge string[] GetAvailablePlugins(DelugeSettings settings); string[] GetEnabledPlugins(DelugeSettings settings); string[] GetAvailableLabels(DelugeSettings settings); - void SetLabel(string hash, string label, DelugeSettings settings); + void SetTorrentLabel(string hash, string label, DelugeSettings settings); void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings); void AddLabel(string label, DelugeSettings settings); @@ -187,7 +187,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge ProcessRequest(settings, "label.add", label); } - public void SetLabel(string hash, string label, DelugeSettings settings) + public void SetTorrentLabel(string hash, string label, DelugeSettings settings) { ProcessRequest(settings, "label.set_torrent", hash, label); } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index e67e2fe59..212efdb0d 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.MusicCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -"); + RuleFor(c => c.MusicImportedCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -"); } } @@ -43,16 +44,19 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional")] public string MusicCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] + [FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Lidarr to set after it has imported the download. Leave blank to disable this feature.")] + public string MusicImportedCategory { get; set; } + + [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] public int RecentTvPriority { get; set; } - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] + [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] public int OlderTvPriority { get; set; } - [FieldDefinition(7, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } - [FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 2fb69a891..d1d9d0afd 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -33,6 +33,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings); + public override void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + // set post-import category + if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && + Settings.MusicImportedCategory != Settings.MusicCategory) + { + try + { + Proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MusicImportedCategory, Settings); + } + catch (DownloadClientException) + { + _logger.Warn("Failed to set post-import torrent label \"{0}\" for {1} in qBittorrent. Does the label exist?", + Settings.MusicImportedCategory, downloadClientItem.Title); + } + } + } + protected override string AddFromMagnetLink(RemoteAlbum remoteAlbum, string hash, string magnetLink) { if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr=")) @@ -42,11 +60,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Proxy.AddTorrentFromUrl(magnetLink, Settings); - if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) - { - Proxy.SetTorrentLabel(hash.ToLower(), Settings.MusicCategory, Settings); - } - var isRecentAlbum = remoteAlbum.IsRecentAlbum(); if (isRecentAlbum && Settings.RecentTvPriority == (int)QBittorrentPriority.First || @@ -69,18 +82,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { Proxy.AddTorrentFromFile(filename, fileContent, Settings); - try - { - if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) - { - Proxy.SetTorrentLabel(hash.ToLower(), Settings.MusicCategory, Settings); - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to set the torrent label for {0}.", filename); - } - try { var isRecentAlbum = remoteAlbum.IsRecentAlbum(); @@ -216,6 +217,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { failures.AddIfNotNull(TestConnection()); if (failures.HasErrors()) return; + failures.AddIfNotNull(TestCategory()); failures.AddIfNotNull(TestPrioritySupport()); failures.AddIfNotNull(TestGetTorrents()); } @@ -293,6 +295,53 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return null; } + private ValidationFailure TestCategory() + { + if (Settings.MusicCategory.IsNullOrWhiteSpace() && Settings.MusicImportedCategory.IsNullOrWhiteSpace()) + { + return null; + } + + // api v1 doesn't need to check/add categories as it's done on set + var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings); + if (version < Version.Parse("2.0")) + { + return null; + } + + Dictionary labels = Proxy.GetLabels(Settings); + + if (Settings.MusicCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.MusicCategory)) + { + Proxy.AddLabel(Settings.MusicCategory, Settings); + labels = Proxy.GetLabels(Settings); + + if (!labels.ContainsKey(Settings.MusicCategory)) + { + return new NzbDroneValidationFailure("MusicCategory", "Configuration of label failed") + { + DetailedDescription = "Lidarr was unable to add the label to qBittorrent." + }; + } + } + + if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.MusicImportedCategory)) + { + Proxy.AddLabel(Settings.MusicImportedCategory, Settings); + labels = Proxy.GetLabels(Settings); + + if (!labels.ContainsKey(Settings.MusicImportedCategory)) + { + return new NzbDroneValidationFailure("MusicImportedCategory", "Configuration of label failed") + { + DetailedDescription = "Lidarr was unable to add the label to qBittorrent." + }; + } + } + + return null; + } + private ValidationFailure TestPrioritySupport() { var recentPriorityDefault = Settings.RecentTvPriority == (int)QBittorrentPriority.Last; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs new file mode 100644 index 000000000..153e8e009 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.QBittorrent +{ + public class QBittorrentLabel + { + public string Name { get; set; } + public string SavePath { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index c60b0ff19..0b3c07e63 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -23,6 +23,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent void RemoveTorrent(string hash, bool removeData, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); + void AddLabel(string label, QBittorrentSettings settings); + Dictionary GetLabels(QBittorrentSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); void PauseTorrent(string hash, QBittorrentSettings settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index d9077a9c2..49bf51f33 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -192,6 +192,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } + public void AddLabel(string label, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/addCategory") + .Post() + .AddFormParameter("category", label); + ProcessRequest(request, settings); + } + + public Dictionary GetLabels(QBittorrentSettings settings) + { + throw new NotSupportedException("qBittorrent api v1 does not support getting all torrent categories"); + } + public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { // Not supported on api v1 @@ -242,7 +255,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent .Post() .AddFormParameter("hashes", hash) .AddFormParameter("value", enabled ? "true" : "false"); - ProcessRequest(request, settings); } @@ -253,7 +265,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent LogResponseContent = true, NetworkCredential = new NetworkCredential(settings.Username, settings.Password) }; - return requestBuilder; } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index d223f92e3..31b2ef3d2 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -91,7 +91,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddQueryParam("category", settings.MusicCategory); } - var response = ProcessRequest>(request, settings); return response; @@ -178,6 +177,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent ProcessRequest(request, settings); } + public void AddLabel(string label, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/api/v2/torrents/createCategory") + .Post() + .AddFormParameter("category", label); + ProcessRequest(request, settings); + } + + public Dictionary GetLabels(QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/api/v2/torrents/categories"); + return Json.Deserialize>(ProcessRequest(request, settings)); + } + public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index bee84f902..47aac30ed 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -11,6 +11,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.Port).InclusiveBetween(1, 65535); + + RuleFor(c => c.MusicCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'"); + RuleFor(c => c.MusicImportedCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'"); } } @@ -40,16 +43,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional")] public string MusicCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] + [FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Lidarr to set after it has imported the download. Leave blank to disable this feature.")] + public string MusicImportedCategory { get; set; } + + [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] public int RecentTvPriority { get; set; } - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] + [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] public int OlderTvPriority { get; set; } - [FieldDefinition(7, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")] + [FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")] public int InitialState { get; set; } - [FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] + [FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index d59996e97..36e0818e3 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -37,11 +37,29 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _rTorrentDirectoryValidator = rTorrentDirectoryValidator; } + public override void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + // set post-import category + if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && + Settings.MusicImportedCategory != Settings.MusicCategory) + { + try + { + _proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MusicImportedCategory, Settings); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set torrent post-import label \"{0}\" for {1} in rTorrent. Does the label exist?", + Settings.MusicImportedCategory, downloadClientItem.Title); + } + } + } + protected override string AddFromMagnetLink(RemoteAlbum remoteAlbum, string hash, string magnetLink) { var priority = (RTorrentPriority)(remoteAlbum.IsRecentAlbum() ? Settings.RecentTvPriority : Settings.OlderTvPriority); - _proxy.AddTorrentFromUrl(magnetLink, Settings.MusicCategory, priority, Settings.TvDirectory, Settings); + _proxy.AddTorrentFromUrl(magnetLink, Settings.MusicCategory, priority, Settings.MusicDirectory, Settings); var tries = 10; var retryDelay = 500; @@ -61,7 +79,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent { var priority = (RTorrentPriority)(remoteAlbum.IsRecentAlbum() ? Settings.RecentTvPriority : Settings.OlderTvPriority); - _proxy.AddTorrentFromFile(filename, fileContent, Settings.MusicCategory, priority, Settings.TvDirectory, Settings); + _proxy.AddTorrentFromFile(filename, fileContent, Settings.MusicCategory, priority, Settings.MusicDirectory, Settings); var tries = 10; var retryDelay = 500; diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs index 45a3c39c5..cc652c7f8 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs @@ -17,12 +17,12 @@ namespace NzbDrone.Core.Download.Clients.rTorrent PathExistsValidator pathExistsValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator) { - RuleFor(c => c.TvDirectory).Cascade(CascadeMode.StopOnFirstFailure) + RuleFor(c => c.MusicDirectory).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(mappedNetworkDriveValidator) .SetValidator(pathExistsValidator) - .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) + .When(c => c.MusicDirectory.IsNotNullOrWhiteSpace()) .When(c => c.Host == "localhost" || c.Host == "127.0.0.1"); } } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs index ff9a4332f..b35063076 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings); void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings); void RemoveTorrent(string hash, RTorrentSettings settings); + void SetTorrentLabel(string hash, string label, RTorrentSettings settings); bool HasHashTorrent(string hash, RTorrentSettings settings); } @@ -44,6 +45,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [XmlRpcMethod("d.name")] string GetName(string hash); + [XmlRpcMethod("d.custom1.set")] + string SetLabel(string hash, string label); + [XmlRpcMethod("system.client_version")] string GetVersion(); } @@ -90,20 +94,20 @@ namespace NzbDrone.Core.Download.Clients.RTorrent foreach (object[] torrent in ret) { - var labelDecoded = System.Web.HttpUtility.UrlDecode((string) torrent[3]); + var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]); var item = new RTorrentTorrent(); - item.Name = (string) torrent[0]; - item.Hash = (string) torrent[1]; - item.Path = (string) torrent[2]; + item.Name = (string)torrent[0]; + item.Hash = (string)torrent[1]; + item.Path = (string)torrent[2]; item.Category = labelDecoded; - item.TotalSize = (long) torrent[4]; - item.RemainingSize = (long) torrent[5]; - item.DownRate = (long) torrent[6]; - item.Ratio = (long) torrent[7]; - item.IsOpen = Convert.ToBoolean((long) torrent[8]); - item.IsActive = Convert.ToBoolean((long) torrent[9]); - item.IsFinished = Convert.ToBoolean((long) torrent[10]); + item.TotalSize = (long)torrent[4]; + item.RemainingSize = (long)torrent[5]; + item.DownRate = (long)torrent[6]; + item.Ratio = (long)torrent[7]; + item.IsOpen = Convert.ToBoolean((long)torrent[8]); + item.IsActive = Convert.ToBoolean((long)torrent[9]); + item.IsFinished = Convert.ToBoolean((long)torrent[10]); items.Add(item); } @@ -157,6 +161,19 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } } + public void SetTorrentLabel(string hash, string label, RTorrentSettings settings) + { + _logger.Debug("Executing remote method: d.custom1.set"); + + var client = BuildClient(settings); + var response = ExecuteRequest(() => client.SetLabel(hash, label)); + + if (response != label) + { + throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label); + } + } + public void RemoveTorrent(string hash, RTorrentSettings settings) { _logger.Debug("Executing remote method: d.erase"); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index e2fc57dc8..86a5d42d2 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -52,16 +52,19 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional.")] public string MusicCategory { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] - public string TvDirectory { get; set; } + [FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Lidarr to set after it has imported the download. Leave blank to disable this feature.")] + public string MusicImportedCategory { get; set; } - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] + [FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] + public string MusicDirectory { get; set; } + + [FieldDefinition(9, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] public int RecentTvPriority { get; set; } - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] + [FieldDefinition(10, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] public int OlderTvPriority { get; set; } - [FieldDefinition(10, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")] + [FieldDefinition(11, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")] public bool AddStopped { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index abb022c6c..88116f725 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -36,12 +36,32 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _torrentCache = cacheManager.GetCache(GetType(), "differentialTorrents"); } + public override void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + // set post-import category + if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && + Settings.MusicImportedCategory != Settings.MusicCategory) + { + _proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MusicImportedCategory, Settings); + + // old label must be explicitly removed + if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) + { + _proxy.RemoveTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MusicCategory, Settings); + } + } + } + protected override string AddFromMagnetLink(RemoteAlbum remoteAlbum, string hash, string magnetLink) { _proxy.AddTorrentFromUrl(magnetLink, Settings); - _proxy.SetTorrentLabel(hash, Settings.MusicCategory, Settings); _proxy.SetTorrentSeedingConfiguration(hash, remoteAlbum.SeedConfiguration, Settings); + if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash, Settings.MusicCategory, Settings); + } + var isRecentAlbum = remoteAlbum.IsRecentAlbum(); if (isRecentAlbum && Settings.RecentTvPriority == (int)UTorrentPriority.First || @@ -58,9 +78,13 @@ namespace NzbDrone.Core.Download.Clients.UTorrent protected override string AddFromTorrentFile(RemoteAlbum remoteAlbum, string hash, string filename, byte[] fileContent) { _proxy.AddTorrentFromFile(filename, fileContent, Settings); - _proxy.SetTorrentLabel(hash, Settings.MusicCategory, Settings); _proxy.SetTorrentSeedingConfiguration(hash, remoteAlbum.SeedConfiguration, Settings); + if (Settings.MusicCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash, Settings.MusicCategory, Settings); + } + var isRecentAlbum = remoteAlbum.IsRecentAlbum(); if (isRecentAlbum && Settings.RecentTvPriority == (int)UTorrentPriority.First || diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs index 8f8940e76..115c72088 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent void RemoveTorrent(string hash, bool removeData, UTorrentSettings settings); void SetTorrentLabel(string hash, string label, UTorrentSettings settings); + void RemoveTorrentLabel(string hash, string label, UTorrentSettings settings); void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings); void SetState(string hash, UTorrentState state, UTorrentSettings settings); } @@ -154,6 +155,20 @@ namespace NzbDrone.Core.Download.Clients.UTorrent ProcessRequest(requestBuilder, settings); } + public void RemoveTorrentLabel(string hash, string label, UTorrentSettings settings) + { + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "setprops") + .AddQueryParam("hash", hash); + + requestBuilder.AddQueryParam("s", "label") + .AddQueryParam("v", label) + .AddQueryParam("s", "label") + .AddQueryParam("v", ""); + + ProcessRequest(requestBuilder, settings); + } + public void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings) { var requestBuilder = BuildRequest(settings) diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index ca487caed..0d111ff35 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -41,13 +41,16 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional")] public string MusicCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] + [FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Lidarr to set after it has imported the download. Leave blank to disable this feature.")] + public string MusicImportedCategory { get; set; } + + [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")] public int RecentTvPriority { get; set; } - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] + [FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] public int OlderTvPriority { get; set; } - [FieldDefinition(7, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")] + [FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")] public int IntialState { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index e2b3f7d10..f6a58d2d2 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -147,5 +147,10 @@ namespace NzbDrone.Core.Download return null; } + + public virtual void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + throw new NotSupportedException(this.Name + " does not support marking items as imported"); + } } } diff --git a/src/NzbDrone.Core/Download/DownloadEventHub.cs b/src/NzbDrone.Core/Download/DownloadEventHub.cs index e0e3bd56e..7ecb9c190 100644 --- a/src/NzbDrone.Core/Download/DownloadEventHub.cs +++ b/src/NzbDrone.Core/Download/DownloadEventHub.cs @@ -35,15 +35,17 @@ namespace NzbDrone.Core.Download public void Handle(DownloadCompletedEvent message) { - if (!_configService.RemoveCompletedDownloads || - message.TrackedDownload.DownloadItem.Removed || - !message.TrackedDownload.DownloadItem.CanBeRemoved || - message.TrackedDownload.DownloadItem.Status == DownloadItemStatus.Downloading) + if (_configService.RemoveCompletedDownloads && + !message.TrackedDownload.DownloadItem.Removed && + message.TrackedDownload.DownloadItem.CanBeRemoved && + message.TrackedDownload.DownloadItem.Status != DownloadItemStatus.Downloading) { - return; + RemoveFromDownloadClient(message.TrackedDownload); } - - RemoveFromDownloadClient(message.TrackedDownload); + else + { + MarkItemAsImported(message.TrackedDownload); + } } public void Handle(DownloadFailedEvent message) @@ -74,7 +76,25 @@ namespace NzbDrone.Core.Download } catch (Exception e) { - _logger.Error(e, "Couldn't remove item from client {0}", trackedDownload.DownloadItem.Title); + _logger.Error(e, "Couldn't remove item {0} from client {1}", trackedDownload.DownloadItem.Title, downloadClient.Name); + } + } + + private void MarkItemAsImported(TrackedDownload trackedDownload) + { + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + try + { + _logger.Debug("[{0}] Marking download as imported from {1}", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClient); + downloadClient.MarkItemAsImported(trackedDownload.DownloadItem); + } + catch (NotSupportedException e) + { + _logger.Debug(e.Message); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't mark item {0} as imported from client {1}", trackedDownload.DownloadItem.Title, downloadClient.Name); } } } diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index cf0d1e419..99b9c0faa 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -13,5 +13,6 @@ namespace NzbDrone.Core.Download IEnumerable GetItems(); void RemoveItem(string downloadId, bool deleteData); DownloadClientInfo GetStatus(); + void MarkItemAsImported(DownloadClientItem downloadClientItem); } }