diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.html b/client/src/app/add-new-torrent/add-new-torrent.component.html index ed6a853..a083d1d 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.html +++ b/client/src/app/add-new-torrent/add-new-torrent.component.html @@ -74,7 +74,10 @@
- @@ -82,7 +85,7 @@

When a torrent is fully downloaded on the provider, perform this action.

- This option is only available for RealDebrid. AllDebrid and Premiumize will always download the full torrent. + This option is only available for RealDebrid. AllDebrid, Premiumize, and Torbox will always download the full torrent.

diff --git a/client/src/app/navbar/navbar.component.ts b/client/src/app/navbar/navbar.component.ts index c47e2b9..95c8067 100644 --- a/client/src/app/navbar/navbar.component.ts +++ b/client/src/app/navbar/navbar.component.ts @@ -15,7 +15,11 @@ export class NavbarComponent implements OnInit { public profile: Profile; public providerLink: string; - constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {} + constructor( + private settingsService: SettingsService, + private authService: AuthService, + private router: Router, + ) {} ngOnInit(): void { this.settingsService.getProfile().subscribe((result) => { @@ -31,6 +35,9 @@ export class NavbarComponent implements OnInit { case 'Premiumize': this.providerLink = 'https://www.premiumize.me/'; break; + case 'TorBox': + this.providerLink = 'https://torbox.app/'; + break; } }); } @@ -40,7 +47,7 @@ export class NavbarComponent implements OnInit { () => { this.router.navigate(['/login']); }, - (err) => {} + (err) => {}, ); } } diff --git a/client/src/app/setup/setup.component.html b/client/src/app/setup/setup.component.html index 3de49e5..643e618 100644 --- a/client/src/app/setup/setup.component.html +++ b/client/src/app/setup/setup.component.html @@ -42,7 +42,7 @@
To be able to use the Real-Debrid Client you need a premium subscription to download torrents.

- Not premium yet? You have the choice of 2 providers: + Not premium yet? You have the choice of 4 providers:
Use this link to sign up to Real-Debrid.Use this link to sign up to Premiumize. + Use this link to sign up to TorBox.
(Referal links)
@@ -65,6 +67,7 @@ + @@ -91,6 +94,10 @@ >https://www.premiumize.me/account.

+

+ You can find your API key here: + https://torbox.app/settings. +

