Add AllDebrid support.

This commit is contained in:
Roger Far 2021-10-30 18:48:32 -06:00
parent 723cc82b11
commit 8774243d2f
32 changed files with 548 additions and 228 deletions

View file

@ -4,6 +4,10 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.9.7] - 2021-10-30
### Added
- Add AllDebrid support.
## [1.9.6] - 2021-10-30
### Added
- Improved handling of errors on the torrent itself

View file

@ -1,16 +1,20 @@
# Real-Debrid Torrent Client
This is a web interface to manage your torrents on Real-Debrid. It supports the following features:
This is a web interface to manage your torrents on Real-Debrid or AllDebrid. It supports the following features:
- Add new torrents through magnets or files
- Download all files from Real Debrid to your local machine automatically
- Download all files from Real Debrid or AllDebrid to your local machine automatically
- Unpack all files when finished downloading
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr or Couchpotato.
- Built with Angular 11 and .NET 5
**You will need a Premium service at Real-Debrid!**
**You will need a Premium service at Real-Debrid or AllDebrid!**
[Click here to sign up (referal link so I can get a few free premium days).](https://real-debrid.com/?id=1348683)
[Click here to sign up at Real-Debrid.](https://real-debrid.com/?id=1348683)
[Click here to sign up AllDebrid.](https://real-debrid.com/?id=1348683)
<sub>(referal links so I can get a few free premium days)</sub>
## Docker Setup

View file

@ -39,12 +39,29 @@
<div class="field">
<label class="label">Download action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="downloadAction">
<option [ngValue]="0">Download all files above a certain size</option>
<option [ngValue]="1">Download all available files on Real-Debrid above a certain size</option>
<option [ngValue]="2">Pick files I want to download</option>
<select [(ngModel)]="downloadAction" [disabled]="provider === 'AllDebrid'">
<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" *ngIf="provider === 'AllDebrid'">
This option is only available for RealDebrid. AllDebrid 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">
@ -59,6 +76,10 @@
</div>
</div>
</div>
<p class="help" *ngIf="provider === 'AllDebrid'">
When downloading with AllDebrid it cannot be guaranteed that only files above this limit will be download as
some files are grouped together in 1 large archive.
</p>
<p class="help" *ngIf="downloadAction === 2">This setting does not apply to manually selected files.</p>
</div>
@ -93,28 +114,14 @@
<div class="field">
<label class="label">Automatic retry downloads</label>
<div class="control">
<input
class="input"
type="number"
max="1000"
min="0"
step="1"
[(ngModel)]="downloadRetryAttempts"
/>
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="downloadRetryAttempts" />
</div>
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
</div>
<div class="field">
<label class="label">Automatic retry torrent</label>
<div class="control">
<input
class="input"
type="number"
max="1000"
min="0"
step="1"
[(ngModel)]="torrentRetryAttempts"
/>
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="torrentRetryAttempts" />
</div>
<p class="help">
When a single download has failed multiple times (see setting above) or when the torrent itself received an
@ -122,12 +129,12 @@
</p>
</div>
</div>
<div fxFlex>
<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 Real-Debrid. <br />
It is possible that there are more files in the torrent, which are not shown here.
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">
@ -136,7 +143,7 @@
Select all
</label>
</div>
<div class="field" *ngIf="downloadAction === 2">
<div class="field" *ngIf="downloadAction === 2 && availableFiles != null">
<label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles">
<input
type="checkbox"
@ -146,9 +153,10 @@
{{ file.filename }} ({{ file.filesize | filesize }})
</label>
</div>
<div class="field" *ngIf="downloadAction !== 2">
<div class="field" *ngIf="downloadAction !== 2 && availableFiles != null">
<label class="is-fullwidth-label is-block" *ngFor="let file of availableFiles">
{{ file.filename }} ({{ file.filesize | filesize }})
{{ file.filename }}
<span *ngIf="file.filesize > 0">({{ file.filesize | filesize }})</span>
</label>
</div>
</div>

View file

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TorrentService } from 'src/app/torrent.service';
import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
import { SettingsService } from '../settings.service';
@Component({
selector: 'app-add-new-torrent',
@ -13,6 +14,8 @@ export class AddNewTorrentComponent implements OnInit {
public magnetLink: string;
private currentTorrentFile: string;
public provider: string;
public category: string;
public priority: number;
@ -24,7 +27,7 @@ export class AddNewTorrentComponent implements OnInit {
public downloadRetryAttempts: number = 3;
public torrentRetryAttempts: number = 1;
public availableFiles: TorrentFileAvailability[] = [];
public availableFiles: TorrentFileAvailability[];
public downloadFiles: { [key: string]: boolean } = {};
public allSelected: boolean;
@ -33,7 +36,11 @@ export class AddNewTorrentComponent implements OnInit {
private selectedFile: File;
constructor(private router: Router, private torrentService: TorrentService) {}
constructor(private router: Router, private torrentService: TorrentService, private settingsService: SettingsService) {
this.settingsService.get().subscribe(settings => {
this.provider = settings.firstOrDefault(m => m.settingId === 'Provider')?.value;
});
}
ngOnInit(): void {}
@ -143,7 +150,7 @@ export class AddNewTorrentComponent implements OnInit {
this.saving = true;
this.error = null;
this.availableFiles = [];
this.availableFiles = null;
this.downloadFiles = {};
this.allSelected = true;

View file

@ -1,4 +1,5 @@
export class Profile {
public provider: string;
public userName: string;
public expiration: Date;
}

View file

@ -38,7 +38,7 @@
</span>
<span>Settings</span>
</a>
<a class="navbar-item" *ngIf="profile" href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener">
<a class="navbar-item" *ngIf="profile" href="{{providerLink}}" target="_blank" rel="noopener">
<span class="icon">
<i class="fas fa-euro-sign" aria-hidden="true"></i>
</span>
@ -55,7 +55,7 @@
<a class="navbar-item"> Profile </a>
<a class="navbar-item" (click)="logout()"> Logout </a>
<hr class="navbar-divider" />
<div class="navbar-item">Version 1.9.6</div>
<div class="navbar-item">Version 1.9.7</div>
</div>
</div>
</div>

View file

@ -13,12 +13,22 @@ export class NavbarComponent implements OnInit {
public showMobileMenu = false;
public profile: Profile;
public providerLink: string;
constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {}
ngOnInit(): void {
this.settingsService.getProfile().subscribe((result) => {
this.profile = result;
switch (result.provider) {
case 'RealDebrid':
this.providerLink = 'https://real-debrid.com/?id=1348683';
break;
case 'AllDebrid':
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
break;
}
});
}

View file

@ -3,6 +3,9 @@
<li [ngClass]="{ 'is-active': activeTab === 0 }" (click)="activeTab = 0">
<a>General</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 4 }" (click)="activeTab = 4">
<a>Provider</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 1 }" (click)="activeTab = 1">
<a>Download Client</a>
</li>
@ -15,8 +18,26 @@
</ul>
</div>
<div *ngIf="activeTab === 0">
<div *ngIf="activeTab === 4">
<div class="field">
<label class="label">Provider</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingProvider">
<option value="RealDebrid">Real-Debrid</option>
<option value="AllDebrid">AllDebrid</option>
</select>
</div>
<p class="help">
The following 2 providers are supported:
<br />
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener">https://real-debrid.com</a>
<br />
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener">https://alldebrid.com</a>
<br/>
At this point only 1 provider can be used at the time.
</p>
</div>
<div class="field" *ngIf="settingProvider === 'RealDebrid'">
<label class="label">Real-Debrid API Key</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
@ -26,7 +47,19 @@
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener">https://real-debrid.com/apitoken</a>.
</p>
</div>
<div class="field" *ngIf="settingProvider === 'AllDebrid'">
<label class="label">AllDebrid API Key</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
</div>
<p class="help">
You can find your API key here:
<a href="https://alldebrid.com/apikeys/" target="_blank" rel="noopener">https://alldebrid.com/apikeys/</a>.
</p>
</div>
</div>
<div *ngIf="activeTab === 0">
<div class="field">
<label class="label">Log level</label>
<div class="control select is-fullwidth">

View file

@ -26,6 +26,7 @@ export class SettingsComponent implements OnInit {
public testAria2cConnectionSuccess: string = null;
public settingLogLevel: string;
public settingProvider: string;
public settingRealDebridApiKey: string;
public settingDownloadPath: string;
public settingMappedPath: string;
@ -57,6 +58,7 @@ export class SettingsComponent implements OnInit {
this.settingsService.get().subscribe(
(results) => {
this.settingProvider = this.getSetting(results, 'Provider');
this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey');
this.settingLogLevel = this.getSetting(results, 'LogLevel');
this.settingDownloadPath = this.getSetting(results, 'DownloadPath');
@ -86,6 +88,10 @@ export class SettingsComponent implements OnInit {
this.saving = true;
const settings: Setting[] = [
{
settingId: 'Provider',
value: this.settingProvider,
},
{
settingId: 'RealDebridApiKey',
value: this.settingRealDebridApiKey,

View file

@ -28,11 +28,7 @@
</div>
</div>
<div class="field">
<button
class="button is-success"
[ngClass]="{ 'is-loading': working }"
[disabled]="working"
>
<button class="button is-success" [ngClass]="{ 'is-loading': working }" [disabled]="working">
Setup
</button>
</div>
@ -46,25 +42,46 @@
<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?
Not premium yet? You have the choice of 2 providers:
<br/>
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
>Use this link to sign up to Real-Debrid.</a
>
<br /><br />
To connect to Real-Debrid please enter your Personal Prive API Token found here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
>https://real-debrid.com/apitoken</a
>.
<br/>
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener"
>Use this link to sign up to AllDebrid.</a
>
<br/>
<small>(Referal links)</small>
</div>
<div class="field">
<label class="label">Provider</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="provider">
<option value="RealDebrid">Real-Debrid</option>
<option value="AllDebrid">AllDebrid</option>
</select>
</div>
</div>
<div class="field">
<label for="" class="label">API Private Token</label>
<div class="control has-icons-left">
<div class="control">
<input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required />
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
</div>
<p class="help" *ngIf="provider === 'RealDebrid'">
You can find your API key here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
>https://real-debrid.com/apitoken</a
>.
</p>
<p class="help" *ngIf="provider === 'AllDebrid'">
You can find your API key here:
<a href="https://alldebrid.com/apikeys/" target="_blank" rel="noopener"
>https://alldebrid.com/apikeys/</a
>.
</p>
</div>
<div class="field">
<button
class="button is-success"

View file

@ -12,6 +12,7 @@ import { SettingsService } from '../settings.service';
export class SetupComponent implements OnInit {
public userName: string;
public password: string;
public provider = 'RealDebrid';
public token: string;
public error: string;
@ -40,11 +41,15 @@ export class SetupComponent implements OnInit {
}
public setToken(): void {
const setting = new Setting();
setting.settingId = 'RealDebridApiKey';
setting.value = this.token;
const settingToken = new Setting();
settingToken.settingId = 'RealDebridApiKey';
settingToken.value = this.token;
this.settingsService.update([setting]).subscribe(
const settingProvider = new Setting();
settingProvider.settingId = 'Provider';
settingProvider.value = this.provider;
this.settingsService.update([settingToken, settingProvider]).subscribe(
() => {
this.step = 3;
this.working = false;

View file

@ -47,7 +47,7 @@ export class TorrentService {
public uploadFile(file: File, torrent: Torrent): Observable<void> {
const formData: FormData = new FormData();
formData.append('file', file);
formData.append('formData', JSON.stringify(torrent));
formData.append('formData', JSON.stringify({ torrent }));
return this.http.post<void>(`/Api/Torrents/UploadFile`, formData);
}

View file

@ -1,10 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UsersService {
constructor() {}
public getLoggedInUser() {}
}

View file

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

View file

@ -40,6 +40,12 @@ namespace RdtClient.Data.Data
{
var seedSettings = new List<Setting>
{
new Setting
{
SettingId = "Provider",
Type = "String",
Value = "RealDebrid"
},
new Setting
{
SettingId = "RealDebridApiKey",

View file

@ -48,6 +48,7 @@ namespace RdtClient.Data.Data
Get = new DbSettings
{
Provider = GetString("Provider"),
RealDebridApiKey = GetString("RealDebridApiKey"),
DownloadPath = GetString("DownloadPath"),
DownloadClient = GetString("DownloadClient"),

View file

@ -1,6 +1,6 @@
namespace RdtClient.Data.Enums
{
public enum RealDebridStatus
public enum TorrentStatus
{
Processing = 0,
WaitingForFileSelection = 1,

View file

@ -46,7 +46,7 @@ namespace RdtClient.Data.Models.Data
public String RdHost { get; set; }
public Int64 RdSplit { get; set; }
public Int64 RdProgress { get; set; }
public RealDebridStatus RdStatus { get; set; }
public TorrentStatus RdStatus { get; set; }
public String RdStatusRaw { get; set; }
public DateTimeOffset RdAdded { get; set; }
public DateTimeOffset? RdEnded { get; set; }

View file

@ -4,6 +4,7 @@ namespace RdtClient.Data.Models.Internal
{
public class DbSettings
{
public String Provider { get; set; }
public String RealDebridApiKey { get; set; }
public String DownloadPath { get; set; }
public String DownloadClient { get; set; }

View file

@ -4,6 +4,7 @@ namespace RdtClient.Data.Models.Internal
{
public class Profile
{
public String Provider { get; set; }
public String UserName { get; set; }
public DateTimeOffset? Expiration { get; set; }
}

View file

@ -15,6 +15,7 @@ namespace RdtClient.Data.Models.TorrentClient
public Int64 Split { get; set; }
public Int64 Progress { get; set; }
public String Status { get; set; }
public Int64 StatusCode { get; set; }
public DateTimeOffset Added { get; set; }
public List<TorrentClientFile> Files { get; set; }
public List<String> Links { get; set; }

View file

@ -5,6 +5,6 @@ namespace RdtClient.Data.Models.TorrentClient
public class TorrentClientUser
{
public String Username { get; set; }
public DateTimeOffset Expiration { get; set; }
public DateTimeOffset? Expiration { get; set; }
}
}

View file

@ -8,6 +8,7 @@ namespace RdtClient.Service
{
public static void Config(IServiceCollection services)
{
services.AddScoped<AllDebridTorrentClient>();
services.AddScoped<Authentication>();
services.AddScoped<Downloads>();
services.AddScoped<QBittorrent>();

View file

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AllDebrid.NET" Version="1.0.1" />
<PackageReference Include="AllDebrid.NET" Version="1.0.2" />
<PackageReference Include="Aria2.NET" Version="1.0.4" />
<PackageReference Include="Downloader" Version="2.2.9" />
<PackageReference Include="MonoTorrent" Version="2.0.1" />

View file

@ -307,7 +307,7 @@ namespace RdtClient.Service.Services
var allDownloadsComplete = torrent.Downloads.All(m => m.Completed.HasValue);
var hasDownloadsWithErrors = torrent.Downloads.Any(m => m.Error != null);
if (torrent.Downloads.Count == 0 || hasDownloadsWithErrors || torrent.RdStatus == RealDebridStatus.Error)
if (torrent.Downloads.Count == 0 || hasDownloadsWithErrors || torrent.RdStatus == TorrentStatus.Error)
{
result.State = "error";
}

View file

@ -4,16 +4,23 @@ using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using AllDebridNET;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
namespace RdtClient.Service.Services.TorrentClients
{
public class AllDebridTorrentClient : ITorrentClient
{
private readonly ILogger<AllDebridTorrentClient> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public AllDebridTorrentClient(IHttpClientFactory httpClientFactory)
public AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
@ -48,13 +55,14 @@ namespace RdtClient.Service.Services.TorrentClients
Split = 0,
Progress = torrent.ProcessingPerc,
Status = torrent.Status,
StatusCode = torrent.StatusCode,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate),
Files = (torrent.Links ?? new List<Link>()).Select(m => new TorrentClientFile
Files = (torrent.Links ?? new List<Link>()).Select((m, i) => new TorrentClientFile
{
Path = m.Filename,
Bytes = m.Size,
Id = 0,
Selected = true
Id = i,
Selected = true,
}).ToList(),
Links = (torrent.Links ?? new List<Link>()).Select(m => m.LinkUrl.ToString()).ToList(),
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate),
@ -76,7 +84,7 @@ namespace RdtClient.Service.Services.TorrentClients
return new TorrentClientUser
{
Username = user.Username,
Expiration = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(user.PremiumUntil)
Expiration = user.PremiumUntil > 0 ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(user.PremiumUntil) : null
};
}
@ -94,12 +102,26 @@ namespace RdtClient.Service.Services.TorrentClients
return result.Id.ToString();
}
public Task<List<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{
return Task.FromResult(new List<TorrentClientAvailableFile>());
var isAvailable = await GetClient().Magnet.InstantAvailabilityAsync(hash);
if (isAvailable)
{
return new List<TorrentClientAvailableFile>
{
new TorrentClientAvailableFile
{
Filename = "All files",
Filesize = 0
}
};
}
return new List<TorrentClientAvailableFile>();
}
public Task SelectFiles(String torrentId, IList<String> fileIds)
public Task SelectFiles(Torrent torrent)
{
return Task.CompletedTask;
}
@ -122,5 +144,136 @@ namespace RdtClient.Service.Services.TorrentClients
return result.Link;
}
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent torrentClientTorrent)
{
try
{
torrentClientTorrent ??= await GetInfo(torrent.RdId);
if (!String.IsNullOrWhiteSpace(torrentClientTorrent.Filename))
{
torrent.RdName = torrentClientTorrent.Filename;
}
if (torrentClientTorrent.Bytes > 0)
{
torrent.RdSize = torrentClientTorrent.Bytes;
}
if (torrentClientTorrent.Files != null)
{
torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files);
}
torrent.RdHost = torrentClientTorrent.Host;
torrent.RdSplit = torrentClientTorrent.Split;
torrent.RdProgress = torrentClientTorrent.Progress;
torrent.RdAdded = torrentClientTorrent.Added;
torrent.RdEnded = torrentClientTorrent.Ended;
torrent.RdSpeed = torrentClientTorrent.Speed;
torrent.RdSeeders = torrentClientTorrent.Seeders;
torrent.RdStatusRaw = torrentClientTorrent.Status;
torrent.RdStatus = torrentClientTorrent.StatusCode switch
{
0 => TorrentStatus.Processing, // Processing In Queue.
1 => TorrentStatus.Downloading, // Processing Downloading.
2 => TorrentStatus.Downloading, // Processing Compressing / Moving.
3 => TorrentStatus.Uploading, // Processing Uploading.
4 => TorrentStatus.Finished, // Finished Ready.
5 => TorrentStatus.Error, // Error Upload fail.
6 => TorrentStatus.Error, // Error Internal error on unpacking.
7 => TorrentStatus.Error, // Error Not downloaded in 20 min.
8 => TorrentStatus.Error, // Error File too big.
9 => TorrentStatus.Error, // Error Internal error.
10 => TorrentStatus.Error, // Error Download took more than 72h.
11 => TorrentStatus.Error, // Error Deleted on the hoster website
_ => TorrentStatus.Error
};
}
catch (Exception ex)
{
if (ex.Message == "Resource not found")
{
torrent.RdStatusRaw = "deleted";
}
else
{
throw;
}
}
return torrent;
}
public async Task<IList<String>> GetDownloadLinks(Torrent torrent)
{
var magnet = await GetClient().Magnet.StatusAsync(torrent.RdId);
var links = magnet.Links;
Log($"Getting download links", torrent);
if (torrent.DownloadMinSize > 0)
{
var minFileSize = torrent.DownloadMinSize * 1024 * 1024;
Log($"Determining which files are over {minFileSize} bytes", torrent);
links = links.Where(m => m.Size > minFileSize)
.ToList();
Log($"Found {links.Count} files that match the minimum file size criterea", torrent);
}
if (links.Count == 0)
{
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
links = magnet.Links;
}
Log($"Selecting links:");
foreach (var link in links)
{
var fileList = link.Files.SelectMany(file => GetFiles(file));
Log($"{link.Filename} ({link.Size}b) {link.LinkUrl}, contains {String.Join(", ", fileList)}");
}
Log("", torrent);
return links.Select(m => m.LinkUrl.ToString()).ToList();
}
private static IList<String> GetFiles(File file, String parent = null)
{
var result = new List<String>
{
$"{parent}/{file.Name}"
};
if (file.Files != null && file.Files.Count > 0)
{
foreach (var subFile in file.Files)
{
result.AddRange(GetFiles(subFile, file.Name));
}
}
return result;
}
private void Log(String message, Torrent torrent = null)
{
if (torrent != null)
{
message = $"{message} {torrent.ToLog()}";
}
_logger.LogDebug(message);
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
namespace RdtClient.Service.Services.TorrentClients
@ -11,10 +12,12 @@ namespace RdtClient.Service.Services.TorrentClients
Task<TorrentClientUser> GetUser();
Task<String> AddMagnet(String magnetLink);
Task<String> AddFile(Byte[] bytes);
Task<List<TorrentClientAvailableFile>> GetAvailableFiles(String hash);
Task SelectFiles(String torrentId, IList<String> fileIds);
Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash);
Task SelectFiles(Torrent torrent);
Task<TorrentClientTorrent> GetInfo(String torrentId);
Task Delete(String torrentId);
Task<String> Unrestrict(String link);
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent torrentClientTorrent);
Task<IList<String>> GetDownloadLinks(Torrent torrent);
}
}

View file

@ -3,17 +3,23 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RDNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
namespace RdtClient.Service.Services.TorrentClients
{
public class RealDebridTorrentClient : ITorrentClient
{
private readonly ILogger<RealDebridTorrentClient> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public RealDebridTorrentClient(IHttpClientFactory httpClientFactory)
public RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
@ -93,7 +99,7 @@ namespace RdtClient.Service.Services.TorrentClients
return new TorrentClientUser
{
Username = user.Username,
Expiration = user.Expiration
Expiration = user.Premium > 0 ? user.Expiration : null
};
}
@ -111,7 +117,7 @@ namespace RdtClient.Service.Services.TorrentClients
return result.Id;
}
public async Task<List<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{
var result = await GetClient().Torrents.GetAvailableFiles(hash);
@ -128,9 +134,66 @@ namespace RdtClient.Service.Services.TorrentClients
return torrentClientAvailableFiles;
}
public async Task SelectFiles(String torrentId, IList<String> fileIds)
public async Task SelectFiles(Data.Models.Data.Torrent torrent)
{
await GetClient().Torrents.SelectFilesAsync(torrentId, fileIds.ToArray());
var files = torrent.Files;
Log("Seleting files", torrent);
if (torrent.DownloadAction == TorrentDownloadAction.DownloadAvailableFiles)
{
Log($"Determining which files are already available on RealDebrid", torrent);
var availableFiles = await GetAvailableFiles(torrent.Hash);
Log($"Found {files.Count}/{torrent.Files.Count} available files on RealDebrid", torrent);
files = torrent.Files.Where(m => availableFiles.Any(f => m.Path.EndsWith(f.Filename))).ToList();
}
else if (torrent.DownloadAction == TorrentDownloadAction.DownloadAll)
{
Log("Selecting all files", torrent);
files = torrent.Files.ToList();
}
else if (torrent.DownloadAction == TorrentDownloadAction.DownloadManual)
{
Log("Selecting manual selected files", torrent);
files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList();
}
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
if (torrent.DownloadAction != TorrentDownloadAction.DownloadManual && torrent.DownloadMinSize > 0)
{
var minFileSize = torrent.DownloadMinSize * 1024 * 1024;
Log($"Determining which files are over {minFileSize} bytes", torrent);
files = files.Where(m => m.Bytes > minFileSize)
.ToList();
Log($"Found {files.Count} files that match the minimum file size criterea", torrent);
}
if (files.Count == 0)
{
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
files = torrent.Files;
}
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
Log($"Selecting files:");
foreach (var file in files)
{
Log($"{file.Id}: {file.Path} ({file.Bytes}b)");
}
Log("", torrent);
await GetClient().Torrents.SelectFilesAsync(torrent.RdId, fileIds.ToArray());
}
public async Task<TorrentClientTorrent> GetInfo(String torrentId)
@ -151,5 +214,104 @@ namespace RdtClient.Service.Services.TorrentClients
return result.Download;
}
public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, TorrentClientTorrent torrentClientTorrent)
{
try
{
var rdTorrent = await GetInfo(torrent.RdId);
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
{
torrent.RdName = rdTorrent.Filename;
}
if (!String.IsNullOrWhiteSpace(rdTorrent.OriginalFilename))
{
torrent.RdName = rdTorrent.OriginalFilename;
}
if (rdTorrent.Bytes > 0)
{
torrent.RdSize = rdTorrent.Bytes;
}
else if (rdTorrent.OriginalBytes > 0)
{
torrent.RdSize = rdTorrent.OriginalBytes;
}
if (rdTorrent.Files != null)
{
torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files);
}
torrent.RdHost = rdTorrent.Host;
torrent.RdSplit = rdTorrent.Split;
torrent.RdProgress = rdTorrent.Progress;
torrent.RdAdded = rdTorrent.Added;
torrent.RdEnded = rdTorrent.Ended;
torrent.RdSpeed = rdTorrent.Speed;
torrent.RdSeeders = rdTorrent.Seeders;
torrent.RdStatusRaw = rdTorrent.Status;
torrent.RdStatus = rdTorrent.Status switch
{
"magnet_error" => TorrentStatus.Error,
"magnet_conversion" => TorrentStatus.Processing,
"waiting_files_selection" => TorrentStatus.WaitingForFileSelection,
"queued" => TorrentStatus.Downloading,
"downloading" => TorrentStatus.Downloading,
"downloaded" => TorrentStatus.Finished,
"error" => TorrentStatus.Error,
"virus" => TorrentStatus.Error,
"compressing" => TorrentStatus.Downloading,
"uploading" => TorrentStatus.Uploading,
"dead" => TorrentStatus.Error,
_ => TorrentStatus.Error
};
}
catch (Exception ex)
{
if (ex.Message == "Resource not found")
{
torrent.RdStatusRaw = "deleted";
}
else
{
throw;
}
}
return torrent;
}
public async Task<IList<String>> GetDownloadLinks(Data.Models.Data.Torrent torrent)
{
var rdTorrent = await GetInfo(torrent.RdId);
var torrentLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList();
Log($"Found {torrentLinks} links", torrent);
// Sometimes RD will give you 1 rar with all files, sometimes it will give you 1 link per file.
if (torrent.Files.Count(m => m.Selected) != torrentLinks.Count &&
torrent.ManualFiles.Count != torrentLinks.Count &&
torrentLinks.Count != 1)
{
return null;
}
return torrentLinks;
}
private void Log(String message, Data.Models.Data.Torrent torrent = null)
{
if (torrent != null)
{
message = $"{message} {torrent.ToLog()}";
}
_logger.LogDebug(message);
}
}
}

View file

@ -473,7 +473,7 @@ namespace RdtClient.Service.Services
Log("Processing", torrent);
// If torrent is erroring out on the Real-Debrid side.
if (torrent.RdStatus == RealDebridStatus.Error)
if (torrent.RdStatus == TorrentStatus.Error)
{
Log($"Torrent reported an error: {torrent.RdStatusRaw}", torrent);
Log($"Torrent retry count {torrent.RetryCount}/{torrent.TorrentRetryAttempts}", torrent);
@ -486,75 +486,26 @@ namespace RdtClient.Service.Services
}
// Real-Debrid is waiting for file selection, select which files to download.
if ((torrent.RdStatus == RealDebridStatus.WaitingForFileSelection || torrent.RdStatus == RealDebridStatus.Finished) &&
if ((torrent.RdStatus == TorrentStatus.WaitingForFileSelection || torrent.RdStatus == TorrentStatus.Finished) &&
torrent.FilesSelected == null &&
torrent.Downloads.Count == 0 &&
torrent.FilesSelected == null)
torrent.Downloads.Count == 0)
{
Log($"Selecting files", torrent);
var files = torrent.Files;
if (torrent.DownloadAction == TorrentDownloadAction.DownloadAvailableFiles)
{
Log($"Determining which files are already available on RealDebrid", torrent);
var availableFiles = await _torrents.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.ToList();
}
else if (torrent.DownloadAction == TorrentDownloadAction.DownloadManual)
{
Log("Selecting manual selected files", torrent);
files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList();
}
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
if (torrent.DownloadAction != TorrentDownloadAction.DownloadManual && torrent.DownloadMinSize > 0)
{
var minFileSize = torrent.DownloadMinSize * 1024 * 1024;
Log($"Determining which files are over {minFileSize} bytes", torrent);
files = files.Where(m => m.Bytes > minFileSize)
.ToList();
Log($"Found {files.Count} files that match the minimum file size criterea", torrent);
}
if (files.Count == 0)
{
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
files = torrent.Files;
}
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
Log($"Selecting files:{Environment.NewLine}{String.Join(Environment.NewLine, files.Select(m => m.Path))}", torrent);
await _torrents.SelectFiles(torrent.TorrentId, fileIds);
await _torrents.SelectFiles(torrent.TorrentId);
await _torrents.UpdateFilesSelected(torrent.TorrentId, DateTime.UtcNow);
}
// Real-Debrid finished downloading the torrent, process the file to host.
if (torrent.RdStatus == RealDebridStatus.Finished)
if (torrent.RdStatus == TorrentStatus.Finished)
{
// The files are selected but there are no downloads yet, check if Real-Debrid has generated links yet.
if (torrent.Downloads.Count == 0 && torrent.FilesSelected != null)
{
Log($"Checking for links", torrent);
Log($"Creating downloads", torrent);
await _torrents.CheckForLinks(torrent.TorrentId);
await _torrents.CreateDownloads(torrent.TorrentId);
}
}

View file

@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
using MonoTorrent;
using Newtonsoft.Json;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
@ -27,18 +26,24 @@ namespace RdtClient.Service.Services
private readonly ITorrentClient _torrentClient;
public static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
public Torrents(ILogger<Torrents> logger,
TorrentData torrentData,
Downloads downloads,
AllDebridTorrentClient allDebridTorrentClient,
RealDebridTorrentClient realDebridTorrentClient)
{
_logger = logger;
_torrentData = torrentData;
_downloads = downloads;
_torrentClient = realDebridTorrentClient;
_torrentClient = Settings.Get.Provider switch
{
"RealDebrid" => realDebridTorrentClient,
"AllDebrid" => allDebridTorrentClient,
_ => null
};
}
public async Task<IList<Torrent>> Get()
@ -73,7 +78,7 @@ namespace RdtClient.Service.Services
if (torrent != null)
{
await UpdateRdData(torrent);
await UpdateTorrentClientData(torrent);
}
return torrent;
@ -144,14 +149,14 @@ namespace RdtClient.Service.Services
return newTorrent;
}
public async Task<List<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{
var result = await _torrentClient.GetAvailableFiles(hash);
return result;
}
public async Task SelectFiles(Guid torrentId, IList<String> fileIds)
public async Task SelectFiles(Guid torrentId)
{
var torrent = await GetById(torrentId);
@ -160,10 +165,10 @@ namespace RdtClient.Service.Services
return;
}
await _torrentClient.SelectFiles(torrent.RdId, fileIds);
await _torrentClient.SelectFiles(torrent);
}
public async Task CheckForLinks(Guid torrentId)
public async Task CreateDownloads(Guid torrentId)
{
var torrent = await GetById(torrentId);
@ -172,16 +177,9 @@ namespace RdtClient.Service.Services
return;
}
var rdTorrent = await _torrentClient.GetInfo(torrent.RdId);
var torrentLinks = await _torrentClient.GetDownloadLinks(torrent);
var torrentLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList();
Log($"Found {torrentLinks} links", torrent);
// Sometimes RD will give you 1 rar with all files, sometimes it will give you 1 link per file.
if (torrent.Files.Count(m => m.Selected) != torrentLinks.Count &&
torrent.ManualFiles.Count != torrentLinks.Count &&
torrentLinks.Count != 1)
if (torrentLinks == null)
{
return;
}
@ -313,6 +311,7 @@ namespace RdtClient.Service.Services
var profile = new Profile
{
Provider = Settings.Get.Provider,
UserName = user.Username,
Expiration = user.Expiration
};
@ -339,7 +338,7 @@ namespace RdtClient.Service.Services
continue;
}
await UpdateRdData(torrent);
await UpdateTorrentClientData(torrent, rdTorrent);
}
foreach (var torrent in torrents)
@ -509,7 +508,7 @@ namespace RdtClient.Service.Services
return null;
}
await UpdateRdData(torrent);
await UpdateTorrentClientData(torrent);
foreach (var download in torrent.Downloads)
{
@ -565,7 +564,7 @@ namespace RdtClient.Service.Services
isFile,
torrent);
await UpdateRdData(newTorrent);
await UpdateTorrentClientData(newTorrent);
return newTorrent;
}
@ -580,7 +579,7 @@ namespace RdtClient.Service.Services
await _torrentData.Update(torrent);
}
private async Task UpdateRdData(Torrent torrent)
private async Task UpdateTorrentClientData(Torrent torrent, TorrentClientTorrent torrentClientTorrent = null)
{
var originalTorrent = JsonConvert.SerializeObject(torrent,
new JsonSerializerSettings
@ -588,70 +587,7 @@ namespace RdtClient.Service.Services
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
try
{
var rdTorrent = await _torrentClient.GetInfo(torrent.RdId);
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
{
torrent.RdName = rdTorrent.Filename;
}
if (!String.IsNullOrWhiteSpace(rdTorrent.OriginalFilename))
{
torrent.RdName = rdTorrent.OriginalFilename;
}
if (rdTorrent.Bytes > 0)
{
torrent.RdSize = rdTorrent.Bytes;
}
else if (rdTorrent.OriginalBytes > 0)
{
torrent.RdSize = rdTorrent.OriginalBytes;
}
if (rdTorrent.Files != null)
{
torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files);
}
torrent.RdHost = rdTorrent.Host;
torrent.RdSplit = rdTorrent.Split;
torrent.RdProgress = rdTorrent.Progress;
torrent.RdAdded = rdTorrent.Added;
torrent.RdEnded = rdTorrent.Ended;
torrent.RdSpeed = rdTorrent.Speed;
torrent.RdSeeders = rdTorrent.Seeders;
torrent.RdStatusRaw = rdTorrent.Status;
torrent.RdStatus = rdTorrent.Status switch
{
"magnet_error" => RealDebridStatus.Error,
"magnet_conversion" => RealDebridStatus.Processing,
"waiting_files_selection" => RealDebridStatus.WaitingForFileSelection,
"queued" => RealDebridStatus.Downloading,
"downloading" => RealDebridStatus.Downloading,
"downloaded" => RealDebridStatus.Finished,
"error" => RealDebridStatus.Error,
"virus" => RealDebridStatus.Error,
"compressing" => RealDebridStatus.Downloading,
"uploading" => RealDebridStatus.Uploading,
"dead" => RealDebridStatus.Error,
_ => RealDebridStatus.Error
};
}
catch (Exception ex)
{
if (ex.Message == "Resource not found")
{
torrent.RdStatusRaw = "deleted";
}
else
{
throw;
}
}
await _torrentClient.UpdateData(torrent, torrentClientTorrent);
var newTorrent = JsonConvert.SerializeObject(torrent,
new JsonSerializerSettings

View file

@ -77,6 +77,15 @@ namespace RdtClient.Web.Controllers
[ModelBinder(BinderType = typeof(JsonModelBinder))]
TorrentControllerUploadFileRequest formData)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Select(x => x.Value.Errors)
.Where(y => y.Count > 0)
.ToList();
return BadRequest(errors);
}
if (file == null || file.Length <= 0)
{
throw new Exception("Invalid torrent file");
@ -99,6 +108,15 @@ namespace RdtClient.Web.Controllers
[Route("UploadMagnet")]
public async Task<ActionResult> UploadMagnet([FromBody] TorrentControllerUploadMagnetRequest request)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Select(x => x.Value.Errors)
.Where(y => y.Count > 0)
.ToList();
return BadRequest(errors);
}
await _torrents.UploadMagnet(request.MagnetLink, request.Torrent);
return Ok();
@ -152,6 +170,7 @@ namespace RdtClient.Web.Controllers
[Route("Retry/{torrentId:guid}")]
public async Task<ActionResult> Retry(Guid torrentId)
{
await _torrents.UpdateRetry(torrentId, DateTimeOffset.UtcNow, 0);
await _torrents.RetryTorrent(torrentId, 0);
return Ok();

View file

@ -4,7 +4,7 @@
<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
<Version>1.9.6</Version>
<Version>1.9.7</Version>
</PropertyGroup>
<ItemGroup>