Merge pull request #585 from asylumexp/master

TorBox support
This commit is contained in:
Roger Far 2024-11-18 03:28:51 -07:00 committed by GitHub
commit 7573492bcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 416 additions and 20 deletions

View file

@ -74,7 +74,10 @@
<div class="field">
<label class="label">Post Torrent Download Action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="downloadAction" [disabled]="provider === 'AllDebrid' || provider === 'Premiumize'">
<select
[(ngModel)]="downloadAction"
[disabled]="provider === 'AllDebrid' || provider === 'Premiumize' || provider === 'TorBox'"
>
<option [ngValue]="0">Download all files</option>
<option [ngValue]="1">Download all available files</option>
<option [ngValue]="2">Manually pick files</option>
@ -82,7 +85,7 @@
</div>
<p class="help">When a torrent is fully downloaded on the provider, perform this action.</p>
<p class="help" *ngIf="provider === 'AllDebrid'">
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.
</p>
</div>

View file

@ -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) => {},
);
}
}

View file

@ -42,7 +42,7 @@
<div class="notification is-primary">
To be able to use the Real-Debrid Client you need a premium subscription to download torrents.
<br /><br />
Not premium yet? You have the choice of 2 providers:
Not premium yet? You have the choice of 4 providers:
<br />
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
>Use this link to sign up to Real-Debrid.</a
@ -55,6 +55,8 @@
<a href="https://www.premiumize.me/" target="_blank" rel="noopener"
>Use this link to sign up to Premiumize.</a
>
<a href="https://torbox.app/" target="_blank" rel="noopener"
>Use this link to sign up to TorBox.</a>
<br />
<small>(Referal links)</small>
</div>
@ -65,6 +67,7 @@
<option [ngValue]="0">Real-Debrid</option>
<option [ngValue]="1">AllDebrid</option>
<option [ngValue]="2">Premiumize</option>
<option [ngValue]="3">TorBox</option>
</select>
</div>
</div>
@ -91,6 +94,10 @@
>https://www.premiumize.me/account</a
>.
</p>
<p class="help" *ngIf="provider === 3">
You can find your API key here:
<a href="https://torbox.app/settings" target="_blank" rel="noopener">https://torbox.app/settings</a>.
</p>
</div>
<div class="field">

View file

@ -11,5 +11,8 @@ public enum Provider
AllDebrid,
[Description("Premiumize")]
Premiumize
Premiumize,
[Description("TorBox")]
TorBox
}

View file

@ -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:
<a href=""https://real-debrid.com/?id=1348683"" target=""_blank"" rel=""noopener"">https://real-debrid.com</a>
<a href=""https://alldebrid.com/?uid=2v91l&lang=en"" target=""_blank"" rel=""noopener"">https://alldebrid.com</a>
<a href=""https://www.premiumize.me/"" target=""_blank"" rel=""noopener"">https://www.premiumize.me/</a>
<a href=""https://torbox.app/"" target=""_blank"" rel=""noopener"">https://torbox.app/</a>
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
<a href=""""https://alldebrid.com/apikeys/"""" target=""""_blank"""" rel=""""noopener"""">https://alldebrid.com/apikeys/</a>
or
<a href=""https://www.premiumize.me/account/"" target=""_blank"" rel=""noopener"">https://www.premiumize.me/account/</a>")]
<a href=""https://www.premiumize.me/account/"" target=""_blank"" rel=""noopener"">https://www.premiumize.me/account/</a>
or
<a href=""""https://torbox.app/settings/"""" target=""""_blank"""" rel=""""noopener"""">https://torbox.app/settings/</a>")]
public String ApiKey { get; set; } = "";
[DisplayName("Automatically import and process torrents added to provider")]

View file

@ -24,6 +24,7 @@ public static class DiConfig
services.AddScoped<RemoteService>();
services.AddScoped<RealDebridTorrentClient>();
services.AddScoped<Settings>();
services.AddScoped<TorBoxTorrentClient>();
services.AddScoped<Torrents>();
services.AddScoped<TorrentRunner>();

View file

@ -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<String?> 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<String?> 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);

View file

@ -22,6 +22,7 @@
<PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SharpCompress" Version="0.38.0" />
<PackageReference Include="TorBox.NET" Version="1.2.1" />
</ItemGroup>
<ItemGroup>

View file

@ -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}")
};

View file

@ -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<TorBoxTorrentClient> 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<IList<TorrentClientTorrent>> GetTorrents()
{
var torrents = new List<TorrentInfoResult>();
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<TorrentClientUser> 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<String> 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<String> 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<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{
var availability = await GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true);
if (availability.Data != null)
{
var availableFiles = new List<TorrentClientAvailableFile>();
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<string> 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<Data.Models.Data.Torrent> 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<IList<String>?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
{
if (torrent.Hash == null)
{
return null;
}
var rdTorrent = await GetInfo(torrent.Hash);
var files = new List<String>();
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<TorrentClientTorrent> GetInfo(String torrentHash)
{
var result = await GetClient().Torrents.GetHashInfoAsync(torrentHash, skipCache: true);
return Map(result!);
}
}

View file

@ -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);

View file

@ -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);
}
});
}