1
0
Fork 0
mirror of https://github.com/Radarr/Radarr synced 2024-12-21 23:42:23 +00:00

New: Labels support for Transmission 4.0

(cherry picked from commit 675e3cd38a14ea33c27f2d66a4be2bf802e17d88)
This commit is contained in:
Bogdan 2024-11-15 04:59:25 +02:00
parent 3cc4105d71
commit a2b38c5b7d
9 changed files with 200 additions and 73 deletions

View file

@ -148,10 +148,5 @@ public static string ConcatToString<TSource>(this IEnumerable<TSource> source, F
{
return string.Join(separator, source.Select(predicate));
}
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer = null)
{
return new HashSet<T>(source, comparer);
}
}
}

View file

@ -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 @@ public void should_fix_forward_slashes()
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();

View file

@ -29,7 +29,8 @@ public void Setup()
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass"
Password = "pass",
MovieCategory = ""
};
Subject.Definition = new DownloadClientDefinition();
@ -152,7 +153,7 @@ protected virtual void GivenTorrents(List<TransmissionTorrent> torrents)
}
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetTorrents(It.IsAny<TransmissionSettings>()))
.Setup(s => s.GetTorrents(null, It.IsAny<TransmissionSettings>()))
.Returns(torrents);
}

View file

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
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;
@ -15,6 +17,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,
@ -28,9 +33,48 @@ public Transmission(ITransmissionProxy proxy,
{
}
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.MovieImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.MovieImportedCategory != Settings.MovieCategory)
{
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.MovieImportedCategory);
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
labels.Remove(Settings.MovieCategory);
}
_proxy.SetTorrentLabels(hash, labels, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn(ex, "Failed to set post-import torrent label \"{0}\" for {1} in Transmission.", Settings.MovieImportedCategory, downloadClientItem.Title);
}
}
}
protected override ValidationFailure ValidateVersion()
{
var versionString = _proxy.GetClientVersion(Settings);
var versionString = _proxy.GetClientVersion(Settings, true);
_logger.Debug("Transmission version information: {0}", versionString);
@ -44,7 +88,5 @@ protected override ValidationFailure ValidateVersion()
return null;
}
public override string Name => "Transmission";
}
}

View file

@ -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;
@ -18,6 +19,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,
@ -37,7 +40,7 @@ public TransmissionBase(ITransmissionProxy proxy,
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>();
@ -45,36 +48,45 @@ public override IEnumerable<DownloadClientItem> GetItems()
{
var outputPath = new OsPath(torrent.DownloadDir);
if (Settings.MovieDirectory.IsNotNullOrWhiteSpace())
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && SupportsLabels && torrent.Labels is { Count: > 0 })
{
if (!new OsPath(Settings.MovieDirectory).Contains(outputPath))
if (!torrent.Labels.Contains(Settings.MovieCategory, StringComparer.InvariantCultureIgnoreCase))
{
continue;
}
}
else if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
else
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.MovieCategory))
if (Settings.MovieDirectory.IsNotNullOrWhiteSpace())
{
continue;
if (!new OsPath(Settings.MovieDirectory).Contains(outputPath))
{
continue;
}
}
else if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.MovieCategory))
{
continue;
}
}
}
outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath);
var item = new DownloadClientItem();
item.DownloadId = torrent.HashString.ToUpper();
item.Category = Settings.MovieCategory;
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.MovieCategory,
Title = torrent.Name,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MovieImportedCategory.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)
{
@ -300,7 +312,7 @@ private ValidationFailure TestGetTorrents()
{
try
{
_proxy.GetTorrents(Settings);
_proxy.GetTorrents(null, Settings);
}
catch (Exception ex)
{
@ -310,5 +322,15 @@ private ValidationFailure TestGetTorrents()
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);
}
}
}

View file

@ -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 @@ public class TransmissionProxy : ITransmissionProxy
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.MovieCategory.IsNotNullOrWhiteSpace())
{
arguments.Add("labels", new List<string> { settings.MovieCategory });
}
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.MovieCategory.IsNotNullOrWhiteSpace())
{
arguments.Add("labels", new List<string> { settings.MovieCategory });
}
ProcessRequest("torrent-add", arguments, settings);
}
@ -82,8 +102,10 @@ public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration
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 @@ public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration
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 @@ public string GetProtocolVersion(TransmissionSettings settings)
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()}";
return config.Version;
if (force)
{
_versionCache.Remove(cacheKey);
}
return _versionCache.Get(cacheKey, () => GetConfig(settings).Version, TimeSpan.FromHours(6));
}
public TransmissionConfig GetConfig(TransmissionSettings settings)
@ -124,21 +157,36 @@ public TransmissionConfig GetConfig(TransmissionSettings settings)
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.
@ -151,14 +199,9 @@ private TransmissionResponse GetSessionStatistics(TransmissionSettings settings)
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?
@ -179,11 +222,14 @@ private TransmissionResponse GetTorrentStatus(IEnumerable<string> hashStrings, T
"seedIdleLimit",
"seedIdleMode",
"fileCount",
"file-count"
"file-count",
"labels"
};
var arguments = new Dictionary<string, object>();
arguments.Add("fields", fields);
var arguments = new Dictionary<string, object>
{
{ "fields", fields }
};
if (hashStrings != null)
{
@ -195,9 +241,14 @@ private TransmissionResponse GetTorrentStatus(IEnumerable<string> hashStrings, T
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);
@ -212,11 +263,11 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionS
{
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;
@ -244,7 +295,7 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionS
_logger.Debug("Transmission authentication succeeded.");
_authSessionIDCache.Set(authKey, sessionId);
_authSessionIdCache.Set(authKey, sessionId);
}
requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId);

View file

@ -32,6 +32,7 @@ public TransmissionSettings()
Host = "localhost";
Port = 9091;
UrlBase = "/transmission/";
MovieCategory = "radarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@ -59,16 +60,19 @@ public TransmissionSettings()
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")]
public string MovieCategory { get; set; }
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")]
[FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")]
public string MovieImportedCategory { get; set; }
[FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")]
public string MovieDirectory { get; set; }
[FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityMovieHelpText")]
[FieldDefinition(9, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityMovieHelpText")]
public int RecentMoviePriority { get; set; }
[FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityMovieHelpText")]
[FieldDefinition(10, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityMovieHelpText")]
public int OlderMoviePriority { get; set; }
[FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
[FieldDefinition(11, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public override NzbDroneValidationResult Validate()

View file

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Transmission
@ -11,6 +13,7 @@ public class TransmissionTorrent
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; }

View file

@ -15,6 +15,9 @@ public class Vuze : TransmissionBase
{
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,
@ -67,7 +70,5 @@ protected override ValidationFailure ValidateVersion()
return null;
}
public override string Name => "Vuze";
}
}