New: Labels support for Transmission 4.0

(cherry picked from commit 675e3cd38a14ea33c27f2d66a4be2bf802e17d88)
pull/5329/head
Bogdan 3 months ago
parent 166f87ae68
commit 7255126af5

@ -13,6 +13,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
[TestFixture]
public class TransmissionFixture : TransmissionFixtureBase<Transmission>
{
[SetUp]
public void Setup_Transmission()
{
Mocker.GetMock<ITransmissionProxy>()
.Setup(v => v.GetClientVersion(It.IsAny<TransmissionSettings>(), It.IsAny<bool>()))
.Returns("4.0.6");
}
[Test]
public void queued_item_should_have_required_properties()
{
@ -272,7 +280,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
public void should_only_check_version_number(string version)
{
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetClientVersion(It.IsAny<TransmissionSettings>()))
.Setup(s => s.GetClientVersion(It.IsAny<TransmissionSettings>(), true))
.Returns(version);
Subject.Test().IsValid.Should().BeTrue();

@ -29,7 +29,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass"
Password = "pass",
MusicCategory = ""
};
Subject.Definition = new DownloadClientDefinition();
@ -152,7 +153,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetTorrents(It.IsAny<TransmissionSettings>()))
.Setup(s => s.GetTorrents(null, It.IsAny<TransmissionSettings>()))
.Returns(torrents);
}

