- 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.
388 lines
13 KiB
C#
388 lines
13 KiB
C#
using System.Diagnostics;
|
|
using AllDebridNET;
|
|
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 File = AllDebridNET.File;
|
|
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
|
|
|
namespace RdtClient.Service.Services.DebridClients;
|
|
|
|
public interface IAllDebridNetClientFactory
|
|
{
|
|
public IAllDebridNETClient GetClient();
|
|
}
|
|
|
|
public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger, IHttpClientFactory httpClientFactory) : IAllDebridNetClientFactory
|
|
{
|
|
public IAllDebridNETClient GetClient()
|
|
{
|
|
try
|
|
{
|
|
var apiKey = Settings.Get.Provider.ApiKey;
|
|
|
|
if (String.IsNullOrWhiteSpace(apiKey))
|
|
{
|
|
throw new("All-Debrid API Key not set in the settings");
|
|
}
|
|
|
|
var httpClient = httpClientFactory.CreateClient();
|
|
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
|
|
|
var allDebridNetClient = new AllDebridNETClient("RealDebridClient", apiKey, httpClient);
|
|
|
|
return allDebridNetClient;
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
logger.LogError(ex, $"The connection to AllDebrid has timed out: {ex.Message}");
|
|
|
|
throw;
|
|
}
|
|
catch (TaskCanceledException ex)
|
|
{
|
|
logger.LogError(ex, $"The connection to AllDebrid has timed out: {ex.Message}");
|
|
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
|
{
|
|
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
private static List<DebridClientTorrent> _cache = [];
|
|
private static Int64 _sessionCounter = 0;
|
|
|
|
private static DebridClientTorrent Map(Magnet torrent)
|
|
{
|
|
var files = GetFiles(torrent.Files);
|
|
return new()
|
|
{
|
|
Id = torrent.Id.ToString(),
|
|
Filename = torrent.Filename ?? "",
|
|
OriginalFilename = torrent.Filename,
|
|
Hash = torrent.Hash ?? "",
|
|
Bytes = torrent.Size ?? 0,
|
|
OriginalBytes = torrent.Size ?? 0,
|
|
Host = null,
|
|
Split = 0,
|
|
Progress = (Int64)Math.Round((torrent.Downloaded ?? 0) * 100.0 / (torrent.Size ?? 1)),
|
|
Status = torrent.Status,
|
|
StatusCode = torrent.StatusCode ?? 0,
|
|
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
|
|
Files = files,
|
|
Links = [],
|
|
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
|
|
Speed = torrent.DownloadSpeed,
|
|
Seeders = torrent.Seeders
|
|
};
|
|
}
|
|
|
|
public async Task<IList<DebridClientTorrent>> GetDownloads()
|
|
{
|
|
var results = await allDebridNetClientFactory.GetClient().Magnet.StatusLiveAsync(SessionId, _sessionCounter);
|
|
|
|
_sessionCounter = results.Counter;
|
|
|
|
if (results.Fullsync == true)
|
|
{
|
|
_cache = (results.Magnets ?? []).Select(Map).ToList();
|
|
}
|
|
else
|
|
{
|
|
// Replace the existing items in the cache with the new ones based on the ID
|
|
foreach (var result in results.Magnets ?? [])
|
|
{
|
|
var existing = _cache.FirstOrDefault(m => m.Id == result.Id.ToString());
|
|
|
|
if (existing != null)
|
|
{
|
|
_cache.Remove(existing);
|
|
}
|
|
|
|
_cache.Add(Map(result));
|
|
}
|
|
}
|
|
|
|
_cache = _cache.Where(m => m.Status != null).ToList();
|
|
|
|
return _cache;
|
|
}
|
|
|
|
public async Task<DebridClientUser> GetUser()
|
|
{
|
|
var user = await allDebridNetClientFactory.GetClient().User.GetAsync() ?? throw new("Unable to get user");
|
|
|
|
return new()
|
|
{
|
|
Username = user.Username,
|
|
Expiration = user.PremiumUntil > 0 ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(user.PremiumUntil) : null
|
|
};
|
|
}
|
|
|
|
public async Task<String> AddTorrentMagnet(String magnetLink)
|
|
{
|
|
try
|
|
{
|
|
var result = await allDebridNetClientFactory.GetClient().Magnet.UploadMagnetAsync(magnetLink);
|
|
|
|
if (result?.Id == null)
|
|
{
|
|
throw new("Unable to add magnet link");
|
|
}
|
|
|
|
var resultId = result.Id.ToString() ?? throw new($"Invalid responseID {result.Id}");
|
|
|
|
return resultId;
|
|
}
|
|
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 allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes);
|
|
|
|
if (result?.Id == null)
|
|
{
|
|
throw new("Unable to add torrent file");
|
|
}
|
|
|
|
var resultId = result.Id.ToString() ?? throw new($"Invalid responseID {result.Id}");
|
|
|
|
return resultId;
|
|
}
|
|
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(Torrent torrent)
|
|
{
|
|
return Task.FromResult<Int32?>(torrent.Files.Count);
|
|
}
|
|
|
|
public async Task Delete(Torrent torrent)
|
|
{
|
|
await allDebridNetClientFactory.GetClient().Magnet.DeleteAsync(torrent.RdId!);
|
|
}
|
|
|
|
public async Task<String> Unrestrict(Torrent torrent, String link)
|
|
{
|
|
var result = await allDebridNetClientFactory.GetClient().Links.DownloadLinkAsync(link);
|
|
|
|
if (result.Link == null)
|
|
{
|
|
throw new("Invalid result link");
|
|
}
|
|
|
|
return result.Link;
|
|
}
|
|
|
|
public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
|
|
{
|
|
try
|
|
{
|
|
if (torrent.RdId == null)
|
|
{
|
|
return torrent;
|
|
}
|
|
|
|
torrentClientTorrent ??= await GetInfo(torrent.RdId);
|
|
|
|
if (!String.IsNullOrWhiteSpace(torrentClientTorrent.Filename))
|
|
{
|
|
torrent.RdName = torrentClientTorrent.Filename;
|
|
}
|
|
|
|
if (torrentClientTorrent.Bytes > 0)
|
|
{
|
|
torrent.RdSize = torrentClientTorrent.Bytes;
|
|
}
|
|
|
|
if (torrentClientTorrent.Files != null && torrentClientTorrent.Files.Count != 0)
|
|
{
|
|
torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files);
|
|
}
|
|
|
|
torrent.ClientKind = Provider.AllDebrid;
|
|
torrent.RdHost = torrentClientTorrent.Host;
|
|
torrent.RdSplit = torrentClientTorrent.Split;
|
|
torrent.RdProgress = torrentClientTorrent.Progress;
|
|
torrent.RdAdded = torrentClientTorrent.Added;
|
|
torrent.RdEnded = torrentClientTorrent.Ended;
|
|
torrent.RdSpeed = torrentClientTorrent.Speed;
|
|
torrent.RdSeeders = torrentClientTorrent.Seeders;
|
|
torrent.RdStatusRaw = torrentClientTorrent.Status;
|
|
|
|
torrent.RdStatus = torrentClientTorrent.StatusCode switch
|
|
{
|
|
0 => TorrentStatus.Processing, // Processing In Queue.
|
|
1 => TorrentStatus.Downloading, // Processing Downloading.
|
|
2 => TorrentStatus.Downloading, // Processing Compressing / Moving.
|
|
3 => TorrentStatus.Uploading, // Processing Uploading.
|
|
4 => TorrentStatus.Finished, // Finished Ready.
|
|
5 => TorrentStatus.Error, // Error Upload fail.
|
|
6 => TorrentStatus.Error, // Error Internal error on unpacking.
|
|
7 => TorrentStatus.Error, // Error Not downloaded in 20 min.
|
|
8 => TorrentStatus.Error, // Error File too big.
|
|
9 => TorrentStatus.Error, // Error Internal error.
|
|
10 => TorrentStatus.Error, // Error Download took more than 72h.
|
|
11 => TorrentStatus.Error, // Error Deleted on the hoster website
|
|
_ => TorrentStatus.Error
|
|
};
|
|
}
|
|
catch (AllDebridException ex)
|
|
{
|
|
if (ex.ErrorCode == "MAGNET_INVALID_ID")
|
|
{
|
|
torrent.RdStatusRaw = "deleted";
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
return torrent;
|
|
}
|
|
|
|
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
|
|
{
|
|
if (torrent.RdId == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Log($"Getting download links", torrent);
|
|
|
|
var allFiles = await allDebridNetClientFactory.GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
|
|
|
|
var files = GetFiles(allFiles);
|
|
|
|
return files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes) && f.DownloadLink != null)
|
|
.Select(f => new DownloadInfo
|
|
{
|
|
FileName = Path.GetFileName(f.Path),
|
|
RestrictedLink = f.DownloadLink!
|
|
})
|
|
.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 allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
|
|
|
|
return Map(result);
|
|
}
|
|
private static List<DebridClientFile> GetFiles(List<File>? files, String parentPath = "")
|
|
{
|
|
if (files == null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return files.SelectMany(file =>
|
|
{
|
|
var currentPath = String.IsNullOrEmpty(parentPath)
|
|
? file.FolderOrFileName
|
|
: Path.Combine(parentPath, file.FolderOrFileName);
|
|
|
|
var result = new List<DebridClientFile>();
|
|
|
|
// If it's a file (has size)
|
|
if (file.Size.HasValue)
|
|
{
|
|
result.Add(new()
|
|
{
|
|
Path = currentPath,
|
|
Bytes = file.Size.Value,
|
|
DownloadLink = file.DownloadLink
|
|
});
|
|
}
|
|
|
|
// Process sub-nodes if they exist
|
|
if (file.SubNodes != null)
|
|
{
|
|
result.AddRange(GetFiles(file.SubNodes, currentPath));
|
|
}
|
|
|
|
return result;
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
private void Log(String message, Torrent? torrent = null)
|
|
{
|
|
if (torrent != null)
|
|
{
|
|
message = $"{message} {torrent.ToLog()}";
|
|
}
|
|
|
|
logger.LogDebug(message);
|
|
}
|
|
|
|
public static String? GetSymlinkPath(Torrent torrent, Download download)
|
|
{
|
|
var fileName = DownloadHelper.GetFileName(download);
|
|
|
|
if (fileName == null || torrent.RdName == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var directory = DownloadHelper.RemoveInvalidPathChars(torrent.RdName);
|
|
|
|
var matchingTorrentFiles = torrent.Files.Where(m => m.Path.EndsWith(fileName)).Where(m => !String.IsNullOrWhiteSpace(m.Path)).ToList();
|
|
|
|
if (matchingTorrentFiles.Count == 0)
|
|
{
|
|
throw new($"Could not find file {fileName} in {torrent.RdName}");
|
|
}
|
|
|
|
// Torrents with a single file in them don't need to have the `RdName` added
|
|
if (torrent.Files.Count == 1)
|
|
{
|
|
return matchingTorrentFiles[0].Path;
|
|
}
|
|
|
|
return Path.Combine(directory, matchingTorrentFiles[0].Path);
|
|
}
|
|
}
|