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 @@
Post Torrent Download Action
-
+
Download all files
Download all available files
Manually pick files
@@ -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 @@
@@ -65,6 +67,7 @@
Real-Debrid
AllDebrid
Premiumize
+ TorBox
@@ -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);
}
});
}