mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
parent
41c39f1f28
commit
71553ad67b
6 changed files with 842 additions and 1 deletions
|
|
@ -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> Trackers { get; set; }
|
||||
}
|
||||
|
||||
public class DownloadsResponse
|
||||
{
|
||||
public List<Download> 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<int> 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<File> Files { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TriblerDownloadSettings>
|
||||
{
|
||||
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<string, object> { { "clientName", Name }, { "clientVersionRange", "8.0.7" } }), ProviderMessageType.Warning);
|
||||
|
||||
public override bool PreferTorrentFile => false;
|
||||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
var configAsync = _proxy.GetConfig(Settings);
|
||||
|
||||
var items = new List<DownloadClientItem>();
|
||||
|
||||
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<OsPath> { _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<ValidationFailure> 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<string, object> { { "clientName", Name } }))
|
||||
{
|
||||
DetailedDescription = ex.Message
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to test");
|
||||
|
||||
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary<string, object> { { "exception", ex.Message } }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Download> GetDownloads(TriblerDownloadSettings settings);
|
||||
List<File> 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<T>(HttpRequestBuilder requestBuilder)
|
||||
where T : new()
|
||||
{
|
||||
return ProcessRequest<T>(requestBuilder.Build());
|
||||
}
|
||||
|
||||
private T ProcessRequest<T>(HttpRequest requestBuilder)
|
||||
where T : new()
|
||||
{
|
||||
var httpRequest = requestBuilder;
|
||||
|
||||
_logger.Debug("Url: {0}", httpRequest.Url);
|
||||
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Execute(httpRequest);
|
||||
return Json.Deserialize<T>(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<TriblerSettingsResponse>(configRequest);
|
||||
}
|
||||
|
||||
public List<File> GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem)
|
||||
{
|
||||
var filesRequest = GetRequestBuilder(settings, "api/downloads/" + downloadItem.Infohash + "/files");
|
||||
return ProcessRequest<GetFilesResponse>(filesRequest).Files;
|
||||
}
|
||||
|
||||
public List<Download> GetDownloads(TriblerDownloadSettings settings)
|
||||
{
|
||||
var downloadRequest = GetRequestBuilder(settings, "api/downloads");
|
||||
var downloads = ProcessRequest<DownloadsResponse>(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<DeleteDownloadResponse>(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<AddDownloadResponse>(addDownloadRequest).Infohash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TriblerDownloadSettings>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue