418 lines
14 KiB
C#
418 lines
14 KiB
C#
using System.Web;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
using RDNET;
|
|
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 = RDNET.Torrent;
|
|
|
|
namespace RdtClient.Service.Services.DebridClients;
|
|
|
|
public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient
|
|
{
|
|
private TimeSpan? _offset;
|
|
|
|
public async Task<IList<DebridClientTorrent>> GetDownloads()
|
|
{
|
|
var offset = 0;
|
|
var results = new List<Torrent>();
|
|
|
|
while (true)
|
|
{
|
|
var pagedResults = await GetClient().Torrents.GetAsync(offset, 5000);
|
|
|
|
results.AddRange(pagedResults);
|
|
|
|
if (pagedResults.Count == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
offset += 5000;
|
|
}
|
|
|
|
return results.Select(Map).ToList();
|
|
}
|
|
|
|
public async Task<DebridClientUser> GetUser()
|
|
{
|
|
var user = await GetClient().User.GetAsync();
|
|
|
|
return new()
|
|
{
|
|
Username = user.Username,
|
|
Expiration = user.Premium > 0 ? user.Expiration : null
|
|
};
|
|
}
|
|
|
|
public async Task<String> AddTorrentMagnet(String magnetLink)
|
|
{
|
|
try
|
|
{
|
|
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(settings.Current.Provider.Timeout));
|
|
|
|
var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, timeoutCancellationToken.Token);
|
|
|
|
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 timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(settings.Current.Provider.Timeout));
|
|
|
|
var result = await GetClient().Torrents.AddFileAsync(bytes, timeoutCancellationToken.Token);
|
|
|
|
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 async Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
|
|
{
|
|
List<DebridClientFile> files;
|
|
|
|
Log("Seleting files", torrent);
|
|
|
|
if (torrent.DownloadAction == TorrentDownloadAction.DownloadManual)
|
|
{
|
|
Log("Selecting manual selected files", torrent);
|
|
files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList();
|
|
}
|
|
else
|
|
{
|
|
Log("Selecting files", torrent);
|
|
files = [.. torrent.Files];
|
|
}
|
|
|
|
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
|
|
|
|
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
|
|
|
|
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
|
|
|
|
if (fileIds.Length == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
|
|
|
|
return fileIds.Length;
|
|
}
|
|
|
|
public async Task Delete(Data.Models.Data.Torrent torrent)
|
|
{
|
|
await GetClient().Torrents.DeleteAsync(torrent.RdId!);
|
|
}
|
|
|
|
public async Task<String> Unrestrict(Data.Models.Data.Torrent torrent, String link)
|
|
{
|
|
var result = await GetClient().Unrestrict.LinkAsync(link);
|
|
|
|
if (result.Download == null)
|
|
{
|
|
throw new($"Unrestrict returned an invalid download");
|
|
}
|
|
|
|
return result.Download;
|
|
}
|
|
|
|
public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, DebridClientTorrent? torrentClientTorrent)
|
|
{
|
|
try
|
|
{
|
|
if (torrent.RdId == null)
|
|
{
|
|
return torrent;
|
|
}
|
|
|
|
if (torrentClientTorrent == null || torrentClientTorrent.Ended == null || String.IsNullOrEmpty(torrentClientTorrent.Filename))
|
|
{
|
|
torrentClientTorrent = await GetInfo(torrent.RdId) ?? throw new($"Resource not found");
|
|
}
|
|
|
|
if (!String.IsNullOrWhiteSpace(torrentClientTorrent.Filename))
|
|
{
|
|
torrent.RdName = torrentClientTorrent.Filename;
|
|
}
|
|
|
|
if (!String.IsNullOrWhiteSpace(torrentClientTorrent.OriginalFilename))
|
|
{
|
|
torrent.RdName = torrentClientTorrent.OriginalFilename;
|
|
}
|
|
|
|
if (torrentClientTorrent.Bytes > 0)
|
|
{
|
|
torrent.RdSize = torrentClientTorrent.Bytes;
|
|
}
|
|
else if (torrentClientTorrent.OriginalBytes > 0)
|
|
{
|
|
torrent.RdSize = torrentClientTorrent.OriginalBytes;
|
|
}
|
|
|
|
if (torrentClientTorrent.Files != null && torrentClientTorrent.Files.Count > 0)
|
|
{
|
|
torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files);
|
|
}
|
|
|
|
torrent.ClientKind = Provider.RealDebrid;
|
|
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.Status switch
|
|
{
|
|
"magnet_error" => TorrentStatus.Error,
|
|
"magnet_conversion" => TorrentStatus.Processing,
|
|
"waiting_files_selection" => TorrentStatus.WaitingForFileSelection,
|
|
"queued" => TorrentStatus.Downloading,
|
|
"downloading" => TorrentStatus.Downloading,
|
|
"downloaded" => TorrentStatus.Finished,
|
|
"error" => TorrentStatus.Error,
|
|
"virus" => TorrentStatus.Error,
|
|
"compressing" => TorrentStatus.Downloading,
|
|
"uploading" => TorrentStatus.Uploading,
|
|
"dead" => TorrentStatus.Error,
|
|
_ => 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;
|
|
}
|
|
|
|
var downloadLinks = rdTorrent.Links
|
|
.Where(m => !String.IsNullOrWhiteSpace(m))
|
|
.Select(l => new DownloadInfo
|
|
{
|
|
RestrictedLink = l,
|
|
FileName = null
|
|
})
|
|
.ToList();
|
|
|
|
Log($"Found {downloadLinks.Count} links", torrent);
|
|
|
|
foreach (var link in downloadLinks)
|
|
{
|
|
Log($"{link}", torrent);
|
|
}
|
|
|
|
Log($"Torrent has {torrent.Files.Count(m => m.Selected)} selected files out of {torrent.Files.Count} files, found {downloadLinks.Count} links, torrent ended: {torrent.RdEnded}",
|
|
torrent);
|
|
|
|
// Check if all the links are set that have been selected
|
|
if (torrent.Files.Count(m => m.Selected) == downloadLinks.Count)
|
|
{
|
|
Log($"Matched {torrent.Files.Count(m => m.Selected)} selected files expected files to {downloadLinks.Count} found files", torrent);
|
|
|
|
return downloadLinks;
|
|
}
|
|
|
|
// Check if all all the links are set for manual selection
|
|
if (torrent.ManualFiles.Count == downloadLinks.Count)
|
|
{
|
|
Log($"Matched {torrent.ManualFiles.Count} manual files expected files to {downloadLinks.Count} found files", torrent);
|
|
|
|
return downloadLinks;
|
|
}
|
|
|
|
// If there is only 1 link, delay for 1 minute to see if more links pop up.
|
|
if (downloadLinks.Count == 1 && torrent.RdEnded.HasValue)
|
|
{
|
|
var expired = DateTime.UtcNow - torrent.RdEnded.Value.ToUniversalTime();
|
|
|
|
Log($"Waiting to see if more links appear, checked for {expired.TotalSeconds} seconds", torrent);
|
|
|
|
if (expired.TotalSeconds > 60.0)
|
|
{
|
|
Log($"Waited long enough", torrent);
|
|
|
|
return downloadLinks;
|
|
}
|
|
}
|
|
|
|
Log($"Did not find any suiteable download links", torrent);
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<String> GetFileName(Download download)
|
|
{
|
|
if (String.IsNullOrWhiteSpace(download.Link))
|
|
{
|
|
return Task.FromResult("");
|
|
}
|
|
|
|
var uri = new Uri(download.Link);
|
|
|
|
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
|
|
}
|
|
|
|
private RdNetClient GetClient()
|
|
{
|
|
try
|
|
{
|
|
var apiKey = settings.Current.Provider.ApiKey;
|
|
|
|
if (String.IsNullOrWhiteSpace(apiKey))
|
|
{
|
|
throw new("Real-Debrid API Key not set in the settings");
|
|
}
|
|
|
|
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
|
|
httpClient.Timeout = TimeSpan.FromSeconds(settings.Current.Provider.Timeout);
|
|
|
|
var rdtNetClient = new RdNetClient(null, httpClient, 5, settings.Current.Provider.ApiHostname);
|
|
rdtNetClient.UseApiAuthentication(apiKey);
|
|
|
|
// Get the server time to fix up the timezones on results
|
|
if (_offset == null)
|
|
{
|
|
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
|
|
_offset = serverTime.Offset;
|
|
}
|
|
|
|
return rdtNetClient;
|
|
}
|
|
catch (AggregateException ae)
|
|
{
|
|
foreach (var inner in ae.InnerExceptions)
|
|
{
|
|
logger.LogError(inner, $"The connection to RealDebrid has failed: {inner.Message}");
|
|
}
|
|
|
|
throw;
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
|
|
|
|
throw;
|
|
}
|
|
catch (TaskCanceledException ex)
|
|
{
|
|
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private DebridClientTorrent Map(Torrent torrent)
|
|
{
|
|
return new()
|
|
{
|
|
Id = torrent.Id,
|
|
Filename = torrent.Filename,
|
|
OriginalFilename = torrent.OriginalFilename,
|
|
Hash = torrent.Hash,
|
|
Bytes = torrent.Bytes,
|
|
OriginalBytes = torrent.OriginalBytes,
|
|
Host = torrent.Host,
|
|
Split = torrent.Split,
|
|
Progress = torrent.Progress,
|
|
Status = torrent.Status,
|
|
Added = ChangeTimeZone(torrent.Added)!.Value,
|
|
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
|
|
{
|
|
Path = m.Path,
|
|
Bytes = m.Bytes,
|
|
Id = m.Id,
|
|
Selected = m.Selected
|
|
})
|
|
.ToList(),
|
|
Links = torrent.Links,
|
|
Ended = ChangeTimeZone(torrent.Ended),
|
|
Speed = torrent.Speed,
|
|
Seeders = torrent.Seeders
|
|
};
|
|
}
|
|
|
|
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
|
|
{
|
|
if (_offset == null)
|
|
{
|
|
return dateTimeOffset;
|
|
}
|
|
|
|
return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value);
|
|
}
|
|
|
|
private async Task<DebridClientTorrent> GetInfo(String torrentId)
|
|
{
|
|
var result = await GetClient().Torrents.GetInfoAsync(torrentId);
|
|
|
|
return Map(result);
|
|
}
|
|
|
|
private void Log(String message, Data.Models.Data.Torrent? torrent = null)
|
|
{
|
|
if (torrent != null)
|
|
{
|
|
message = $"{message} {torrent.ToLog()}";
|
|
}
|
|
|
|
logger.LogDebug(message);
|
|
}
|
|
}
|