- Applied to TorBox, can be extended easily to other providers - Reads response headers commonplace for pre-emptive rate limit throttling and retry-after - When a rate limit is reached, displays a warning in the UI - Fix socket leak from direct allocation of HttpClient, replaced with factory which handles pooling and re-use of sockets. While HttpClient is Disposable, it doesn't gaurantee (and does not) directly release underlying sockets for queries at the time the client is disposed. These sockets will go into a TCP WAIT state often for a very long time. The expected pattern in C# is to always use the HttClientFactory which will correctly handle re-use of the OS sockets in suqsequent queries reducing resource and memory leaks.
331 lines
10 KiB
C#
331 lines
10 KiB
C#
using System.Diagnostics;
|
|
using DebridLinkFrNET;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
using RdtClient.Data.Enums;
|
|
using RdtClient.Data.Models.Data;
|
|
using RdtClient.Data.Models.DebridClient;
|
|
using RdtClient.Data.Models.Internal;
|
|
using RdtClient.Service.Helpers;
|
|
using Download = RdtClient.Data.Models.Data.Download;
|
|
using Torrent = DebridLinkFrNET.Models.Torrent;
|
|
|
|
namespace RdtClient.Service.Services.DebridClients;
|
|
|
|
public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
|
{
|
|
private DebridLinkFrNETClient GetClient()
|
|
{
|
|
try
|
|
{
|
|
var apiKey = Settings.Get.Provider.ApiKey;
|
|
|
|
if (String.IsNullOrWhiteSpace(apiKey))
|
|
{
|
|
throw new("DebridLink API Key not set in the settings");
|
|
}
|
|
|
|
var httpClient = httpClientFactory.CreateClient();
|
|
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
|
|
|
|
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
|
|
|
|
return debridLinkClient;
|
|
}
|
|
catch (AggregateException ae)
|
|
{
|
|
foreach (var inner in ae.InnerExceptions)
|
|
{
|
|
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
|
|
}
|
|
|
|
throw;
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
|
|
|
|
throw;
|
|
}
|
|
catch (TaskCanceledException ex)
|
|
{
|
|
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private DebridClientTorrent Map(Torrent torrent)
|
|
{
|
|
return new()
|
|
{
|
|
Id = torrent.Id ?? "",
|
|
Filename = torrent.Name ?? "",
|
|
OriginalFilename = torrent.Name ?? "",
|
|
Hash = torrent.HashString ?? "",
|
|
Bytes = torrent.TotalSize,
|
|
OriginalBytes = 0,
|
|
Host = torrent.ServerId ?? "",
|
|
Split = 0,
|
|
Progress = torrent.DownloadPercent,
|
|
Status = torrent.Status.ToString(),
|
|
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
|
|
Files = (torrent.Files ?? []).Select((m, i) => new DebridClientFile
|
|
{
|
|
Path = m.Name ?? "",
|
|
Bytes = m.Size,
|
|
Id = i,
|
|
Selected = true,
|
|
DownloadLink = m.DownloadUrl
|
|
})
|
|
.ToList(),
|
|
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
|
|
Ended = null,
|
|
Speed = torrent.UploadSpeed,
|
|
Seeders = torrent.PeersConnected,
|
|
};
|
|
}
|
|
|
|
public async Task<IList<DebridClientTorrent>> GetDownloads()
|
|
{
|
|
var page = 0;
|
|
var results = new List<Torrent>();
|
|
|
|
while (true)
|
|
{
|
|
var pagedResults = await GetClient().Seedbox.ListAsync(null, page, 50);
|
|
|
|
results.AddRange(pagedResults);
|
|
|
|
if (pagedResults.Count == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
page += 1;
|
|
}
|
|
|
|
return results.Select(Map).ToList();
|
|
}
|
|
|
|
public async Task<DebridClientUser> GetUser()
|
|
{
|
|
var user = await GetClient().Account.Infos();
|
|
|
|
return new()
|
|
{
|
|
Username = user.Username,
|
|
Expiration = user.PremiumLeft > 0 ? DateTimeOffset.Now.AddSeconds(user.PremiumLeft) : null
|
|
};
|
|
}
|
|
|
|
public async Task<String> AddTorrentMagnet(String magnetLink)
|
|
{
|
|
try
|
|
{
|
|
var result = await GetClient().Seedbox.AddTorrentAsync(magnetLink);
|
|
|
|
return result.Id ?? "";
|
|
}
|
|
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
|
|
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
|
|
}
|
|
}
|
|
|
|
public async Task<String> AddTorrentFile(Byte[] bytes)
|
|
{
|
|
try
|
|
{
|
|
var result = await GetClient().Seedbox.AddTorrentByFileAsync(bytes);
|
|
|
|
return result.Id ?? "";
|
|
}
|
|
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
|
|
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
|
|
}
|
|
}
|
|
|
|
public Task<String> AddNzbLink(String nzbLink)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public Task<String> AddNzbFile(Byte[] bytes, String? name)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
|
|
{
|
|
return Task.FromResult<IList<DebridClientAvailableFile>>([]);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
|
|
{
|
|
return Task.FromResult<Int32?>(torrent.Files.Count);
|
|
}
|
|
|
|
public async Task Delete(Data.Models.Data.Torrent torrent)
|
|
{
|
|
await GetClient().Seedbox.DeleteAsync(torrent.RdId!);
|
|
}
|
|
|
|
public Task<String> Unrestrict(Data.Models.Data.Torrent torrent, String link)
|
|
{
|
|
return Task.FromResult(link);
|
|
}
|
|
|
|
public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, DebridClientTorrent? torrentClientTorrent)
|
|
{
|
|
try
|
|
{
|
|
if (torrent.RdId == null)
|
|
{
|
|
return torrent;
|
|
}
|
|
|
|
var rdTorrent = await GetInfo(torrent.RdId) ?? throw new($"Resource not found");
|
|
|
|
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
|
|
{
|
|
torrent.RdName = rdTorrent.Filename;
|
|
}
|
|
|
|
if (!String.IsNullOrWhiteSpace(rdTorrent.OriginalFilename))
|
|
{
|
|
torrent.RdName = rdTorrent.OriginalFilename;
|
|
}
|
|
|
|
if (rdTorrent.Bytes > 0)
|
|
{
|
|
torrent.RdSize = rdTorrent.Bytes;
|
|
}
|
|
else if (rdTorrent.OriginalBytes > 0)
|
|
{
|
|
torrent.RdSize = rdTorrent.OriginalBytes;
|
|
}
|
|
|
|
if (rdTorrent.Files != null)
|
|
{
|
|
torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files);
|
|
}
|
|
|
|
torrent.ClientKind = Provider.DebridLink;
|
|
torrent.RdHost = rdTorrent.Host;
|
|
torrent.RdSplit = rdTorrent.Split;
|
|
torrent.RdProgress = rdTorrent.Progress;
|
|
torrent.RdAdded = rdTorrent.Added;
|
|
torrent.RdEnded = rdTorrent.Ended;
|
|
torrent.RdSpeed = rdTorrent.Speed;
|
|
torrent.RdSeeders = rdTorrent.Seeders;
|
|
torrent.RdStatusRaw = rdTorrent.Status;
|
|
|
|
/*
|
|
0 Torrent is stopped
|
|
1 Torrent is queued to verify local data
|
|
2 Torrent is verifying local data
|
|
3 Torrent is queued to download
|
|
4 Torrent is downloading
|
|
5 Torrent is queued to seed
|
|
6 Torrent is seeding
|
|
100 Torrent is stored
|
|
*/
|
|
|
|
torrent.RdStatus = rdTorrent.Status switch
|
|
{
|
|
"100" => TorrentStatus.Finished,
|
|
"1" => TorrentStatus.Processing,
|
|
"2" => TorrentStatus.Processing,
|
|
"3" => TorrentStatus.Processing,
|
|
"4" => TorrentStatus.Downloading,
|
|
"5" => TorrentStatus.Finished,
|
|
"6" => TorrentStatus.Finished,
|
|
_ => TorrentStatus.Error
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message == "Resource not found")
|
|
{
|
|
torrent.RdStatusRaw = "deleted";
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
return torrent;
|
|
}
|
|
|
|
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
|
|
{
|
|
if (torrent.RdId == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var rdTorrent = await GetInfo(torrent.RdId);
|
|
|
|
if (rdTorrent.Links == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return rdTorrent.Files?
|
|
.Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
|
|
.Select(m => new DownloadInfo
|
|
{
|
|
RestrictedLink = m.DownloadLink!,
|
|
FileName = Path.GetFileName(m.Path)
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<String> GetFileName(Download download)
|
|
{
|
|
// FileName is set in GetDownlaadInfos
|
|
Debug.Assert(download.FileName != null);
|
|
|
|
return Task.FromResult(download.FileName);
|
|
}
|
|
|
|
private async Task<DebridClientTorrent> GetInfo(String torrentId)
|
|
{
|
|
var result = await GetClient().Seedbox.ListAsync(torrentId);
|
|
|
|
return Map(result.First());
|
|
}
|
|
|
|
private void Log(String message, Data.Models.Data.Torrent? torrent = null)
|
|
{
|
|
if (torrent != null)
|
|
{
|
|
message = $"{message} {torrent.ToLog()}";
|
|
}
|
|
|
|
logger.LogDebug(message);
|
|
}
|
|
|
|
public static String? GetSymlinkPath(Data.Models.Data.Torrent torrent, Download download)
|
|
{
|
|
if (torrent.RdName == null || download.FileName == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Single file torrents always have that file at `/mnt-root/Seedbox/filename.ext`
|
|
if (torrent.Files.Count == 1)
|
|
{
|
|
return download.FileName;
|
|
}
|
|
|
|
return Path.Combine(torrent.RdName, download.FileName);
|
|
}
|
|
}
|