390 lines
13 KiB
C#
390 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, 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<AllDebridDebridClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
|
{
|
|
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
private static List<DebridClientTorrent> _cache = [];
|
|
private static Int64 _sessionCounter;
|
|
|
|
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 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<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);
|
|
}
|
|
}
|