Merge remote-tracking branch 'rogerfar/master'

This commit is contained in:
YvesPa 2024-12-10 11:12:55 +01:00
commit f9335be163
24 changed files with 435 additions and 163 deletions

View file

@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.90] - 2024-12-06
### Changed
- Download individual files from Torbox instead of a zip file.
### Removed
- Removed ability to select instant files from AllDebrid.
## [2.0.89] - 2024-11-24
### Changed
- Disabled selecting of files as Real-Debrid was the only provider that supported that.
## [2.0.88] - 2024-11-24
### Changed
- Catch disabled instant availability endpoint from Real Debrid.
## [2.0.87] - 2024-11-18
### Added
- Torbox support.
- qBittorrent API authentication when no authentication is used.
### Changed
- .NET version changed to .NET 9.
- Changed download limit to be split by active torrents.
## [2.0.86] - 2024-09-03 ## [2.0.86] - 2024-09-03
### Changed ### Changed
- Add potential fix for BASE_PATH. - Add potential fix for BASE_PATH.

View file

@ -21,7 +21,7 @@ RUN \
RUN ls -FCla /appclient/root RUN ls -FCla /appclient/root
# Stage 2 - Build the backend # Stage 2 - Build the backend
FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim-amd64 AS dotnet-build-env FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim-amd64 AS dotnet-build-env
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG BUILDPLATFORM ARG BUILDPLATFORM
@ -66,14 +66,14 @@ RUN \
RUN \ RUN \
if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \ if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \
wget https://download.visualstudio.microsoft.com/download/pr/c3bf3103-efdb-42e0-af55-bbf861a4215b/dc22eda8877933b8c6569e3823f18d21/aspnetcore-runtime-8.0.0-linux-musl-arm64.tar.gz && \ wget https://download.visualstudio.microsoft.com/download/pr/59a041e1-921e-405e-8092-95333f80f9ca/63e83e3feb70e05ca05ed5db3c579be2/aspnetcore-runtime-9.0.0-linux-musl-arm.tar.gz && \
tar zxf aspnetcore-runtime-8.0.0-linux-musl-arm64.tar.gz -C /usr/share/dotnet ; \ tar zxf aspnetcore-runtime-9.0.0-linux-musl-arm.tar.gz -C /usr/share/dotnet ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \ elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
wget https://download.visualstudio.microsoft.com/download/pr/c3bf3103-efdb-42e0-af55-bbf861a4215b/dc22eda8877933b8c6569e3823f18d21/aspnetcore-runtime-8.0.0-linux-musl-arm64.tar.gz && \ wget https://download.visualstudio.microsoft.com/download/pr/e137f557-83cb-4f55-b1c8-e5f59ccd3cba/b8ba6f2ab96d0961757b71b00c201f31/aspnetcore-runtime-9.0.0-linux-musl-arm64.tar.gz && \
tar zxf aspnetcore-runtime-8.0.0-linux-musl-arm64.tar.gz -C /usr/share/dotnet ; \ tar zxf aspnetcore-runtime-9.0.0-linux-musl-arm64.tar.gz -C /usr/share/dotnet ; \
else \ else \
wget https://download.visualstudio.microsoft.com/download/pr/7aa33fc7-07fe-48c2-8e44-a4bfb4928535/3b96ec50970eee414895ef3a5b188bcd/aspnetcore-runtime-8.0.0-linux-musl-x64.tar.gz && \ wget https://download.visualstudio.microsoft.com/download/pr/86d7a513-fe71-4f37-b9ec-fdcf5566cce8/e72574fc82d7496c73a61f411d967d8e/aspnetcore-runtime-9.0.0-linux-musl-x64.tar.gz && \
tar zxf aspnetcore-runtime-8.0.0-linux-musl-x64.tar.gz -C /usr/share/dotnet ; \ tar zxf aspnetcore-runtime-9.0.0-linux-musl-x64.tar.gz -C /usr/share/dotnet ; \
fi fi
RUN \ RUN \

View file

