From 772631d47f534260a0745c92e1a9f4b237d9f386 Mon Sep 17 00:00:00 2001
From: Sam Heinz <54530346+asylumexp@users.noreply.github.com>
Date: Thu, 18 Jul 2024 20:14:35 +1000
Subject: [PATCH] partially complete about 1/4 of the TorBoxClient
Up to available files
---
.../RdtClient.Service.csproj | 1 +
.../TorrentClients/TorBoxTorrentClient.cs | 429 ++++++++++++++++++
2 files changed, 430 insertions(+)
create mode 100644 server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj
index c736d2e..2c5daa6 100644
--- a/server/RdtClient.Service/RdtClient.Service.csproj
+++ b/server/RdtClient.Service/RdtClient.Service.csproj
@@ -22,6 +22,7 @@
+
diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
new file mode 100644
index 0000000..02c1d21
--- /dev/null
+++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
@@ -0,0 +1,429 @@
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using TorBoxNET;
+using RdtClient.Data.Enums;
+using RdtClient.Data.Models.TorrentClient;
+using RdtClient.Service.Helpers;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace RdtClient.Service.Services.TorrentClients;
+
+public class TorBoxTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory) : ITorrentClient
+{
+ private TimeSpan? _offset;
+
+ private TorBoxNetClient GetClient()
+ {
+ try
+ {
+ var apiKey = Settings.Get.Provider.ApiKey;
+
+ if (String.IsNullOrWhiteSpace(apiKey))
+ {
+ throw new("TorBox API Key not set in the settings");
+ }
+
+ var httpClient = httpClientFactory.CreateClient();
+ httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
+
+ var rdtNetClient = new TorBoxNetClient(null, httpClient, 5);
+ rdtNetClient.UseApiAuthentication(apiKey);
+
+ // Get the server time to fix up the timezones on results
+ if (_offset == null)
+ {
+ var serverTime = rdtNetClient.Api.GetIsoTimeAsync();
+ _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 TorrentClientTorrent Map(Torrent torrent)
+ {
+ return new()
+ {
+ Id = torrent.Id.ToString(),
+ Filename = torrent.Name,
+ OriginalFilename = torrent.Name,
+ Hash = torrent.Hash,
+ Bytes = torrent.Size,
+ OriginalBytes = torrent.Size,
+ Host = null,
+ Split = 0,
+ Progress = (Int64)((torrent.Progress) * 100.0),
+ Status = torrent.DownloadState,
+ Added = ChangeTimeZone(torrent.CreatedAt)!.Value,
+ Files = (torrent.Files ?? []).Select(m => new TorrentClientFile
+ {
+ Path = m.S3Path.Replace(m.Hash + "/", string.Empty),
+ Bytes = m.Size,
+ Id = m.Id,
+ Selected = true
+ }).ToList(),
+ Links = [],
+ Ended = ChangeTimeZone(torrent.UpdatedAt),
+ Speed = torrent.DownloadSpeed,
+ Seeders = torrent.Seeds,
+ };
+ }
+
+ public async Task> GetTorrents()
+ {
+ var results = await GetClient().Torrents.GetAsync(true);
+
+ return results!.Select(Map).ToList();
+ }
+
+ public async Task GetUser()
+ {
+ var user = await GetClient().User.GetAsync(false);
+
+ return new()
+ {
+ Username = user.Data!.Email,
+ Expiration = user.Data!.Plan != 0 ? user.Data!.PremiumExpiresAt.Value : null
+ };
+ }
+
+ public async Task AddMagnet(String magnetLink)
+ {
+ var result = await GetClient().Torrents.AddMagnetAsync(magnetLink);
+
+ return result.Data?.Torrent_ID.ToString()!;
+ }
+
+ public async Task AddFile(Byte[] bytes)
+ {
+ var result = await GetClient().Torrents.AddFileAsync(bytes);
+
+ return result.Data?.Torrent_ID.ToString()!;
+ }
+
+ public async Task> GetAvailableFiles(String hash)
+ {
+ var result = await GetClient().Torrents.GetAvailableFiles(hash);
+
+ var files = result.SelectMany(m => m.Value).SelectMany(m => m.Value).SelectMany(m => m.Values);
+
+ var groups = files.Where(m => m.Filename != null).GroupBy(m => $"{m.Filename}-{m.Filesize}");
+
+ var torrentClientAvailableFiles = groups.Select(m => new TorrentClientAvailableFile
+ {
+ Filename = m.First().Filename!,
+ Filesize = m.First().Filesize
+ }).ToList();
+
+ return torrentClientAvailableFiles;
+ }
+
+ public async Task SelectFiles(Data.Models.Data.Torrent torrent)
+ {
+ var files = torrent.Files;
+
+ Log("Seleting files", torrent);
+
+ if (torrent.DownloadAction == TorrentDownloadAction.DownloadAvailableFiles)
+ {
+ Log($"Determining which files are already available on RealDebrid", torrent);
+
+ var availableFiles = await GetAvailableFiles(torrent.Hash);
+
+ Log($"Found {files.Count}/{torrent.Files.Count} available files on RealDebrid", torrent);
+
+ files = torrent.Files.Where(m => availableFiles.Any(f => m.Path.EndsWith(f.Filename))).ToList();
+ }
+ else if (torrent.DownloadAction == TorrentDownloadAction.DownloadAll)
+ {
+ Log("Selecting all files", torrent);
+ files = [.. torrent.Files];
+ }
+ else 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();
+ }
+
+ Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
+
+ if (torrent.DownloadAction != TorrentDownloadAction.DownloadManual && torrent.DownloadMinSize > 0)
+ {
+ var minFileSize = torrent.DownloadMinSize * 1024 * 1024;
+
+ Log($"Determining which files are over {minFileSize} bytes", torrent);
+
+ files = files.Where(m => m.Bytes > minFileSize)
+ .ToList();
+
+ Log($"Found {files.Count} files that match the minimum file size criterea", torrent);
+ }
+
+ if (!String.IsNullOrWhiteSpace(torrent.IncludeRegex))
+ {
+ Log($"Using regular expression {torrent.IncludeRegex} to include only files matching this regex", torrent);
+
+ var newFiles = new List();
+ foreach (var file in files)
+ {
+ if (Regex.IsMatch(file.Path, torrent.IncludeRegex))
+ {
+ Log($"* Including {file.Path}", torrent);
+ newFiles.Add(file);
+ }
+ else
+ {
+ Log($"* Excluding {file.Path}", torrent);
+ }
+ }
+
+ files = newFiles;
+
+ Log($"Found {files.Count} files that match the regex", torrent);
+ }
+ else if (!String.IsNullOrWhiteSpace(torrent.ExcludeRegex))
+ {
+ Log($"Using regular expression {torrent.IncludeRegex} to ignore files matching this regex", torrent);
+
+ var newFiles = new List();
+ foreach (var file in files)
+ {
+ if (!Regex.IsMatch(file.Path, torrent.ExcludeRegex))
+ {
+ Log($"* Including {file.Path}", torrent);
+ newFiles.Add(file);
+ }
+ else
+ {
+ Log($"* Excluding {file.Path}", torrent);
+ }
+ }
+
+ files = newFiles;
+
+ Log($"Found {files.Count} files that match the regex", torrent);
+ }
+
+ if (files.Count == 0)
+ {
+ Log($"Filtered all files out! Downloading ALL files instead!", torrent);
+
+ files = torrent.Files;
+ }
+
+ var fileIds = files.Select(m => m.Id.ToString()).ToArray();
+
+ Log($"Selecting files:");
+
+ foreach (var file in files)
+ {
+ Log($"{file.Id}: {file.Path} ({file.Bytes}b)");
+ }
+
+ Log("", torrent);
+
+ await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
+ }
+
+ public async Task Delete(String torrentId)
+ {
+ await GetClient().Torrents.DeleteAsync(torrentId);
+ }
+
+ public async Task Unrestrict(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 UpdateData(Data.Models.Data.Torrent torrent, TorrentClientTorrent? 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.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;
+
+ torrent.RdStatus = rdTorrent.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?> GetDownloadLinks(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)).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;
+ }
+
+ private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
+ {
+ if (_offset == null)
+ {
+ return dateTimeOffset;
+ }
+
+ return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value);
+ }
+
+ private async Task 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);
+ }
+}
\ No newline at end of file