@ -1,8 +1,10 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
@ -13,6 +15,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public class Transmission : TransmissionBase
{
public override string Name => "Transmission";
public override bool SupportsLabels => HasClientVersion(4, 0);
public Transmission(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
@ -25,9 +30,48 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
}
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
if (!SupportsLabels)
{
throw new NotSupportedException($"{Name} does not support marking items as imported");
}
// set post-import category
if (Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.MusicImportedCategory != Settings.MusicCategory)
{
var hash = downloadClientItem.DownloadId.ToLowerInvariant();
var torrent = _proxy.GetTorrents(new[] { hash }, Settings).FirstOrDefault();
if (torrent == null)
{
_logger.Warn("Could not find torrent with hash \"{0}\" in Transmission.", hash);
return;
}
try
{
var labels = torrent.Labels.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
labels.Add(Settings.MusicImportedCategory);
if (Settings.MusicCategory.IsNotNullOrWhiteSpace())
{
labels.Remove(Settings.MusicCategory);
}
_proxy.SetTorrentLabels(hash, labels, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn(ex, "Failed to set post-import torrent label \"{0}\" for {1} in Transmission.", Settings.MusicImportedCategory, downloadClientItem.Title);
}
}
}
protected override ValidationFailure ValidateVersion()
{
var versionString = _proxy.GetClientVersion(Settings);
var versionString = _proxy.GetClientVersion(Settings, true);
_logger.Debug("Transmission version information: {0}", versionString);
@ -41,7 +85,5 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return null;
}
public override string Name => "Transmission";
}
}

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@ -17,6 +18,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public abstract class TransmissionBase : TorrentClientBase<TransmissionSettings>
{
public abstract bool SupportsLabels { get; }
protected readonly ITransmissionProxy _proxy;
public TransmissionBase(ITransmissionProxy proxy,
@ -35,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public override IEnumerable<DownloadClientItem> GetItems()
{
var configFunc = new Lazy<TransmissionConfig>(() => _proxy.GetConfig(Settings));
var torrents = _proxy.GetTorrents(Settings);
var torrents = _proxy.GetTorrents(null, Settings);
var items = new List<DownloadClientItem>();
@ -43,36 +46,45 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
var outputPath = new OsPath(torrent.DownloadDir);
if (Settings.MusicDirectory.IsNotNullOrWhiteSpace())
if (Settings.MusicCategory.IsNotNullOrWhiteSpace() && SupportsLabels && torrent.Labels is { Count: > 0 })
{
if (!new OsPath(Settings.MusicDirectory).Contains(outputPath))
if (!torrent.Labels.Contains(Settings.MusicCategory, StringComparer.InvariantCultureIgnoreCase))
{
continue;
}
}
else if (Settings.MusicCategory.IsNotNullOrWhiteSpace())
else
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.MusicCategory))
if (Settings.MusicDirectory.IsNotNullOrWhiteSpace())
{
continue;
if (!new OsPath(Settings.MusicDirectory).Contains(outputPath))
{
continue;
}
}
else if (Settings.MusicCategory.IsNotNullOrWhiteSpace())
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.MusicCategory))
{
continue;
}
}
}
outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath);
var item = new DownloadClientItem();
item.DownloadId = torrent.HashString.ToUpper();
item.Category = Settings.MusicCategory;
item.Title = torrent.Name;
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
item.OutputPath = GetOutputPath(outputPath, torrent);
item.TotalSize = torrent.TotalSize;
item.RemainingSize = torrent.LeftUntilDone;
item.SeedRatio = torrent.DownloadedEver <= 0 ? 0 :
(double)torrent.UploadedEver / torrent.DownloadedEver;
var item = new DownloadClientItem
{
DownloadId = torrent.HashString.ToUpper(),
Category = Settings.MusicCategory,
Title = torrent.Name,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MusicImportedCategory.IsNotNullOrWhiteSpace() && SupportsLabels),
OutputPath = GetOutputPath(outputPath, torrent),
TotalSize = torrent.TotalSize,
RemainingSize = torrent.LeftUntilDone,
SeedRatio = torrent.DownloadedEver <= 0 ? 0 : (double)torrent.UploadedEver / torrent.DownloadedEver
};
if (torrent.Eta >= 0)
{
@ -297,7 +309,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
try
{
_proxy.GetTorrents(Settings);
_proxy.GetTorrents(null, Settings);
}
catch (Exception ex)
{
@ -307,5 +319,15 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return null;
}
protected bool HasClientVersion(int major, int minor)
{
var rawVersion = _proxy.GetClientVersion(Settings);
var versionResult = Regex.Match(rawVersion, @"(?<!\(|(\d|\.)+)(\d|\.)+(?!\)|(\d|\.)+)").Value;
var clientVersion = Version.Parse(versionResult);
return clientVersion >= new Version(major, minor);
}
}
}

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using Newtonsoft.Json.Linq;
using NLog;
@ -12,15 +15,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public interface ITransmissionProxy
{
List<TransmissionTorrent> GetTorrents(TransmissionSettings settings);
IReadOnlyCollection<TransmissionTorrent> GetTorrents(IReadOnlyCollection<string> hashStrings, TransmissionSettings settings);
void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings);
void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings);
TransmissionConfig GetConfig(TransmissionSettings settings);
string GetProtocolVersion(TransmissionSettings settings);
string GetClientVersion(TransmissionSettings settings);
string GetClientVersion(TransmissionSettings settings, bool force = false);
void RemoveTorrent(string hash, bool removeData, TransmissionSettings settings);
void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings);
void SetTorrentLabels(string hash, IEnumerable<string> labels, TransmissionSettings settings);
}
public class TransmissionProxy : ITransmissionProxy
@ -28,50 +32,66 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private ICached<string> _authSessionIDCache;
private readonly ICached<string> _authSessionIdCache;
private readonly ICached<string> _versionCache;
public TransmissionProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_authSessionIDCache = cacheManager.GetCache<string>(GetType(), "authSessionID");
_authSessionIdCache = cacheManager.GetCache<string>(GetType(), "authSessionID");
_versionCache = cacheManager.GetCache<string>(GetType(), "versions");
}
public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings)
public IReadOnlyCollection<TransmissionTorrent> GetTorrents(IReadOnlyCollection<string> hashStrings, TransmissionSettings settings)
{
var result = GetTorrentStatus(settings);
var result = GetTorrentStatus(hashStrings, settings);
var torrents = ((JArray)result.Arguments["torrents"]).ToObject<List<TransmissionTorrent>>();
var torrents = ((JArray)result.Arguments["torrents"]).ToObject<ReadOnlyCollection<TransmissionTorrent>>();
return torrents;
}
public void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("filename", torrentUrl);
arguments.Add("paused", settings.AddPaused);
var arguments = new Dictionary<string, object>
{
{ "filename", torrentUrl },
{ "paused", settings.AddPaused }
};
if (!downloadDirectory.IsNullOrWhiteSpace())
{
arguments.Add("download-dir", downloadDirectory);
}
if (settings.MusicCategory.IsNotNullOrWhiteSpace())
{
arguments.Add("labels", new List<string> { settings.MusicCategory });
}
ProcessRequest("torrent-add", arguments, settings);
}
public void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("metainfo", Convert.ToBase64String(torrentData));
arguments.Add("paused", settings.AddPaused);
var arguments = new Dictionary<string, object>
{
{ "metainfo", Convert.ToBase64String(torrentData) },
{ "paused", settings.AddPaused }
};
if (!downloadDirectory.IsNullOrWhiteSpace())
{
arguments.Add("download-dir", downloadDirectory);
}
if (settings.MusicCategory.IsNotNullOrWhiteSpace())
{
arguments.Add("labels", new List<string> { settings.MusicCategory });
}
ProcessRequest("torrent-add", arguments, settings);
}
@ -82,8 +102,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return;
}
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new[] { hash });
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hash } }
};
if (seedConfiguration.Ratio != null)
{
@ -97,6 +119,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission
arguments.Add("seedIdleMode", 1);
}
// Avoid extraneous request if no limits are to be set
if (arguments.All(arg => arg.Key == "ids"))
{
return;
}
ProcessRequest("torrent-set", arguments, settings);
}
@ -107,11 +135,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return config.RpcVersion;
}
public string GetClientVersion(TransmissionSettings settings)
public string GetClientVersion(TransmissionSettings settings, bool force = false)
{
var config = GetConfig(settings);
var cacheKey = $"version:{$"{GetBaseUrl(settings)}:{settings.Password}".SHA256Hash()}";
if (force)
{
_versionCache.Remove(cacheKey);
}
return config.Version;
return _versionCache.Get(cacheKey, () => GetConfig(settings).Version, TimeSpan.FromHours(6));
}
public TransmissionConfig GetConfig(TransmissionSettings settings)
@ -124,21 +157,36 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public void RemoveTorrent(string hashString, bool removeData, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new string[] { hashString });
arguments.Add("delete-local-data", removeData);
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hashString } },
{ "delete-local-data", removeData }
};
ProcessRequest("torrent-remove", arguments, settings);
}
public void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new string[] { hashString });
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hashString } }
};
ProcessRequest("queue-move-top", arguments, settings);
}
public void SetTorrentLabels(string hash, IEnumerable<string> labels, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hash } },
{ "labels", labels.ToImmutableHashSet() }
};
ProcessRequest("torrent-set", arguments, settings);
}
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
{
// Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio.
@ -150,14 +198,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return ProcessRequest("session-stats", null, settings);
}
private TransmissionResponse GetTorrentStatus(TransmissionSettings settings)
{
return GetTorrentStatus(null, settings);
}
private TransmissionResponse GetTorrentStatus(IEnumerable<string> hashStrings, TransmissionSettings settings)
{
var fields = new string[]
var fields = new List<string>
{
"id",
"hashString", // Unique torrent ID. Use this instead of the client id?
@ -177,11 +220,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission
"seedRatioMode",
"seedIdleLimit",
"seedIdleMode",
"fileCount"
"fileCount",
"labels"
};
var arguments = new Dictionary<string, object>();
arguments.Add("fields", fields);
var arguments = new Dictionary<string, object>
{
{ "fields", fields }
};
if (hashStrings != null)
{
@ -193,9 +239,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return result;
}
private string GetBaseUrl(TransmissionSettings settings)
{
return HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
}
private HttpRequestBuilder BuildRequest(TransmissionSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
var requestBuilder = new HttpRequestBuilder(GetBaseUrl(settings))
.Resource("rpc")
.Accept(HttpAccept.Json);
@ -210,11 +261,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
var sessionId = _authSessionIDCache.Find(authKey);
var sessionId = _authSessionIdCache.Find(authKey);
if (sessionId == null || reauthenticate)
{
_authSessionIDCache.Remove(authKey);
_authSessionIdCache.Remove(authKey);
var authLoginRequest = BuildRequest(settings).Build();
authLoginRequest.SuppressHttpError = true;
@ -242,7 +293,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
_logger.Debug("Transmission authentication succeeded.");
_authSessionIDCache.Set(authKey, sessionId);
_authSessionIdCache.Set(authKey, sessionId);
}
requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId);

@ -33,6 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
Host = "localhost";
Port = 9091;
UrlBase = "/transmission/";
MusicCategory = "lidarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@ -56,16 +57,19 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
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 Transmission location")]
[FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")]
public string MusicImportedCategory { get; set; }
[FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")]
public string MusicDirectory { get; set; }
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")]
[FieldDefinition(9, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing albums released within the last 14 days")]
public int RecentMusicPriority { get; set; }
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")]
[FieldDefinition(10, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")]
public int OlderMusicPriority { get; set; }
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
[FieldDefinition(11, Label = "Add Paused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.Transmission
{
public class TransmissionTorrent
@ -9,6 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public long TotalSize { get; set; }
public long LeftUntilDone { get; set; }
public bool IsFinished { get; set; }
public IReadOnlyCollection<string> Labels { get; set; } = Array.Empty<string>();
public long Eta { get; set; }
public TransmissionTorrentStatus Status { get; set; }
public long SecondsDownloading { get; set; }

@ -14,6 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Vuze
{
private const int MINIMUM_SUPPORTED_PROTOCOL_VERSION = 14;
public override string Name => "Vuze";
public override bool SupportsLabels => false;
public Vuze(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
@ -65,7 +68,5 @@ namespace NzbDrone.Core.Download.Clients.Vuze
return null;
}
public override string Name => "Vuze";
}
}

Loading…
Cancel
Save