@ -6,7 +6,7 @@ This is a web interface to manage your torrents on Real-Debrid, AllDebrid or Pre
- Download all files from Real-Debrid, AllDebrid or Premiumize to your local machine automatically - Download all files from Real-Debrid, AllDebrid or Premiumize to your local machine automatically
- Unpack all files when finished downloading - Unpack all files when finished downloading
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato. - Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato.
- Built with Angular 15 and .NET 8 - Built with Angular 15 and .NET 9
**You will need a Premium service at Real-Debrid, AllDebrid or Premiumize!** **You will need a Premium service at Real-Debrid, AllDebrid or Premiumize!**
@ -30,7 +30,7 @@ Instead of running in Docker you can install it as a service in Windows or Linux
## Windows Service ## Windows Service
1. Make sure you have the ASP.NET Core Runtime 8 installed: [https://dotnet.microsoft.com/download/dotnet/8.0](https://dotnet.microsoft.com/download/dotnet/8.0) 1. Make sure you have the **ASP.NET Core Runtime 9.0.0** installed: [https://dotnet.microsoft.com/download/dotnet/9.0](https://dotnet.microsoft.com/download/dotnet/9.0)
2. Get the latest zip file from the Releases page and extract it to your host. 2. Get the latest zip file from the Releases page and extract it to your host.
3. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host. 3. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host.
4. In `appsettings.json` replace the `Database` `Path` to a path on your host. 4. In `appsettings.json` replace the `Database` `Path` to a path on your host.
@ -52,7 +52,7 @@ Instead of running in Docker you can install it as a service in Linux.
```rm packages-microsoft-prod.deb``` ```rm packages-microsoft-prod.deb```
```sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0``` ```sudo apt-get update && sudo apt-get install -y dotnet-sdk-9.0```
2. Get latest archive from [releases](https://github.com/rogerfar/rdt-client/releases): 2. Get latest archive from [releases](https://github.com/rogerfar/rdt-client/releases):
```wget <zip_url>``` ```wget <zip_url>```
@ -188,7 +188,7 @@ By default the application runs in the root of your hosted address (i.e. https:/
- NodeJS - NodeJS
- NPM - NPM
- Angular CLI - Angular CLI
- .NET 8 - .NET 9
- Visual Studio 2022 - Visual Studio 2022
- (optional) Resharper - (optional) Resharper

View file

@ -72,35 +72,6 @@
</p> </p>
</div> </div>
<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'">
<option [ngValue]="0">Download all files</option>
<option [ngValue]="1">Download all available files</option>
<option [ngValue]="2">Manually pick files</option>
</select>
</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.
</p>
</div>
<div
class="notification is-success is-light"
*ngIf="provider === 'AllDebrid' && availableFiles && availableFiles.length > 0"
>
This torrent is available for immediate download.
</div>
<div
class="notification is-warning is-light"
*ngIf="provider === 'AllDebrid' && availableFiles && availableFiles.length === 0"
>
This torrent is not available for immediate download.
</div>
<div class="field"> <div class="field">
<label class="label">Minimum file size to download</label> <label class="label">Minimum file size to download</label>
<div class="control"> <div class="control">
@ -221,39 +192,6 @@
</p> </p>
</div> </div>
</div> </div>
<div fxFlex *ngIf="provider === 'RealDebrid'">
<div class="field">
<label class="label">Available files</label>
<p class="help">
These files are available for immediate download from your debrid provider. <br />
It is possible that there are more files in the torrent, which are not shown here.<br />
</p>
<div class="scroll-container">
<div class="field" *ngIf="downloadAction === 2">
<label class="checkbox is-fullwidth-label">
<input type="checkbox" [checked]="allSelected" (change)="downloadFileCheckedAll()" />
Select all
</label>
</div>
<div class="field" *ngIf="downloadAction === 2 && availableFiles !== null">
<label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles">
<input
type="checkbox"
[checked]="downloadFiles[file.filename]"
(change)="downloadFileChecked(file.filename)"
/>
{{ file.filename }} ({{ file.filesize | filesize }})
</label>
</div>
<div class="field" *ngIf="downloadAction !== 2 && availableFiles !== null">
<label class="is-fullwidth-label is-block" *ngFor="let file of availableFiles">
<span [ngClass]="{ 'strike-through': isRegexExcluded(file) }">{{ file.filename }}</span>
<span *ngIf="file.filesize > 0">({{ file.filesize | filesize }})</span>
</label>
</div>
</div>
</div>
</div>
</div> </div>
<div class="field"> <div class="field">

View file

@ -55,7 +55,7 @@
<a class="navbar-item" routerLink="profile"> Profile </a> <a class="navbar-item" routerLink="profile"> Profile </a>
<a class="navbar-item" (click)="logout()"> Logout </a> <a class="navbar-item" (click)="logout()"> Logout </a>
<hr class="navbar-divider" /> <hr class="navbar-divider" />
<a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version 2.0.86</a> <a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version 2.0.90</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,7 +15,11 @@ export class NavbarComponent implements OnInit {
public profile: Profile; public profile: Profile;
public providerLink: string; 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 { ngOnInit(): void {
this.settingsService.getProfile().subscribe((result) => { this.settingsService.getProfile().subscribe((result) => {
@ -31,6 +35,9 @@ export class NavbarComponent implements OnInit {
case 'Premiumize': case 'Premiumize':
this.providerLink = 'https://www.premiumize.me/'; this.providerLink = 'https://www.premiumize.me/';
break; break;
case 'TorBox':
this.providerLink = 'https://torbox.app/';
break;
} }
}); });
} }
@ -40,7 +47,7 @@ export class NavbarComponent implements OnInit {
() => { () => {
this.router.navigate(['/login']); this.router.navigate(['/login']);
}, },
(err) => {} (err) => {},
); );
} }
} }

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "rdt-client", "name": "rdt-client",
"version": "2.0.86", "version": "2.0.90",
"description": "This is a web interface to manage your torrents on Real-Debrid.", "description": "This is a web interface to manage your torrents on Real-Debrid.",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {

View file

@ -51,12 +51,12 @@ public class TorrentData(DataContext dataContext)
public async Task<Torrent?> GetByHash(String hash) public async Task<Torrent?> GetByHash(String hash)
{ {
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons hash = hash.ToLower();
var dbTorrent = await dataContext.Torrents var dbTorrent = await dataContext.Torrents
.AsNoTracking() .AsNoTracking()
.Include(m => m.Downloads) .Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.Hash.ToLower() == hash.ToLower()); .FirstOrDefaultAsync(m => m.Hash == hash);
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
if (dbTorrent == null) if (dbTorrent == null)
{ {

View file

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

View file

@ -155,10 +155,11 @@ http://127.0.0.1:6800/jsonrpc.")]
public class DbSettingsProvider public class DbSettingsProvider
{ {
[DisplayName("Provider")] [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://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://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://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.")] At this point only 1 provider can be used at the time.")]
public Provider Provider { get; set; } = Provider.RealDebrid; public Provider Provider { get; set; } = Provider.RealDebrid;
@ -168,7 +169,9 @@ At this point only 1 provider can be used at the time.")]
or or
<a href=""""https://alldebrid.com/apikeys/"""" target=""""_blank"""" rel=""""noopener"""">https://alldebrid.com/apikeys/</a> <a href=""""https://alldebrid.com/apikeys/"""" target=""""_blank"""" rel=""""noopener"""">https://alldebrid.com/apikeys/</a>
or 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; } = ""; public String ApiKey { get; set; } = "";
[DisplayName("Automatically import and process torrents added to provider")] [DisplayName("Automatically import and process torrents added to provider")]

View file

@ -1,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.8" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Serilog" Version="4.0.1" /> <PackageReference Include="Serilog" Version="4.2.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

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

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@ -11,17 +11,18 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="AllDebrid.NET" Version="1.0.12" /> <PackageReference Include="AllDebrid.NET" Version="1.0.12" />
<PackageReference Include="Aria2.NET" Version="1.0.5" /> <PackageReference Include="Aria2.NET" Version="1.0.5" />
<PackageReference Include="Downloader" Version="3.1.2" /> <PackageReference Include="Downloader" Version="3.3.1" />
<PackageReference Include="Downloader.NET" Version="1.0.12" /> <PackageReference Include="Downloader.NET" Version="1.0.12" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.8" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.8.0" /> <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="MonoTorrent" Version="3.0.2" /> <PackageReference Include="MonoTorrent" Version="3.0.2" />
<PackageReference Include="Polly" Version="8.4.1" /> <PackageReference Include="Polly" Version="8.5.0" />
<PackageReference Include="Premiumize.NET" Version="1.0.4" /> <PackageReference Include="Premiumize.NET" Version="1.0.4" />
<PackageReference Include="RD.NET" Version="2.1.6" /> <PackageReference Include="RD.NET" Version="2.1.6" />
<PackageReference Include="Serilog" Version="4.0.1" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SharpCompress" Version="0.38.0" /> <PackageReference Include="SharpCompress" Version="0.38.0" />
<PackageReference Include="TorBox.NET" Version="1.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -26,7 +26,7 @@ public class InternalDownloader : IDownloader
_uri = uri; _uri = uri;
_filePath = filePath; _filePath = filePath;
_downloadConfiguration = new(); _downloadConfiguration = new();
SetSettings(); SetSettings();
@ -131,14 +131,14 @@ public class InternalDownloader : IDownloader
{ {
settingDownloadParallelCount = 1; settingDownloadParallelCount = 1;
} }
var settingDownloadMaxSpeed = Settings.Get.DownloadClient.MaxSpeed; var settingDownloadMaxSpeed = Settings.Get.DownloadClient.MaxSpeed;
if (settingDownloadMaxSpeed <= 0) if (settingDownloadMaxSpeed <= 0)
{ {
settingDownloadMaxSpeed = 0; settingDownloadMaxSpeed = 0;
} }
settingDownloadMaxSpeed /= Math.Max(TorrentRunner.ActiveDownloadClients.Count, 1);
settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024; settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024;
var settingDownloadTimeout = Settings.Get.DownloadClient.Timeout; var settingDownloadTimeout = Settings.Get.DownloadClient.Timeout;

View file

@ -118,23 +118,9 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
return resultId; return resultId;
} }
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{ {
var isAvailable = await GetClient().Magnet.InstantAvailabilityAsync(hash); return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
if (isAvailable)
{
return
[
new()
{
Filename = "All files",
Filesize = 0
}
];
}
return [];
} }
public Task SelectFiles(Torrent torrent) public Task SelectFiles(Torrent torrent)

View file

@ -117,8 +117,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{ {
var result = new List<TorrentClientAvailableFile>(); return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
return Task.FromResult<IList<TorrentClientAvailableFile>>(result);
} }
public Task SelectFiles(Torrent torrent) public Task SelectFiles(Torrent torrent)

View file

@ -137,49 +137,27 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return result.Id; return result.Id;
} }
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{ {
var result = await GetClient().Torrents.GetAvailableFiles(hash); return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
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) public async Task SelectFiles(Data.Models.Data.Torrent torrent)
{ {
var files = torrent.Files; IList<TorrentClientFile> files;
Log("Seleting files", torrent); Log("Seleting files", torrent);
if (torrent.DownloadAction == TorrentDownloadAction.DownloadAvailableFiles) if (torrent.DownloadAction == TorrentDownloadAction.DownloadManual)
{
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); Log("Selecting manual selected files", torrent);
files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList(); files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList();
} }
else
{
Log("Selecting all files", torrent);
files = [.. torrent.Files];
}
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent); Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);

View file

@ -0,0 +1,319 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using TorBoxNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
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)
{
// 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;
const Int32 seed = 3;
var result = await GetClient().Torrents.AddFileAsync(bytes, seed);
if (result.Error == "ACTIVE_LIMIT")
{
using var stream = new MemoryStream(bytes);
var torrent = await MonoTorrent.Torrent.LoadAsync(stream);
return torrent.InfoHashes.V1!.ToHex().ToLowerInvariant();
}
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)
{
return (availability.Data[0]?.Files ?? []).Select(file => new TorrentClientAvailableFile
{
Filename = file.Name,
Filesize = file.Size
})
.ToList();
}
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('/');
var zipped = segments[5] == "zip";
var 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)
{
var files = new List<String>();
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true);
foreach (var file in torrent.Files)
{
var newFile = $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}";
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, Downloads downloads,
AllDebridTorrentClient allDebridTorrentClient, AllDebridTorrentClient allDebridTorrentClient,
PremiumizeTorrentClient premiumizeTorrentClient, PremiumizeTorrentClient premiumizeTorrentClient,
RealDebridTorrentClient realDebridTorrentClient) RealDebridTorrentClient realDebridTorrentClient,
TorBoxTorrentClient torBoxTorrentClient)
{ {
private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
@ -40,6 +41,7 @@ public class Torrents(
Provider.Premiumize => premiumizeTorrentClient, Provider.Premiumize => premiumizeTorrentClient,
Provider.RealDebrid => realDebridTorrentClient, Provider.RealDebrid => realDebridTorrentClient,
Provider.AllDebrid => allDebridTorrentClient, Provider.AllDebrid => allDebridTorrentClient,
Provider.TorBox => torBoxTorrentClient,
_ => throw new("Invalid Provider") _ => throw new("Invalid Provider")
}; };
} }