diff --git a/server/RdtClient.Data/Enums/Provider.cs b/server/RdtClient.Data/Enums/Provider.cs index 7422663..ff372dc 100644 --- a/server/RdtClient.Data/Enums/Provider.cs +++ b/server/RdtClient.Data/Enums/Provider.cs @@ -11,5 +11,8 @@ public enum Provider AllDebrid, [Description("Premiumize")] - Premiumize + Premiumize, + + [Description("TorBox")] + TorBox } \ No newline at end of file diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 10381fd..9402b33 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -123,7 +123,7 @@ http://127.0.0.1:6800/jsonrpc.")] [DisplayName("Aria2c Secret (only used for the Aria2c Downloader)")] [Description("The secret of your Aria2c instance. Optional.")] public String Aria2cSecret { get; set; } = "mysecret123"; - + [DisplayName("Aria2c Download Path")] [Description("The root path to download the file to on the Aria2c host, if empty use the Download path setting.")] public String? Aria2cDownloadPath { get; set; } = null; @@ -140,10 +140,11 @@ http://127.0.0.1:6800/jsonrpc.")] public class DbSettingsProvider { [DisplayName("Provider")] - [Description(@"The following 3 providers are supported: + [Description(@"The following 4 providers are supported: https://real-debrid.com https://alldebrid.com https://www.premiumize.me/ +https://torbox.app/ At this point only 1 provider can be used at the time.")] public Provider Provider { get; set; } = Provider.RealDebrid; @@ -153,7 +154,9 @@ At this point only 1 provider can be used at the time.")] or https://alldebrid.com/apikeys/ or -https://www.premiumize.me/account/")] +https://www.premiumize.me/account/ +or +https://torbox.app/settings/")] public String ApiKey { get; set; } = ""; [DisplayName("Automatically import and process torrents added to provider")] diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index e5f7aa2..695443f 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -24,6 +24,7 @@ public static class DiConfig services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/server/RdtClient.Service/Helpers/DownloadHelper.cs b/server/RdtClient.Service/Helpers/DownloadHelper.cs index bbb1775..c427ac1 100644 --- a/server/RdtClient.Service/Helpers/DownloadHelper.cs +++ b/server/RdtClient.Service/Helpers/DownloadHelper.cs @@ -1,11 +1,14 @@ -using System.Web; +using System; +using System.Web; +using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; +using RdtClient.Service.Services; namespace RdtClient.Service.Helpers; public static class DownloadHelper { - public static String? GetDownloadPath(String downloadPath, Torrent torrent, Download download) + public static async Task GetDownloadPath(String downloadPath, Torrent torrent, Download download) { var fileUrl = download.Link; @@ -21,6 +24,20 @@ public static class DownloadHelper var fileName = uri.Segments.Last(); + if (Settings.Get.Provider.Provider == Provider.TorBox) + { + using (HttpClient client = new HttpClient()) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Head, uri); + HttpResponseMessage response = await client.SendAsync(request); + + if (response.Content.Headers.ContentDisposition != null) + { + fileName = response.Content.Headers.ContentDisposition.FileName!.Trim('"'); + } + } + } + fileName = HttpUtility.UrlDecode(fileName); fileName = FileHelper.RemoveInvalidFileNameChars(fileName); @@ -51,7 +68,7 @@ public static class DownloadHelper return filePath; } - public static String? GetDownloadPath(Torrent torrent, Download download) + public static async Task GetDownloadPath(Torrent torrent, Download download) { var fileUrl = download.Link; @@ -65,6 +82,20 @@ public static class DownloadHelper var fileName = uri.Segments.Last(); + if (Settings.Get.Provider.Provider == Provider.TorBox) + { + using (HttpClient client = new HttpClient()) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Head, uri); + HttpResponseMessage response = await client.SendAsync(request); + + if (response.Content.Headers.ContentDisposition != null) + { + fileName = response.Content.Headers.ContentDisposition.FileName!.Trim('"'); + } + } + } + fileName = HttpUtility.UrlDecode(fileName); fileName = FileHelper.RemoveInvalidFileNameChars(fileName); diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj index 86ce058..ae30459 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/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 3ab400b..d5faf3d 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -43,15 +43,15 @@ public class DownloadClient(Download download, Torrent torrent, String destinati if (Type != Data.Enums.DownloadClient.Symlink) { - await FileHelper.Delete(filePath); + await FileHelper.Delete(filePath.Result!); } Downloader = Type switch { - Data.Enums.DownloadClient.Internal => new InternalDownloader(download.Link, filePath), - Data.Enums.DownloadClient.Bezzad => new BezzadDownloader(download.Link, filePath), - Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(download.RemoteId, download.Link, filePath, downloadPath, category), - Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(download.Link, filePath, downloadPath), + Data.Enums.DownloadClient.Internal => new InternalDownloader(download.Link, filePath.Result!), + Data.Enums.DownloadClient.Bezzad => new BezzadDownloader(download.Link, filePath.Result!), + Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(download.RemoteId, download.Link, filePath.Result!, downloadPath.Result!, category), + Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(download.Link, filePath.Result!, downloadPath.Result!), _ => throw new($"Unknown download client {Type}") }; diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs new file mode 100644 index 0000000..8ed91e9 --- /dev/null +++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs @@ -0,0 +1,338 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using TorBoxNET; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.TorrentClient; +using RdtClient.Service.Helpers; + +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 torBoxNetClient = new TorBoxNetClient(null, httpClient, 5); + torBoxNetClient.UseApiAuthentication(apiKey); + + // Get the server time to fix up the timezones on results + if (_offset == null) + { + var serverTime = DateTimeOffset.UtcNow; + _offset = serverTime.Offset; + } + + return torBoxNetClient; + } + 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(TorrentInfoResult torrent) + { + return new() + { + Id = torrent.Hash, + Filename = torrent.Name, + OriginalFilename = torrent.Name, + Hash = torrent.Hash, + Bytes = torrent.Size, + OriginalBytes = torrent.Size, + Host = torrent.DownloadPresent.ToString(), + Split = 0, + Progress = (Int64)((torrent.Progress) * 100.0), + Status = torrent.DownloadState, + Added = ChangeTimeZone(torrent.CreatedAt)!.Value, + Files = (torrent.Files ?? []).Select(m => new TorrentClientFile + { + Path = string.Join("/", m.Name.Split('/').Skip(1)), + 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 torrents = new List(); + + var currentTorrents = await GetClient().Torrents.GetCurrentAsync(true); + if (currentTorrents != null) + { + torrents.AddRange(currentTorrents); + } + + var queuedTorrents = await GetClient().Torrents.GetQueuedAsync(true); + if (queuedTorrents != null) + { + torrents.AddRange(queuedTorrents); + } + + return torrents!.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 seeding = Settings.Get.Integrations.Default.FinishedAction; + + // Line is not working right now, will disable seeding and fix in december when I have time again. + // var seed = (seeding == TorrentFinishedAction.RemoveAllTorrents || seeding == TorrentFinishedAction.RemoveRealDebrid) ? 3 : 2; + + var seed = 3; + var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, seed, false); + + if (result.Error == "ACTIVE_LIMIT") + { + var magnetLinkInfo = MonoTorrent.MagnetLink.Parse(magnetLink); + return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant(); + } + else + { + return result.Data!.Hash!; + } + } + + public async Task AddFile(Byte[] bytes) + { + var seeding = Settings.Get.Integrations.Default.FinishedAction; + + // Line is not working right now, will disable seeding and fix in december when I have time again. + // var seed = (seeding == TorrentFinishedAction.RemoveAllTorrents || seeding == TorrentFinishedAction.RemoveRealDebrid) ? 3 : 2; + + var seed = 3; + + var result = await GetClient().Torrents.AddFileAsync(bytes, seed, false); + if (result.Error == "ACTIVE_LIMIT") + { + using (var stream = new MemoryStream(bytes)) + { + var torrent = MonoTorrent.Torrent.Load(stream); + return torrent!.InfoHashes.V1!.ToHex().ToLowerInvariant(); + } + } + else + { + return result.Data!.Hash!; + } + } + + public async Task> GetAvailableFiles(String hash) + { + var availability = await GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true); + + if (availability.Data != null) + { + var availableFiles = new List(); + foreach (var file in availability.Data[0]?.Files!) + { + availableFiles.Add( + new TorrentClientAvailableFile + { + Filename = file!.Name, + Filesize = file.Size + }); + } + return availableFiles; + } + else + { + return []; + } + + + } + + public Task SelectFiles(Data.Models.Data.Torrent torrent) + { + return Task.CompletedTask; + } + + public async Task Delete(String torrentId) + { + await GetClient().Torrents.ControlAsync(torrentId, "delete"); + } + + public async Task Unrestrict(string link) + { + var segments = link.Split('/'); + + bool zipped = segments[5] == "zip"; + string fileId = zipped ? "0" : segments[5]; + + var result = await GetClient().Torrents.RequestDownloadAsync(Convert.ToInt32(segments[4]), Convert.ToInt32(fileId), zipped); + + if (result?.Error != null) + { + throw new("Unrestrict returned an invalid download"); + } + + return result!.Data!; + } + + public async Task UpdateData(Data.Models.Data.Torrent torrent, TorrentClientTorrent? torrentClientTorrent) + { + try + { + if (torrent.RdId == null) + { + return torrent; + } + + var rdTorrent = await GetInfo(torrent.Hash) ?? 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; + + if (rdTorrent.Host == "True") + { + torrent.RdStatus = TorrentStatus.Finished; + } + else + { + torrent.RdStatus = rdTorrent.Status switch + { + "queued" => TorrentStatus.Processing, + "metaDL" => TorrentStatus.Processing, + "checking" => TorrentStatus.Processing, + "checkingResumeData" => TorrentStatus.Processing, + "paused" => TorrentStatus.Downloading, + "downloading" => TorrentStatus.Downloading, + "completed" => TorrentStatus.Downloading, + "uploading" => TorrentStatus.Downloading, + "uploading (no peers)" => TorrentStatus.Downloading, + "stalled" => TorrentStatus.Downloading, + "stalled (no seeds)" => TorrentStatus.Downloading, + "processing" => TorrentStatus.Downloading, + "cached" => TorrentStatus.Finished, + _ => 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.Hash == null) + { + return null; + } + + var rdTorrent = await GetInfo(torrent.Hash); + var files = new List(); + + var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true); + + var newFile = $"https://torbox.app/fakedl/{torrentId?.Id}/zip"; + files.Add(newFile); + + return files; + } + + private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) + { + if (_offset == null) + { + return dateTimeOffset; + } + + return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value); + } + + private async Task GetInfo(String torrentHash) + { + var result = await GetClient().Torrents.GetHashInfoAsync(torrentHash, skipCache: true); + + return Map(result!); + } +} diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index d1ff70d..4885e1b 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -22,7 +22,8 @@ public class Torrents( Downloads downloads, AllDebridTorrentClient allDebridTorrentClient, PremiumizeTorrentClient premiumizeTorrentClient, - RealDebridTorrentClient realDebridTorrentClient) + RealDebridTorrentClient realDebridTorrentClient, + TorBoxTorrentClient torBoxTorrentClient) { private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); @@ -40,6 +41,7 @@ public class Torrents( Provider.Premiumize => premiumizeTorrentClient, Provider.RealDebrid => realDebridTorrentClient, Provider.AllDebrid => allDebridTorrentClient, + Provider.TorBox => torBoxTorrentClient, _ => throw new("Invalid Provider") }; } @@ -551,7 +553,7 @@ public class Torrents( { Log($"Deleting {filePath}", download, download.Torrent); - await FileHelper.Delete(filePath); + await FileHelper.Delete(filePath.Result!); } Log($"Resetting", download, download.Torrent); diff --git a/server/RdtClient.Service/Services/UnpackClient.cs b/server/RdtClient.Service/Services/UnpackClient.cs index 2724f43..c702d71 100644 --- a/server/RdtClient.Service/Services/UnpackClient.cs +++ b/server/RdtClient.Service/Services/UnpackClient.cs @@ -31,7 +31,7 @@ public class UnpackClient(Download download, String destinationPath) { if (!_cancellationTokenSource.IsCancellationRequested) { - await Unpack(filePath, _cancellationTokenSource.Token); + await Unpack(filePath.Result!, _cancellationTokenSource.Token); } }); }