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 logger, IHttpClientFactory httpClientFactory, ISettings settings) : IAllDebridNetClientFactory { public IAllDebridNETClient GetClient() { try { var apiKey = settings.Current.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 logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient { private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); private static List _cache = []; private static Int64 _sessionCounter; public async Task> 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 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 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 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 AddNzbLink(String nzbLink) { throw new NotSupportedException(); } public Task AddNzbFile(Byte[] bytes, String? name) { throw new NotSupportedException(); } public Task> GetAvailableFiles(String hash) { return Task.FromResult>([]); } /// public Task SelectFiles(Torrent torrent) { return Task.FromResult(torrent.Files.Count); } public async Task Delete(Torrent torrent) { await allDebridNetClientFactory.GetClient().Magnet.DeleteAsync(torrent.RdId!); } public async Task 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 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?> 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(); } /// public Task GetFileName(Download download) { // FileName is set in GetDownlaadInfos Debug.Assert(download.FileName != null); return Task.FromResult(download.FileName); } 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 }; } private async Task 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 GetFiles(List? 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(); // 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); } }