View file

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.QBittorrent; using RdtClient.Data.Models.QBittorrent;
using RdtClient.Service.Services; using RdtClient.Service.Services;
@ -21,6 +22,11 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
{ {
logger.LogDebug($"Auth login"); logger.LogDebug($"Auth login");
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
{
return Ok("Ok.");
}
if (String.IsNullOrWhiteSpace(request.UserName) || String.IsNullOrEmpty(request.Password)) if (String.IsNullOrWhiteSpace(request.UserName) || String.IsNullOrEmpty(request.Password))
{ {
return Ok("Fails."); return Ok("Fails.");

View file

@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using System.Net;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Hosting.WindowsServices;
@ -136,8 +135,6 @@ builder.Host.UseWindowsService();
RdtClient.Data.DiConfig.Config(builder.Services, appSettings); RdtClient.Data.DiConfig.Config(builder.Services, appSettings);
builder.Services.RegisterRdtServices(); builder.Services.RegisterRdtServices();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
try try
{ {
// Build the app // Build the app

View file

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId> <UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
<Version>2.0.86</Version> <Version>2.0.90</Version>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@ -29,17 +29,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.8" /> <PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.0" />
<PackageReference Include="Serilog" Version="4.0.1" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup> </ItemGroup>

View file

@ -199,10 +199,12 @@
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=NAMESPACE_005FALIAS/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String> <s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=NAMESPACE_005FALIAS/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String> <s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String> <s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EPsiFeatures_002EVisualStudio_002EBackend_002EDaemon_002EHighlightingSettingsMigrator/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Housekeeping/VsHighlighting/SuppressVsSquiggles/@EntryValue">True</s:Boolean>
</wpf:ResourceDictionary> </wpf:ResourceDictionary>