diff --git a/src/NzbDrone.Core/Download/Clients/Tribler/Models/TriblerDownloadClientApi.cs b/src/NzbDrone.Core/Download/Clients/Tribler/Models/TriblerDownloadClientApi.cs new file mode 100644 index 000000000..f3efa974c --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Tribler/Models/TriblerDownloadClientApi.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace NzbDrone.Core.Download.Clients.Tribler +{ + public enum DownloadStatus + { + [EnumMember(Value = @"WAITING4HASHCHECK")] + Waiting4HashCheck = 0, + + [EnumMember(Value = @"HASHCHECKING")] + Hashchecking = 1, + + [EnumMember(Value = @"METADATA")] + Metadata = 2, + + [EnumMember(Value = @"DOWNLOADING")] + Downloading = 3, + + [EnumMember(Value = @"SEEDING")] + Seeding = 4, + + [EnumMember(Value = @"STOPPED")] + Stopped = 5, + + [EnumMember(Value = @"ALLOCATING_DISKSPACE")] + AllocatingDiskspace = 6, + + [EnumMember(Value = @"EXIT_NODES")] + Exitnodes = 7, + + [EnumMember(Value = @"CIRCUITS")] + Circuits = 8, + + [EnumMember(Value = @"STOPPED_ON_ERROR")] + StoppedOnError = 9, + + [EnumMember(Value = @"LOADING")] + Loading = 10, + } + + public class Trackers + { + public string Url { get; set; } + [JsonProperty("peers")] + public object Peers { get; set; } + [JsonProperty("status")] + public string Status { get; set; } + } + + public class Download + { + public string Name { get; set; } + public float? Progress { get; set; } + public string Infohash { get; set; } + public bool? AnonDownload { get; set; } + public float? Availability { get; set; } + public double? Eta { get; set; } + public long? TotalPieces { get; set; } + public long? NumSeeds { get; set; } + public long? AllTimeUpload { get; set; } + public long? AllTimeDownload { get; set; } + + [JsonProperty("status")] + [JsonConverter(typeof(StringEnumConverter))] + public DownloadStatus? Status { get; set; } + + public int? StatusCode { get; set; } + public float? AllTimeRatio { get; set; } + public long? TimeAdded { get; set; } + public long? MaxUploadSpeed { get; set; } + public long? MaxDownloadSpeed { get; set; } + public long? Hops { get; set; } + public bool? SafeSeeding { get; set; } + public string Error { get; set; } + public long? TotalDown { get; set; } + public long? Size { get; set; } + public string Destination { get; set; } + public float? SpeedDown { get; set; } + public float? SpeedUp { get; set; } + public long? NumPeers { get; set; } + public List Trackers { get; set; } + } + + public class DownloadsResponse + { + public List Downloads { get; set; } + } + + public class AddDownloadRequest + { + [JsonProperty("anon_hops")] + public long? AnonymityHops { get; set; } + + [JsonProperty("safe_seeding")] + public bool? SafeSeeding { get; set; } + public string Destination { get; set; } + + [JsonProperty("uri", Required = Newtonsoft.Json.Required.Always)] + [Required(AllowEmptyStrings = true)] + public string Uri { get; set; } + } + + public class AddDownloadResponse + { + public string Infohash { get; set; } + public bool? Started { get; set; } + } + + public class RemoveDownloadRequest + { + [JsonProperty("remove_data")] + public bool? RemoveData { get; set; } + } + + public class DeleteDownloadResponse + { + public bool? Removed { get; set; } + public string Infohash { get; set; } + } + + public class UpdateDownloadRequest + { + [JsonProperty("anon_hops")] + public long? AnonHops { get; set; } + + [JsonProperty("selected_files")] + public List Selected_files { get; set; } + + public string State { get; set; } + } + + public class UpdateDownloadResponse + { + public bool? Modified { get; set; } + public string Infohash { get; set; } + } + + public class File + { + public long? Size { get; set; } + public long? Index { get; set; } + public string Name { get; set; } + public float? Progress { get; set; } + public bool? Included { get; set; } + } + + public class GetFilesResponse + { + public List Files { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Tribler/Models/TriblerSettingsApi.cs b/src/NzbDrone.Core/Download/Clients/Tribler/Models/TriblerSettingsApi.cs new file mode 100644 index 000000000..fc689ceef --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Tribler/Models/TriblerSettingsApi.cs @@ -0,0 +1,189 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace NzbDrone.Core.Indexers.Tribler +{ + public class TriblerSettingsResponse + { + public Settings Settings { get; set; } + } + + public class Settings + { + public Api Api { get; set; } + public bool Statistics { get; set; } + + [JsonProperty("content_discovery_community")] + public ContentDiscoveryCommunity ContentDiscoveryCommunity { get; set; } + public Database Database { get; set; } + + [JsonProperty("dht_discovery")] + public DHTDiscovery DHTDiscovery { get; set; } + + [JsonProperty("knowledge_community")] + public KnowledgeCommunity KnowledgeCommunity { get; set; } + public LibTorrent LibTorrent { get; set; } + public Recommender Recommender { get; set; } + public Rendezvous RecoRendezvousmmender { get; set; } + + [JsonProperty("torrent_checker")] + public TorrentChecker TorrentChecker { get; set; } + + [JsonProperty("tunnel_community")] + public TunnelCommunity TunnelCommunity { get; set; } + + public Versioning Versioning { get; set; } + + [JsonProperty("watch_folder")] + public WatchFolder WatchFolder { get; set; } + + [JsonProperty("state_dir")] + public string StateDir { get; set; } + + [JsonProperty("memory_db")] + public bool? MemoryDB { get; set; } + } + + public class Api + { + [JsonProperty("http_enabled")] + public bool HttpEnabled { get; set; } + + [JsonProperty("http_port")] + public int HttpPort { get; set; } + + [JsonProperty("http_host")] + public string HttpHost { get; set; } + + [JsonProperty("https_enabled")] + public bool HttpsEnabled { get; set; } + + [JsonProperty("https_port")] + public int HttpsPort { get; set; } + + [JsonProperty("https_host")] + public string HttpsHost { get; set; } + + [JsonProperty("https_certfile")] + public string HttpsCertFile { get; set; } + + [JsonProperty("http_port_running")] + public int HttpPortRunning { get; set; } + + [JsonProperty("https_port_running")] + public int HttpsPortRunning { get; set; } + } + + public class ContentDiscoveryCommunity + { + public bool? Enabled { get; set; } + } + + public class Database + { + public bool? Enabled { get; set; } + } + + public class DHTDiscovery + { + public bool? Enabled { get; set; } + } + + public class KnowledgeCommunity + { + public bool? Enabled { get; set; } + } + + public class LibTorrent + { + [JsonProperty("download_defaults")] + public LibTorrentDownloadDefaults DownloadDefaults { get; set; } + + // contains a lot more data, but it's not needed currently + } + + public class Recommender + { + public bool? Enabled { get; set; } + } + + public class Rendezvous + { + public bool? Enabled { get; set; } + } + + public class TorrentChecker + { + [JsonProperty("enabled")] + public bool? Enabled { get; set; } + } + + public class TunnelCommunity + { + [JsonProperty("enabled")] + public bool? Enabled { get; set; } + + [JsonProperty("min_circuits")] + public int? MinCircuits { get; set; } + + [JsonProperty("max_circuits")] + public int? MaxCircuits { get; set; } + } + + public class Versioning + { + [JsonProperty("enabled")] + public bool? Enabled { get; set; } + } + + public class WatchFolder + { + [JsonProperty("enabled")] + public bool? Enabled { get; set; } + [JsonProperty("directory")] + public string Directory { get; set; } + [JsonProperty("check_interval")] + public int? CheckInterval { get; set; } + } + + public class LibTorrentDownloadDefaults + { + [JsonProperty("anonymity_enabled")] + public bool? AnonymityEnabled { get; set; } + + [JsonProperty("number_hops")] + public int? NumberHops { get; set; } + + [JsonProperty("safeseeding_enabled")] + public bool? SafeSeedingEnabled { get; set; } + + [JsonProperty("saveas")] + public string SaveAS { get; set; } + + [JsonProperty("seeding_mode")] + [JsonConverter(typeof(StringEnumConverter))] + public DownloadDefaultsSeedingMode? SeedingMode { get; set; } + + [JsonProperty("seeding_ratio")] + public double? SeedingRatio { get; set; } + + [JsonProperty("seeding_time")] + public double? SeedingTime { get; set; } + } + + public enum DownloadDefaultsSeedingMode + { + [EnumMember(Value = @"ratio")] + Ratio = 0, + + [EnumMember(Value = @"forever")] + Forever = 1, + + [EnumMember(Value = @"time")] + Time = 2, + + [EnumMember(Value = @"never")] + Never = 3, + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadClient.cs b/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadClient.cs new file mode 100644 index 000000000..4046dec3a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadClient.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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; +using NzbDrone.Core.Indexers.Tribler; +using NzbDrone.Core.Localization; +using NzbDrone.Core.MediaFiles.TorrentInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Download.Clients.Tribler +{ + public class TriblerDownloadClient : TorrentClientBase + { + private readonly ITriblerDownloadClientProxy _proxy; + + public TriblerDownloadClient( + ITriblerDownloadClientProxy triblerDownloadClientProxy, + ITorrentFileInfoReader torrentFileInfoReader, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, + Logger logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) + { + _proxy = triblerDownloadClientProxy; + } + + public override string Name => "Tribler"; + + public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientTriblerProviderMessage", new Dictionary { { "clientName", Name }, { "clientVersionRange", "8.0.7" } }), ProviderMessageType.Warning); + + public override bool PreferTorrentFile => false; + + public override IEnumerable GetItems() + { + var configAsync = _proxy.GetConfig(Settings); + + var items = new List(); + + var downloads = _proxy.GetDownloads(Settings); + + foreach (var download in downloads) + { + // If totalsize == 0 the torrent is a magnet downloading metadata + if (download.Size == null || download.Size == 0) + { + continue; + } + + var item = new DownloadClientItem + { + DownloadId = download.Infohash, + Title = download.Name, + + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false) + }; + + // some concurrency could make this faster. + var files = _proxy.GetDownloadFiles(Settings, download); + + item.OutputPath = new OsPath(download.Destination); + + if (files.Count == 1) + { + item.OutputPath += files.First().Name; + } + else + { + item.OutputPath += item.Title; + } + + item.TotalSize = (long)download.Size; + item.RemainingSize = (long)(download.Size * (1 - download.Progress)); + item.SeedRatio = download.AllTimeRatio; + + if (download.Eta.HasValue) + { + if (download.Eta.Value >= TimeSpan.FromDays(365).TotalSeconds) + { + item.RemainingTime = TimeSpan.FromDays(365); + } + else if (download.Eta.Value < 0) + { + item.RemainingTime = TimeSpan.FromSeconds(0); + } + else + { + item.RemainingTime = TimeSpan.FromSeconds(download.Eta.Value); + } + } + + item.Message = download.Error; + + // tribler always saves files unencrypted to disk. + item.IsEncrypted = false; + + switch (download.Status) + { + case DownloadStatus.Hashchecking: + case DownloadStatus.Waiting4HashCheck: + case DownloadStatus.Circuits: + case DownloadStatus.Exitnodes: + case DownloadStatus.Downloading: + item.Status = DownloadItemStatus.Downloading; + break; + case DownloadStatus.Metadata: + case DownloadStatus.AllocatingDiskspace: + item.Status = DownloadItemStatus.Queued; + break; + case DownloadStatus.Seeding: + case DownloadStatus.Stopped: + item.Status = DownloadItemStatus.Completed; + break; + case DownloadStatus.StoppedOnError: + item.Status = DownloadItemStatus.Failed; + break; + case DownloadStatus.Loading: + default: // new status in API? default to downloading + item.Message = "Unknown download state: " + download.Status; + _logger.Info(item.Message); + item.Status = DownloadItemStatus.Downloading; + break; + } + + // Override status if completed, but not finished downloading + if (download.Status == DownloadStatus.Stopped && download.Progress < 1) + { + item.Status = DownloadItemStatus.Paused; + } + + if (download.Error != null && download.Error.Length > 0) + { + item.Status = DownloadItemStatus.Warning; + item.Message = download.Error; + } + + item.CanBeRemoved = item.CanMoveFiles = HasReachedSeedLimit(download, configAsync); + + items.Add(item); + } + + return items; + } + + public override void RemoveItem(DownloadClientItem item, bool deleteData) + { + _proxy.RemoveDownload(Settings, item, deleteData); + } + + public override DownloadClientInfo GetStatus() + { + var config = _proxy.GetConfig(Settings); + var destDir = config.Settings.LibTorrent.DownloadDefaults.SaveAS; + + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + destDir = string.Format("{0}/.{1}", destDir, Settings.TvCategory); + } + + return new DownloadClientInfo + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", + OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) } + }; + } + + protected static bool HasReachedSeedLimit(Download torrent, TriblerSettingsResponse config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (torrent == null) + { + throw new ArgumentNullException(nameof(torrent)); + } + + // if download is still running then it's not finished. + if (torrent.Status != DownloadStatus.Stopped) + { + return false; + } + + switch (config.Settings.LibTorrent.DownloadDefaults.SeedingMode) + { + case DownloadDefaultsSeedingMode.Ratio: + + return torrent.AllTimeRatio.HasValue + && torrent.AllTimeRatio >= config.Settings.LibTorrent.DownloadDefaults.SeedingRatio; + + case DownloadDefaultsSeedingMode.Time: + var downloadStarted = DateTimeOffset.FromUnixTimeSeconds(torrent.TimeAdded.Value); + var maxSeedingTime = TimeSpan.FromSeconds(config.Settings.LibTorrent.DownloadDefaults.SeedingTime ?? 0); + + return torrent.TimeAdded.HasValue + && downloadStarted.Add(maxSeedingTime) < DateTimeOffset.Now; + + case DownloadDefaultsSeedingMode.Never: + return true; + + case DownloadDefaultsSeedingMode.Forever: + default: + return false; + } + } + + protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) + { + var addDownloadRequestObject = new AddDownloadRequest + { + Destination = GetDownloadDirectory(), + Uri = magnetLink, + SafeSeeding = Settings.SafeSeeding, + AnonymityHops = Settings.AnonymityLevel + }; + + return _proxy.AddFromMagnetLink(Settings, addDownloadRequestObject); + } + + protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + { + // TODO: Tribler 8.x does support adding from a torrent file, but it's not a simple put command. + throw new NotSupportedException("Tribler does not support torrent files, only magnet links"); + } + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + + if (failures.HasErrors()) + { + return; + } + } + + protected string GetDownloadDirectory() + { + if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) + { + return Settings.TvDirectory; + } + + if (!Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + return null; + } + + var config = _proxy.GetConfig(Settings); + var destDir = config.Settings.LibTorrent.DownloadDefaults.SaveAS; + + return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}"; + } + + protected ValidationFailure TestConnection() + { + try + { + var downloads = GetItems(); + return null; + } + catch (DownloadClientAuthenticationException ex) + { + _logger.Error(ex, ex.Message); + + return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("DownloadClientValidationApiKeyIncorrect")); + } + catch (DownloadClientUnavailableException ex) + { + _logger.Error(ex, ex.Message); + + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) + { + DetailedDescription = ex.Message + }; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to test"); + + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); + } + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadClientProxy.cs b/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadClientProxy.cs new file mode 100644 index 000000000..4c5d4aec4 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadClientProxy.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Net.Http; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Indexers.Tribler; + +namespace NzbDrone.Core.Download.Clients.Tribler +{ + public interface ITriblerDownloadClientProxy + { + List GetDownloads(TriblerDownloadSettings settings); + List GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem); + TriblerSettingsResponse GetConfig(TriblerDownloadSettings settings); + void RemoveDownload(TriblerDownloadSettings settings, DownloadClientItem item, bool deleteData); + string AddFromMagnetLink(TriblerDownloadSettings settings, AddDownloadRequest downloadRequest); + } + + public class TriblerDownloadClientProxy : ITriblerDownloadClientProxy + { + protected readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public TriblerDownloadClientProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + private HttpRequestBuilder GetRequestBuilder(TriblerDownloadSettings settings, string relativePath = null) + { + var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); + baseUrl = HttpUri.CombinePath(baseUrl, relativePath); + + var requestBuilder = new HttpRequestBuilder(baseUrl) + .Accept(HttpAccept.Json); + + requestBuilder.Headers.Add("X-Api-Key", settings.ApiKey); + requestBuilder.LogResponseContent = true; + + return requestBuilder; + } + + private T ProcessRequest(HttpRequestBuilder requestBuilder) + where T : new() + { + return ProcessRequest(requestBuilder.Build()); + } + + private T ProcessRequest(HttpRequest requestBuilder) + where T : new() + { + var httpRequest = requestBuilder; + + _logger.Debug("Url: {0}", httpRequest.Url); + + try + { + var response = _httpClient.Execute(httpRequest); + return Json.Deserialize(response.Content); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new DownloadClientAuthenticationException("Unauthorized - AuthToken is invalid", ex); + } + + throw new DownloadClientUnavailableException("Unable to connect to Tribler. Status Code: {0}", ex.Response.StatusCode, ex); + } + } + + public TriblerSettingsResponse GetConfig(TriblerDownloadSettings settings) + { + var configRequest = GetRequestBuilder(settings, "api/settings"); + return ProcessRequest(configRequest); + } + + public List GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem) + { + var filesRequest = GetRequestBuilder(settings, "api/downloads/" + downloadItem.Infohash + "/files"); + return ProcessRequest(filesRequest).Files; + } + + public List GetDownloads(TriblerDownloadSettings settings) + { + var downloadRequest = GetRequestBuilder(settings, "api/downloads"); + var downloads = ProcessRequest(downloadRequest); + return downloads.Downloads; + } + + public void RemoveDownload(TriblerDownloadSettings settings, DownloadClientItem item, bool deleteData) + { + var deleteDownloadRequestObject = new RemoveDownloadRequest + { + RemoveData = deleteData + }; + + var deleteRequestBuilder = GetRequestBuilder(settings, "api/downloads/" + item.DownloadId.ToLower()); + deleteRequestBuilder.Method = HttpMethod.Delete; + + var deleteRequest = deleteRequestBuilder.Build(); + deleteRequest.SetContent(Json.ToJson(deleteDownloadRequestObject)); + + ProcessRequest(deleteRequest); + } + + public string AddFromMagnetLink(TriblerDownloadSettings settings, AddDownloadRequest downloadRequest) + { + var addDownloadRequestBuilder = GetRequestBuilder(settings, "api/downloads"); + addDownloadRequestBuilder.Method = HttpMethod.Put; + + var addDownloadRequest = addDownloadRequestBuilder.Build(); + addDownloadRequest.SetContent(Json.ToJson(downloadRequest)); + + return ProcessRequest(addDownloadRequest).Infohash; + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadSettings.cs b/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadSettings.cs new file mode 100644 index 000000000..11a5da8f8 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Tribler/TriblerDownloadSettings.cs @@ -0,0 +1,74 @@ +using System.Text.RegularExpressions; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Download.Clients.Tribler +{ + public class TriblerSettingsValidator : AbstractValidator + { + public TriblerSettingsValidator() + { + RuleFor(c => c.Host).ValidHost(); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); + RuleFor(c => c.UrlBase).ValidUrlBase(); + RuleFor(c => c.ApiKey).NotEmpty(); + RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -"); + RuleFor(c => c.TvCategory).Empty() + .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) + .WithMessage("Cannot use Category and Directory"); + RuleFor(c => c.AnonymityLevel).GreaterThanOrEqualTo(0); + } + } + + public class TriblerDownloadSettings : IProviderConfig + { + private static readonly TriblerSettingsValidator Validator = new TriblerSettingsValidator(); + + public TriblerDownloadSettings() + { + Host = "localhost"; + Port = 20100; + UrlBase = ""; + AnonymityLevel = 1; + SafeSeeding = true; + } + + [FieldDefinition(1, Label = "Host", Type = FieldType.Textbox)] + public string Host { get; set; } + + [FieldDefinition(2, Label = "Port", Type = FieldType.Textbox)] + public int Port { get; set; } + + [FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + public bool UseSsl { get; set; } + + [FieldDefinition(4, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Tribler")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]")] + public string UrlBase { get; set; } + + [FieldDefinition(5, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, HelpText = "DownloadClientTriblerSettingsApiKeyHelpText")] + public string ApiKey { get; set; } + + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] + public string TvCategory { get; set; } + + [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTriblerSettingsDirectoryHelpText")] + public string TvDirectory { get; set; } + + [FieldDefinition(8, Label = "DownloadClientTriblerSettingsAnonymityLevel", Type = FieldType.Number, HelpText = "DownloadClientTriblerSettingsAnonymityLevelHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientTriblerSettingsAnonymityLevel", "url", "https://www.tribler.org/anonymity.html")] + public int AnonymityLevel { get; set; } + + [FieldDefinition(9, Label = "DownloadClientTriblerSettingsSafeSeeding", Type = FieldType.Checkbox, HelpText = "DownloadClientTriblerSettingsSafeSeedingHelpText")] + public bool SafeSeeding { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f8ac021b6..fb01f2422 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -549,7 +549,13 @@ "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}", "DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'", - "DownloadClientUTorrentProviderMessage": "uTorrent has a history of including cryptominers, malware and ads, we strongly encourage you to choose a different client.", + "DownloadClientTriblerSettingsAnonymityLevel": "Anonymity level", + "DownloadClientTriblerSettingsAnonymityLevelHelpText": "Number of proxies to use when downloading content. To disable set to 0. Proxies reduce download/upload speed. See {url}", + "DownloadClientTriblerSettingsApiKeyHelpText": "[api].key from triblerd.conf", + "DownloadClientTriblerSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Tribler location", + "DownloadClientTriblerSettingsSafeSeeding": "Safe Seeding", + "DownloadClientTriblerSettingsSafeSeedingHelpText": "When enabled, only seed through proxies.", + "DownloadClientTriblerProviderMessage": "The tribler integration is highly experimental. Tested against {clientName} version {clientVersionRange}.", "DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error", "DownloadClientUnavailable": "Download Client Unavailable", "DownloadClientValidationApiKeyIncorrect": "API Key Incorrect",