Usenet support

This commit is contained in:
omgbeez 2026-02-13 13:15:57 -05:00 committed by Clifford Roche
parent d76d26131c
commit e93e4f6f22
69 changed files with 4678 additions and 803 deletions

View file

@ -1,5 +1,24 @@
<div class="field"> <div class="field">
<div> <div class="tabs is-toggle is-fullwidth">
<ul>
<li [class.is-active]="type === 'torrent'">
<a (click)="changeType('torrent')">
<span class="icon is-small"><i class="fas fa-magnet" aria-hidden="true"></i></span>
<span>Torrent</span>
</a>
</li>
<li [class.is-active]="type === 'nzb'">
<a (click)="changeType('nzb')">
<span class="icon is-small"><i class="fas fa-file-archive" aria-hidden="true"></i></span>
<span>NZB</span>
</a>
</li>
</ul>
</div>
</div>
<div class="field">
<div [hidden]="type !== 'torrent'">
<div class="field"> <div class="field">
<label class="label">Torrent file</label> <label class="label">Torrent file</label>
<div class="file has-name"> <div class="file has-name">
@ -31,15 +50,53 @@
></textarea> ></textarea>
</div> </div>
</div> </div>
</div>
<div [hidden]="type !== 'nzb'">
<div class="field"> <div class="field">
<div class="control"> <label class="label">NZB file</label>
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()"> <div class="file has-name">
<span>Add Torrent</span> <label class="file-label">
</button> <input class="file-input" type="file" name="resume" (change)="pickFile($event)" [disabled]="saving" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Pick an NZB file... </span>
</span>
<span class="file-name">
{{ fileName }}
</span>
</label>
</div> </div>
</div> </div>
<div class="field">
<label class="label">NZB Link</label>
<div class="control">
<textarea
class="textarea"
placeholder="Paste your NZB link here"
[(ngModel)]="nzbLink"
[disabled]="saving"
></textarea>
</div>
</div>
</div>
<div class="field" style="margin-top: 10px">
<div class="control">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
@if (type === 'torrent') {
<span>Add Torrent</span>
} @else {
<span>Add NZB</span>
}
</button>
</div>
</div>
<div>
<div class="separator">Advanced settings</div> <div class="separator">Advanced settings</div>
<div class="field"> <div class="field">
@ -53,7 +110,7 @@
</select> </select>
</div> </div>
<p class="help"> <p class="help">
Select which downloader is used to download this torrent from the debrid provider to your host. Select which downloader is used to download this from the debrid provider to your host.
</p> </p>
</div> </div>
@ -66,7 +123,7 @@
</select> </select>
</div> </div>
<p class="help"> <p class="help">
When a torrent is finished downloading on the provider, perform this action. Use this setting if you only want When a download is finished on the provider, perform this action. Use this setting if you only want
to add files to your debrid provider but not download them to the host. to add files to your debrid provider but not download them to the host.
</p> </p>
</div> </div>
@ -142,10 +199,10 @@
<select [(ngModel)]="finishedAction"> <select [(ngModel)]="finishedAction">
<option [ngValue]="0">Do nothing</option> <option [ngValue]="0">Do nothing</option>
@if (downloadClient !== 2) { @if (downloadClient !== 2) {
<option [ngValue]="1">Remove torrent from provider and client</option> <option [ngValue]="1">Remove from provider and client</option>
<option [ngValue]="2">Remove torrent from provider</option> <option [ngValue]="2">Remove from provider</option>
} }
<option [ngValue]="3">Remove torrent from client</option> <option [ngValue]="3">Remove from client</option>
</select> </select>
</div> </div>
</div> </div>
@ -156,7 +213,7 @@
<input class="input" type="number" [(ngModel)]="finishedActionDelay" /> <input class="input" type="number" [(ngModel)]="finishedActionDelay" />
</div> </div>
<p class="help"> <p class="help">
When a torrent is finished downloading on the provider, perform this action. Use this setting if you only want When a download is finished on the provider, perform this action. Use this setting if you only want
to add files to your debrid provider but not download them to the host. to add files to your debrid provider but not download them to the host.
</p> </p>
</div> </div>
@ -175,9 +232,14 @@
<input class="input" type="number" step="1" [(ngModel)]="priority" /> <input class="input" type="number" step="1" [(ngModel)]="priority" />
</div> </div>
<p class="help"> <p class="help">
Set the priority for this torrent where 1 is the highest. When empty it will be assigned the lowest priority. Set the priority where 1 is the highest. When empty it will be assigned the lowest priority.
</p> </p>
</div> </div>
</div>
</div>
<div class="field">
<div>
<div class="field"> <div class="field">
<label class="label">Automatic retry downloads</label> <label class="label">Automatic retry downloads</label>
<div class="control"> <div class="control">
@ -186,13 +248,13 @@
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p> <p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Automatic retry torrent</label> <label class="label">Automatic retry download</label>
<div class="control"> <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> </div>
<p class="help"> <p class="help">
When a single download has failed multiple times (see setting above) or when the torrent itself received an When a single download has failed multiple times (see setting above) or when the download itself received an
error it will retry the full torrent this many times before marking it failed. error it will retry the full download this many times before marking it failed.
</p> </p>
</div> </div>
<div class="field"> <div class="field">
@ -206,13 +268,13 @@
</p> </p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Torrent maximum lifetime</label> <label class="label">Maximum lifetime</label>
<div class="control"> <div class="control">
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" /> <input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" />
</div> </div>
<p class="help"> <p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the The maximum lifetime of a download in minutes. When this time has passed, mark it as error. If the
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable. download is completed, the lifetime setting will not apply. 0 to disable.
</p> </p>
</div> </div>
</div> </div>
@ -226,10 +288,3 @@
</div> </div>
</div> </div>
<div class="field">
<div class="control">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
<span>Add Torrent</span>
</button>
</div>
</div>

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TorrentService } from 'src/app/torrent.service'; import { TorrentService } from 'src/app/torrent.service';
import { Torrent, TorrentFileAvailability } from '../models/torrent.model'; import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@ -15,8 +15,10 @@ import { NgClass } from '@angular/common';
standalone: true, standalone: true,
}) })
export class AddNewTorrentComponent implements OnInit { export class AddNewTorrentComponent implements OnInit {
public type: 'torrent' | 'nzb' = 'torrent';
public fileName: string; public fileName: string;
public magnetLink: string; public magnetLink: string;
public nzbLink: string;
private currentTorrentFile: string; private currentTorrentFile: string;
public provider: string; public provider: string;
@ -58,8 +60,19 @@ export class AddNewTorrentComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.queryParams.subscribe((params) => { this.activatedRoute.queryParams.subscribe((params) => {
if (params['type'] === 'nzb') {
this.type = 'nzb';
} else if (params['type'] === 'torrent') {
this.type = 'torrent';
}
if (params['magnet']) { if (params['magnet']) {
this.magnetLink = decodeURIComponent(params['magnet']); this.magnetLink = decodeURIComponent(params['magnet']);
this.type = 'torrent';
}
if (params['nzb']) {
this.nzbLink = decodeURIComponent(params['nzb']);
this.type = 'nzb';
} }
}); });
this.settingsService.get().subscribe((settings) => { this.settingsService.get().subscribe((settings) => {
@ -97,6 +110,12 @@ export class AddNewTorrentComponent implements OnInit {
} }
} }
public changeType(type: 'torrent' | 'nzb'): void {
this.type = type;
this.fileName = null;
this.selectedFile = null;
}
public pickFile(evt: Event): void { public pickFile(evt: Event): void {
const files = (evt.target as HTMLInputElement).files; const files = (evt.target as HTMLInputElement).files;
@ -152,25 +171,49 @@ export class AddNewTorrentComponent implements OnInit {
torrent.lifetime = this.torrentLifetime; torrent.lifetime = this.torrentLifetime;
torrent.downloadClient = this.downloadClient; torrent.downloadClient = this.downloadClient;
if (this.magnetLink) { if (this.type === 'torrent') {
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({ torrent.type = DownloadType.Torrent;
next: () => this.router.navigate(['/']), if (this.magnetLink) {
error: (err) => { this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({
this.error = err.error; next: () => this.router.navigate(['/']),
this.saving = false; error: (err) => {
}, this.error = err.error;
}); this.saving = false;
} else if (this.selectedFile) { },
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({ });
next: () => this.router.navigate(['/']), } else if (this.selectedFile) {
error: (err) => { this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({
this.error = err.error; next: () => this.router.navigate(['/']),
this.saving = false; error: (err) => {
}, this.error = err.error;
}); this.saving = false;
},
});
} else {
this.error = 'No magnet or file uploaded';
this.saving = false;
}
} else { } else {
this.error = 'No magnet or file uploaded'; if (this.nzbLink) {
this.saving = false; this.torrentService.uploadNzbLink(this.nzbLink, torrent).subscribe({
next: () => this.router.navigate(['/']),
error: (err) => {
this.error = err.error;
this.saving = false;
},
});
} else if (this.selectedFile) {
this.torrentService.uploadNzbFile(this.selectedFile, torrent).subscribe({
next: () => this.router.navigate(['/']),
error: (err) => {
this.error = err.error;
this.saving = false;
},
});
} else {
this.error = 'No NZB link or file uploaded';
this.saving = false;
}
} }
} }
@ -181,6 +224,9 @@ export class AddNewTorrentComponent implements OnInit {
} }
public checkFiles(): void { public checkFiles(): void {
if (this.type === 'nzb') {
return;
}
if (this.magnetLink && this.magnetLink === this.currentTorrentFile) { if (this.magnetLink && this.magnetLink === this.currentTorrentFile) {
return; return;
} }

View file

@ -28,6 +28,7 @@ export class Torrent {
public lifetime: number; public lifetime: number;
public priority: number; public priority: number;
public type: DownloadType;
public error: string; public error: string;
public rdId: string; public rdId: string;
@ -73,3 +74,8 @@ export enum RealDebridStatus {
Error = 99, Error = 99,
} }
export enum DownloadType {
Torrent = 0,
Nzb = 1,
}

View file

@ -24,13 +24,13 @@
<span class="icon"> <span class="icon">
<i class="fas fa-table" aria-hidden="true"></i> <i class="fas fa-table" aria-hidden="true"></i>
</span> </span>
<span>Torrents</span> <span>Torrents/NZBs</span>
</a> </a>
<a class="navbar-item" routerLink="/add"> <a class="navbar-item" routerLink="/add" [queryParams]="{ type: 'torrent' }">
<span class="icon"> <span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i> <i class="fas fa-plus" aria-hidden="true"></i>
</span> </span>
<span>Add New Torrent</span> <span>Add Torrent/NZB</span>
</a> </a>
<a class="navbar-item" routerLink="/settings"> <a class="navbar-item" routerLink="/settings">
<span class="icon"> <span class="icon">

View file

@ -77,7 +77,7 @@ export class TorrentStatusPipe implements PipeTransform {
case RealDebridStatus.Queued: case RealDebridStatus.Queued:
return 'Not Yet Added to Provider'; return 'Not Yet Added to Provider';
case RealDebridStatus.Downloading: case RealDebridStatus.Downloading:
if (torrent.rdSeeders < 1) { if (torrent.rdSeeders < 1 && torrent.type !== 1) {
return `Torrent stalled`; return `Torrent stalled`;
} }
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize'); const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');

View file

@ -79,6 +79,20 @@ export class TorrentService {
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadFile`, formData); return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadFile`, formData);
} }
public uploadNzbLink(nzbLink: string, torrent: Torrent): Observable<void> {
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadNzbLink`, {
nzbLink,
torrent,
});
}
public uploadNzbFile(file: File, torrent: Torrent): Observable<void> {
const formData: FormData = new FormData();
formData.append('file', file);
formData.append('formData', JSON.stringify({ torrent }));
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadNzbFile`, formData);
}
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> { public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFilesMagnet`, { return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFilesMagnet`, {
magnetLink, magnetLink,

View file

@ -13,6 +13,7 @@ public interface ITorrentData
String hash, String hash,
String? fileOrMagnetContents, String? fileOrMagnetContents,
Boolean isFile, Boolean isFile,
DownloadType downloadType,
DownloadClient downloadClient, DownloadClient downloadClient,
Torrent torrent); Torrent torrent);

View file

@ -9,7 +9,7 @@ namespace RdtClient.Data.Data;
public class SettingData(DataContext dataContext, ILogger<SettingData> logger) public class SettingData(DataContext dataContext, ILogger<SettingData> logger)
{ {
public static DbSettings Get { get; } = new(); public static DbSettings Get { get; private set; } = new();
public static IList<SettingProperty> GetAll() public static IList<SettingProperty> GetAll()
{ {

View file

@ -65,6 +65,7 @@ public class TorrentData(DataContext dataContext) : ITorrentData
String hash, String hash,
String? fileOrMagnetContents, String? fileOrMagnetContents,
Boolean isFile, Boolean isFile,
DownloadType downloadType,
DownloadClient downloadClient, DownloadClient downloadClient,
Torrent torrent) Torrent torrent)
{ {
@ -84,6 +85,7 @@ public class TorrentData(DataContext dataContext) : ITorrentData
ExcludeRegex = torrent.ExcludeRegex, ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles, DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = downloadClient, DownloadClient = downloadClient,
Type = downloadType,
FileOrMagnet = fileOrMagnetContents, FileOrMagnet = fileOrMagnetContents,
IsFile = isFile, IsFile = isFile,
Priority = torrent.Priority, Priority = torrent.Priority,
@ -141,6 +143,20 @@ public class TorrentData(DataContext dataContext) : ITorrentData
await dataContext.SaveChangesAsync(); await dataContext.SaveChangesAsync();
} }
public async Task UpdateHash(Torrent torrent, String hash)
{
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
if (dbTorrent == null)
{
return;
}
dbTorrent.Hash = hash.ToLower();
await dataContext.SaveChangesAsync();
}
public async Task Update(Torrent torrent) public async Task Update(Torrent torrent)
{ {
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId); var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);

View file

@ -0,0 +1,7 @@
namespace RdtClient.Data.Enums;
public enum DownloadType
{
Torrent = 0,
Nzb = 1
}

View file

@ -0,0 +1,485 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
#nullable disable
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20251224022148_AddDownloadTypeToTorrent")]
partial class AddDownloadTypeToTorrent
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("RemoteId")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<int?>("ClientKind")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DeleteOnError")
.HasColumnType("INTEGER");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<int>("DownloadClient")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<int>("DownloadRetryAttempts")
.HasColumnType("INTEGER");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("ExcludeRegex")
.HasColumnType("TEXT");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<int>("FinishedActionDelay")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("HostDownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("IncludeRegex")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<int>("Lifetime")
.HasColumnType("INTEGER");
b.Property<int?>("Priority")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long?>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long?>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long?>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int?>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Retry")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<int>("TorrentRetryAttempts")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RdtClient.Data.Migrations
{
/// <inheritdoc />
public partial class AddDownloadTypeToTorrent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Type",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Type",
table: "Torrents");
}
}
}

View file

@ -402,6 +402,9 @@ namespace RdtClient.Data.Migrations
b.Property<int>("TorrentRetryAttempts") b.Property<int>("TorrentRetryAttempts")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("TorrentId"); b.HasKey("TorrentId");
b.ToTable("Torrents"); b.ToTable("Torrents");

View file

@ -2,7 +2,7 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json; using System.Text.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
namespace RdtClient.Data.Models.Data; namespace RdtClient.Data.Models.Data;
@ -30,6 +30,7 @@ public class Torrent
public DateTimeOffset? Completed { get; set; } public DateTimeOffset? Completed { get; set; }
public DateTimeOffset? Retry { get; set; } public DateTimeOffset? Retry { get; set; }
public DownloadType Type { get; set; }
public String? FileOrMagnet { get; set; } public String? FileOrMagnet { get; set; }
public Boolean IsFile { get; set; } public Boolean IsFile { get; set; }
@ -61,7 +62,7 @@ public class Torrent
public String? RdFiles { get; set; } public String? RdFiles { get; set; }
[NotMapped] [NotMapped]
public IList<TorrentClientFile> Files public IList<DebridClientFile> Files
{ {
get get
{ {
@ -72,7 +73,7 @@ public class Torrent
try try
{ {
return JsonSerializer.Deserialize<List<TorrentClientFile>>(RdFiles) ?? []; return JsonSerializer.Deserialize<List<DebridClientFile>>(RdFiles) ?? [];
} }
catch catch
{ {

View file

@ -1,6 +1,6 @@
namespace RdtClient.Data.Models.TorrentClient; namespace RdtClient.Data.Models.DebridClient;
public class TorrentClientAvailableFile public class DebridClientAvailableFile
{ {
public String Filename { get; set; } = default!; public String Filename { get; set; } = default!;

View file

@ -1,6 +1,6 @@
namespace RdtClient.Data.Models.TorrentClient; namespace RdtClient.Data.Models.DebridClient;
public class TorrentClientFile public class DebridClientFile
{ {
public Int64 Id { get; set; } public Int64 Id { get; set; }
public String Path { get; set; } = default!; public String Path { get; set; } = default!;

View file

@ -1,6 +1,8 @@
namespace RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Enums;
public class TorrentClientTorrent namespace RdtClient.Data.Models.DebridClient;
public class DebridClientTorrent
{ {
public String Id { get; set; } = default!; public String Id { get; set; } = default!;
public String Filename { get; set; } = default!; public String Filename { get; set; } = default!;
@ -15,7 +17,9 @@ public class TorrentClientTorrent
public String? Message { get; set; } public String? Message { get; set; }
public Int64 StatusCode { get; set; } public Int64 StatusCode { get; set; }
public DateTimeOffset? Added { get; set; } public DateTimeOffset? Added { get; set; }
public List<TorrentClientFile>? Files { get; set; }
public DownloadType Type { get; set; } = DownloadType.Torrent;
public List<DebridClientFile>? Files { get; set; }
public List<String>? Links { get; set; } public List<String>? Links { get; set; }
public DateTimeOffset? Ended { get; set; } public DateTimeOffset? Ended { get; set; }
public Int64? Speed { get; set; } public Int64? Speed { get; set; }

View file

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

View file

@ -1,5 +1,6 @@
using System.ComponentModel; using RdtClient.Data.Enums;
using RdtClient.Data.Enums; using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
@ -263,6 +264,7 @@ public class DbSettingsWatch
[DisplayName("Check Interval")] [DisplayName("Check Interval")]
[Description("Time in seconds to check the folder for new files.")] [Description("Time in seconds to check the folder for new files.")]
[Range(10, Int32.MaxValue, ErrorMessage = "Interval must be at least 10 seconds.")]
public Int32 Interval { get; set; } = 60; public Int32 Interval { get; set; } = 60;
[DisplayName("Import Defaults")] [DisplayName("Import Defaults")]

View file

@ -1,5 +1,5 @@
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
namespace RdtClient.Data.Models.Internal; namespace RdtClient.Data.Models.Internal;
@ -20,6 +20,7 @@ public class TorrentDto
public DateTimeOffset Added { get; set; } public DateTimeOffset Added { get; set; }
public DateTimeOffset? FilesSelected { get; set; } public DateTimeOffset? FilesSelected { get; set; }
public DateTimeOffset? Completed { get; set; } public DateTimeOffset? Completed { get; set; }
public DownloadType Type { get; set; }
public Boolean IsFile { get; set; } public Boolean IsFile { get; set; }
public Int32? Priority { get; set; } public Int32? Priority { get; set; }
public Int32 RetryCount { get; set; } public Int32 RetryCount { get; set; }
@ -40,7 +41,7 @@ public class TorrentDto
public DateTimeOffset? RdEnded { get; set; } public DateTimeOffset? RdEnded { get; set; }
public Int64? RdSpeed { get; set; } public Int64? RdSpeed { get; set; }
public Int64? RdSeeders { get; set; } public Int64? RdSeeders { get; set; }
public IList<TorrentClientFile> Files { get; set; } = []; public IList<DebridClientFile> Files { get; set; } = [];
public IList<DownloadDto> Downloads { get; set; } = []; public IList<DownloadDto> Downloads { get; set; } = [];
} }

View file

@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdCategory
{
[JsonPropertyName("name")]
public String Name { get; set; } = "default";
[JsonPropertyName("order")]
public Int32 Order { get; set; } = 0;
[JsonPropertyName("dir")]
public String Dir { get; set; } = "";
[JsonPropertyName("newzbin")]
public String Newzbin { get; set; } = "";
[JsonPropertyName("priority")]
public Int32 Priority { get; set; } = -100;
[JsonPropertyName("script")]
public String Script { get; set; } = "None";
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdConfig
{
[JsonPropertyName("misc")]
public SabnzbdMisc Misc { get; set; } = new();
[JsonPropertyName("categories")]
public List<SabnzbdCategory> Categories { get; set; } = new();
[JsonPropertyName("servers")]
public List<Object> Servers { get; set; } = new();
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdHistory
{
[JsonPropertyName("total_slots")]
public Int32 TotalSlots { get; set; }
[JsonPropertyName("noofslots")]
public Int32 NoOfSlots { get; set; }
[JsonPropertyName("slots")]
public List<SabnzbdHistorySlot> Slots { get; set; } = new();
}

View file

@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdHistorySlot
{
[JsonPropertyName("nzo_id")]
public String NzoId { get; set; } = "";
[JsonPropertyName("name")]
public String Name { get; set; } = "";
[JsonPropertyName("size")]
public String Size { get; set; } = "0 B";
[JsonPropertyName("status")]
public String Status { get; set; } = "Completed";
[JsonPropertyName("category")]
public String Category { get; set; } = "Default";
[JsonPropertyName("storage")]
public String Path { get; set; } = "";
}

View file

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdMisc
{
[JsonPropertyName("complete_dir")]
public String CompleteDir { get; set; } = "";
[JsonPropertyName("download_dir")]
public String DownloadDir { get; set; } = "";
[JsonPropertyName("port")]
public String Port { get; set; } = "6500";
[JsonPropertyName("version")]
public String Version { get; set; } = "4.4.0";
}

View file

@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdQueue
{
[JsonPropertyName("version")]
public String Version { get; set; } = "4.4.0";
[JsonPropertyName("status")]
public String Status { get; set; } = "Idle";
[JsonPropertyName("speed")]
public String Speed { get; set; } = "0 ";
[JsonPropertyName("size")]
public String Size { get; set; } = "0 B";
[JsonPropertyName("sizeleft")]
public String SizeLeft { get; set; } = "0 B";
[JsonPropertyName("noofslots")]
public Int32 NoOfSlots { get; set; }
[JsonPropertyName("slots")]
public List<SabnzbdQueueSlot> Slots { get; set; } = new();
}

View file

@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdQueueSlot
{
[JsonPropertyName("index")]
public Int32 Index { get; set; }
[JsonPropertyName("nzo_id")]
public String NzoId { get; set; } = "";
[JsonPropertyName("filename")]
public String Filename { get; set; } = "";
[JsonPropertyName("size")]
public String Size { get; set; } = "0 B";
[JsonPropertyName("sizeleft")]
public String SizeLeft { get; set; } = "0 B";
[JsonPropertyName("percentage")]
public String Percentage { get; set; } = "0";
[JsonPropertyName("status")]
public String Status { get; set; } = "Downloading";
[JsonPropertyName("cat")]
public String Category { get; set; } = "Default";
[JsonPropertyName("timeleft")]
public String TimeLeft { get; set; } = "0:00:00";
[JsonPropertyName("priority")]
public String Priority { get; set; } = "Normal";
}

View file

@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace RdtClient.Data.Models.Sabnzbd;
public class SabnzbdResponse
{
[JsonPropertyName("queue")]
public SabnzbdQueue? Queue { get; set; }
[JsonPropertyName("history")]
public SabnzbdHistory? History { get; set; }
[JsonPropertyName("version")]
public String? Version { get; set; }
[JsonPropertyName("status")]
public Boolean? Status { get; set; }
[JsonPropertyName("error")]
public String? Error { get; set; }
[JsonPropertyName("nzo_ids")]
public List<String>? NzoIds { get; set; }
[JsonPropertyName("config")]
public SabnzbdConfig? Config { get; set; }
[JsonPropertyName("categories")]
public List<String>? Categories { get; set; }
}

View file

@ -0,0 +1,213 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Data;
using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Services;
using System.Reflection;
namespace RdtClient.Service.Test.BackgroundServices;
public class WatchFolderCheckerTests : IDisposable
{
private readonly Mock<ILogger<WatchFolderChecker>> _loggerMock;
private readonly Mock<IServiceProvider> _serviceProviderMock;
private readonly Mock<IServiceScope> _serviceScopeMock;
private readonly Mock<IServiceProvider> _scopeServiceProviderMock;
private readonly Mock<Torrents> _torrentsServiceMock;
private readonly String _testPath;
public WatchFolderCheckerTests()
{
_loggerMock = new Mock<ILogger<WatchFolderChecker>>();
_serviceProviderMock = new Mock<IServiceProvider>();
_serviceScopeMock = new Mock<IServiceScope>();
_scopeServiceProviderMock = new Mock<IServiceProvider>();
_torrentsServiceMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
_serviceProviderMock
.Setup(x => x.GetService(typeof(IServiceScopeFactory)))
.Returns(new MockScopeFactory(_serviceScopeMock.Object));
_serviceScopeMock
.Setup(x => x.ServiceProvider)
.Returns(_scopeServiceProviderMock.Object);
_scopeServiceProviderMock
.Setup(x => x.GetService(typeof(Torrents)))
.Returns(_torrentsServiceMock.Object);
_testPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(_testPath);
// Reset Settings and Startup
SetStartupReady(true);
ResetSettings();
Settings.Get.Watch.Path = _testPath;
}
public void Dispose()
{
if (Directory.Exists(_testPath))
{
Directory.Delete(_testPath, true);
}
}
private static void SetStartupReady(Boolean ready)
{
var property = typeof(Startup).GetProperty("Ready", BindingFlags.Public | BindingFlags.Static);
property?.SetValue(null, ready);
}
private static void ResetSettings()
{
var settings = new Data.Models.Internal.DbSettings
{
Watch = new Data.Models.Internal.DbSettingsWatch
{
Interval = 0,
Default = new Data.Models.Internal.DbSettingsDefaultsWithCategory()
},
DownloadClient = new Data.Models.Internal.DbSettingsDownloadClient()
};
var property = typeof(SettingData).GetProperty("Get", BindingFlags.Public | BindingFlags.Static);
property?.SetValue(null, settings);
}
private static void ResetPrevCheck(WatchFolderChecker checker)
{
var field = typeof(WatchFolderChecker).GetField("_prevCheck", BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(checker, DateTime.MinValue);
}
[Fact]
public async Task ExecuteAsync_ShouldProcessTorrentFile()
{
// Arrange
var filePath = Path.Combine(_testPath, "test.torrent");
var content = "torrent content"u8.ToArray();
await File.WriteAllBytesAsync(filePath, content);
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
ResetPrevCheck(checker);
var cts = new CancellationTokenSource();
_torrentsServiceMock
.Setup(x => x.AddFileToDebridQueue(It.IsAny<Byte[]>(), It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
// Act
var task = checker.StartAsync(cts.Token);
await Task.Delay(500); // Give it some time to process
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
// Assert
_torrentsServiceMock.Verify(x => x.AddFileToDebridQueue(
It.Is<Byte[]>(b => b.SequenceEqual(content)),
It.IsAny<Torrent>()), Times.AtLeastOnce);
Assert.False(File.Exists(filePath));
Assert.True(File.Exists(Path.Combine(_testPath, "processed", "test.torrent")));
}
[Fact]
public async Task ExecuteAsync_ShouldProcessMagnetFile()
{
// Arrange
var filePath = Path.Combine(_testPath, "test.magnet");
var content = "magnet:?xt=urn:btih:123";
await File.WriteAllTextAsync(filePath, content);
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
ResetPrevCheck(checker);
var cts = new CancellationTokenSource();
_torrentsServiceMock
.Setup(x => x.AddMagnetToDebridQueue(It.IsAny<String>(), It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
// Act
var task = checker.StartAsync(cts.Token);
await Task.Delay(500);
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
// Assert
_torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(
content,
It.IsAny<Torrent>()), Times.AtLeastOnce);
Assert.False(File.Exists(filePath));
Assert.True(File.Exists(Path.Combine(_testPath, "processed", "test.magnet")));
}
[Fact]
public async Task ExecuteAsync_ShouldProcessNzbFile()
{
// Arrange
var filePath = Path.Combine(_testPath, "test.nzb");
var content = "nzb content"u8.ToArray();
await File.WriteAllBytesAsync(filePath, content);
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
ResetPrevCheck(checker);
var cts = new CancellationTokenSource();
_torrentsServiceMock
.Setup(x => x.AddNzbFileToDebridQueue(It.IsAny<Byte[]>(), It.IsAny<String>(), It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
// Act
var task = checker.StartAsync(cts.Token);
await Task.Delay(500);
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
// Assert
_torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(
It.Is<Byte[]>(b => b.SequenceEqual(content)),
"test.nzb",
It.IsAny<Torrent>()), Times.AtLeastOnce);
Assert.False(File.Exists(filePath));
Assert.True(File.Exists(Path.Combine(_testPath, "processed", "test.nzb")));
}
[Fact]
public async Task ExecuteAsync_ShouldIgnoreOtherFiles()
{
// Arrange
var filePath = Path.Combine(_testPath, "test.txt");
await File.WriteAllTextAsync(filePath, "ignore me");
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
ResetPrevCheck(checker);
var cts = new CancellationTokenSource();
// Act
var task = checker.StartAsync(cts.Token);
await Task.Delay(500);
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
// Assert
_torrentsServiceMock.Verify(x => x.AddFileToDebridQueue(It.IsAny<Byte[]>(), It.IsAny<Torrent>()), Times.Never);
_torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(It.IsAny<String>(), It.IsAny<Torrent>()), Times.Never);
_torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(It.IsAny<Byte[]>(), It.IsAny<String>(), It.IsAny<Torrent>()), Times.Never);
Assert.True(File.Exists(filePath));
}
private class MockScopeFactory(IServiceScope scope) : IServiceScopeFactory
{
public IServiceScope CreateScope() => scope;
}
}

View file

@ -1,7 +1,7 @@
using System.IO.Abstractions.TestingHelpers; using System.IO.Abstractions.TestingHelpers;
using System.Text.Json; using System.Text.Json;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
namespace RdtClient.Service.Test.Helpers; namespace RdtClient.Service.Test.Helpers;
@ -183,7 +183,7 @@ public class DownloadHelperTest
var fileRelativePath = "inside/lots/of/subdirectories/file.txt"; var fileRelativePath = "inside/lots/of/subdirectories/file.txt";
IList<TorrentClientFile> files = IList<DebridClientFile> files =
[ [
new() new()
{ {
@ -219,7 +219,7 @@ public class DownloadHelperTest
var fileRelativePath = "inside/lots/of/subdirectories/file.txt"; var fileRelativePath = "inside/lots/of/subdirectories/file.txt";
IList<TorrentClientFile> files = IList<DebridClientFile> files =
[ [
new() new()
{ {

View file

@ -0,0 +1,101 @@
using RdtClient.Service.Helpers;
namespace RdtClient.Service.Test.Helpers;
public class FileHelperTest
{
[Fact]
public void RemoveInvalidFileNameChars_RemovesInvalidChars()
{
// Arrange
var invalidChars = Path.GetInvalidFileNameChars();
var input = "test" + new String(invalidChars) + "file.txt";
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
Assert.Equal("testfile.txt", result);
}
[Fact]
public void RemoveInvalidFileNameChars_RemovesDirectorySeparators()
{
// Arrange
var input = "folder/subfolder\\file.txt";
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
Assert.Equal("foldersubfolderfile.txt", result);
}
[Fact]
public void RemoveInvalidFileNameChars_RemovesDoubleDots()
{
// Arrange
var input = "test..file.txt";
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
Assert.Equal("testfile.txt", result);
}
[Fact]
public void RemoveInvalidFileNameChars_TrimsLeadingSeparators()
{
// Arrange
var input = "/test/file.txt";
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
// Note: The method first splits by invalid chars (including /),
// so "/test/file.txt" becomes "testfile.txt" through String.Concat(Split).
// Then it trims leading separators.
Assert.Equal("testfile.txt", result);
}
[Fact]
public void RemoveInvalidFileNameChars_HandlesValidFileName()
{
// Arrange
var input = "valid_file-123.txt";
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
Assert.Equal("valid_file-123.txt", result);
}
[Fact]
public void RemoveInvalidFileNameChars_HandlesEmptyString()
{
// Arrange
var input = "";
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
Assert.Equal("", result);
}
[Theory]
[InlineData("test/../file.txt", "testfile.txt")]
[InlineData(".../test.txt", ".test.txt")]
[InlineData("test....txt", "testtxt")]
public void RemoveInvalidFileNameChars_ComplexCases(String input, String expected)
{
// Act
var result = FileHelper.RemoveInvalidFileNameChars(input);
// Assert
Assert.Equal(expected, result);
}
}

View file

@ -20,6 +20,7 @@
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />
<PackageReference Include="TorBox.NET" Version="1.6.0" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -35,4 +36,10 @@
<ProjectReference Include="..\RdtClient.Service\RdtClient.Service.csproj" /> <ProjectReference Include="..\RdtClient.Service\RdtClient.Service.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="TorBoxNET">
<HintPath>..\..\..\torbox-net\TorBoxNET\bin\Release\netstandard2.0\TorBoxNET.dll</HintPath>
</Reference>
</ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,135 @@
using System.IO.Abstractions.TestingHelpers;
using System.Text;
using Moq;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using TorrentsService = RdtClient.Service.Services.Torrents;
namespace RdtClient.Service.Test.Services;
public class NzbTorrentsTest
{
private readonly Mocks _mocks;
private readonly MockFileSystem _fileSystem;
private readonly TorrentsService _torrents;
public NzbTorrentsTest()
{
_mocks = new Mocks();
_fileSystem = new MockFileSystem();
_torrents = new TorrentsService(
_mocks.TorrentsLoggerMock.Object,
_mocks.TorrentDataMock.Object,
_mocks.DownloadsMock.Object,
_mocks.ProcessFactoryMock.Object,
_fileSystem,
_mocks.EnricherMock.Object,
null!,
null!,
null!,
null!,
null!
);
}
[Fact]
public async Task AddNzbLinkToDebridQueue_ValidLink_AddsToQueue()
{
// Arrange
var nzbLink = "http://example.com/test.nzb";
var torrent = new Torrent
{
DownloadClient = DownloadClient.Bezzad
};
_mocks.TorrentDataMock.Setup(t => t.GetByHash(It.IsAny<String>())).ReturnsAsync((Torrent)null!);
_mocks.TorrentDataMock.Setup(t => t.Add(
null,
It.IsAny<String>(),
nzbLink,
false,
DownloadType.Nzb,
torrent.DownloadClient,
It.IsAny<Torrent>()
)).ReturnsAsync(new Torrent { Hash = "mockHash", RdName = "test.nzb" });
// Act
var result = await _torrents.AddNzbLinkToDebridQueue(nzbLink, torrent);
// Assert
Assert.NotNull(result);
Assert.Equal("test.nzb", torrent.RdName);
Assert.Equal(TorrentStatus.Queued, torrent.RdStatus);
_mocks.TorrentDataMock.Verify(t => t.Add(
null,
It.IsAny<String>(),
nzbLink,
false,
DownloadType.Nzb,
torrent.DownloadClient,
torrent
), Times.Once);
}
[Fact]
public async Task AddNzbLinkToDebridQueue_InvalidLink_ThrowsException()
{
// Arrange
var nzbLink = "invalid-link";
var torrent = new Torrent();
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _torrents.AddNzbLinkToDebridQueue(nzbLink, torrent));
}
[Fact]
public async Task AddNzbFileToDebridQueue_ValidFile_AddsToQueue()
{
// Arrange
var nzbContent = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var bytes = Encoding.UTF8.GetBytes(nzbContent);
var torrent = new Torrent
{
DownloadClient = DownloadClient.Bezzad
};
_mocks.TorrentDataMock.Setup(t => t.GetByHash(It.IsAny<String>())).ReturnsAsync((Torrent)null!);
_mocks.TorrentDataMock.Setup(t => t.Add(
null,
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
torrent.DownloadClient,
It.IsAny<Torrent>()
)).ReturnsAsync(new Torrent { Hash = "mockHash", RdName = "Test NZB Title" });
// Act
var result = await _torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent);
// Assert
Assert.NotNull(result);
Assert.Equal("Test NZB Title", torrent.RdName);
Assert.Equal(TorrentStatus.Queued, torrent.RdStatus);
_mocks.TorrentDataMock.Verify(t => t.Add(
null,
It.IsAny<String>(),
Convert.ToBase64String(bytes),
true,
DownloadType.Nzb,
torrent.DownloadClient,
torrent
), Times.Once);
}
[Fact]
public async Task AddNzbFileToDebridQueue_InvalidXml_ThrowsException()
{
// Arrange
var bytes = Encoding.UTF8.GetBytes("not xml");
var torrent = new Torrent();
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent));
}
}

View file

@ -0,0 +1,57 @@
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Services;
namespace RdtClient.Service.Test.Services;
public class QBittorrentTest
{
private readonly Mock<ILogger<QBittorrent>> _loggerMock;
private readonly Mock<Torrents> _torrentsMock;
private readonly Mock<Authentication> _authenticationMock;
private readonly QBittorrent _qBittorrent;
public QBittorrentTest()
{
_loggerMock = new Mock<ILogger<QBittorrent>>();
_torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
_authenticationMock = new Mock<Authentication>(null!, null!, null!);
_qBittorrent = new QBittorrent(_loggerMock.Object, null!, _authenticationMock.Object, _torrentsMock.Object, null!);
}
[Fact]
public async Task TorrentInfo_ShouldOnlyReturnTorrents()
{
// Arrange
var allTorrents = new List<Torrent>
{
new()
{
TorrentId = Guid.NewGuid(),
Hash = "hash1",
RdName = "Torrent 1",
Type = DownloadType.Torrent
},
new()
{
TorrentId = Guid.NewGuid(),
Hash = "hash2",
RdName = "NZB 1",
Type = DownloadType.Nzb
}
};
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(allTorrents);
// Act
var result = await _qBittorrent.TorrentInfo();
// Assert
Assert.Single(result);
Assert.Equal("hash1", result[0].Hash);
Assert.Equal("Torrent 1", result[0].Name);
}
}

View file

@ -0,0 +1,399 @@
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services;
namespace RdtClient.Service.Test.Services;
public class SabnzbdTest
{
private readonly Mock<ILogger<Sabnzbd>> _loggerMock = new();
private readonly Mock<Torrents> _torrentsMock;
private readonly AppSettings _appSettings = new() { Port = 6500 };
public SabnzbdTest()
{
_torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(new List<Torrent>());
}
[Fact]
public async Task GetQueue_ShouldReturnCorrectStructure()
{
// Arrange
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "Name 1",
RdProgress = 50,
Type = DownloadType.Nzb,
Downloads = new List<Download>
{
new()
{ BytesTotal = 1000, BytesDone = 500 }
}
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetQueue();
// Assert
Assert.NotNull(result);
Assert.Single(result.Slots);
Assert.Equal("hash1", result.Slots[0].NzoId);
Assert.Equal("Name 1", result.Slots[0].Filename);
// (50% debrid + 50% download) / 2 = 50%
Assert.Equal("50", result.Slots[0].Percentage);
}
[Fact]
public async Task GetQueue_ShouldCalculatePercentageCorrectly_WhenDifferentProgress()
{
// Arrange
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "Name 1",
RdProgress = 100,
Type = DownloadType.Nzb,
Downloads = new List<Download>
{
new()
{ BytesTotal = 1000, BytesDone = 0 }
}
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetQueue();
// Assert
// (100% debrid + 0% download) / 2 = 50%
Assert.Equal("50", result.Slots[0].Percentage);
}
[Fact]
public async Task GetQueue_ShouldCalculateTimeLeftCorrectly()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var added = now.AddMinutes(-10);
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "Name 1",
Added = added,
RdProgress = 100, // 100% debrid
Type = DownloadType.Nzb,
Downloads = new List<Download>
{
new()
{
BytesTotal = 1000,
BytesDone = 0 // 0% download
}
}
}
};
// Overall progress = (1.0 + 0.0) / 2 = 0.5
// Elapsed = 10 minutes
// Total estimated = 10 / 0.5 = 20 minutes
// Time left = 20 - 10 = 10 minutes
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetQueue();
// Assert
// Allow for some small variation in time calculation during test execution
var timeLeftParts = result.Slots[0].TimeLeft.Split(':');
var hours = Int32.Parse(timeLeftParts[0]);
var minutes = Int32.Parse(timeLeftParts[1]);
Assert.Equal(0, hours);
Assert.InRange(minutes, 9, 11);
}
[Fact]
public async Task GetQueue_ShouldCalculateTimeLeftCorrectly_WithRetry()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var added = now.AddMinutes(-20);
var retry = now.AddMinutes(-10);
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "Name 1",
Added = added,
Retry = retry,
RdProgress = 100, // 100% debrid
Type = DownloadType.Nzb,
Downloads = new List<Download>
{
new()
{
BytesTotal = 1000,
BytesDone = 0 // 0% download
}
}
}
};
// Later of Added and Retry is Retry (-10 mins)
// Overall progress = (1.0 + 0.0) / 2 = 0.5
// Elapsed = 10 minutes
// Total estimated = 10 / 0.5 = 20 minutes
// Time left = 20 - 10 = 10 minutes
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetQueue();
// Assert
var timeLeftParts = result.Slots[0].TimeLeft.Split(':');
var hours = Int32.Parse(timeLeftParts[0]);
var minutes = Int32.Parse(timeLeftParts[1]);
Assert.Equal(0, hours);
Assert.InRange(minutes, 9, 11);
}
[Fact]
public async Task GetQueue_ShouldOnlyReturnNzbs()
{
// Arrange
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "NZB 1",
Type = DownloadType.Nzb,
Downloads = new List<Download>()
},
new()
{
Hash = "hash2",
RdName = "Torrent 1",
Type = DownloadType.Torrent,
Downloads = new List<Download>()
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetQueue();
// Assert
Assert.Single(result.Slots);
Assert.Equal("hash1", result.Slots[0].NzoId);
}
[Fact]
public async Task GetHistory_ShouldOnlyReturnNzbs()
{
// Arrange
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "NZB 1",
Type = DownloadType.Nzb,
Completed = DateTimeOffset.UtcNow,
Downloads = new List<Download>()
},
new()
{
Hash = "hash2",
RdName = "Torrent 1",
Type = DownloadType.Torrent,
Completed = DateTimeOffset.UtcNow,
Downloads = new List<Download>()
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetHistory();
// Assert
Assert.Single(result.Slots);
Assert.Equal("hash1", result.Slots[0].NzoId);
}
[Fact]
public async Task GetHistory_ShouldReturnFullPath()
{
// Arrange
var savePath = @"C:\Downloads";
Data.Data.SettingData.Get.DownloadClient.MappedPath = savePath;
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "NZB 1",
Category = "radarr",
Type = DownloadType.Nzb,
Completed = DateTimeOffset.UtcNow,
Downloads = new List<Download>()
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetHistory();
// Assert
Assert.Single(result.Slots);
var expectedPath = Path.Combine(savePath, "radarr", "NZB 1");
Assert.Equal(expectedPath, result.Slots[0].Path);
}
[Fact]
public async Task GetHistory_ShouldReturnFullPath_NoCategory()
{
// Arrange
var savePath = @"C:\Downloads";
Data.Data.SettingData.Get.DownloadClient.MappedPath = savePath;
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "NZB 1",
Category = null,
Type = DownloadType.Nzb,
Completed = DateTimeOffset.UtcNow,
Downloads = new List<Download>()
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetHistory();
// Assert
Assert.Single(result.Slots);
var expectedPath = Path.Combine(savePath, "NZB 1");
Assert.Equal(expectedPath, result.Slots[0].Path);
}
[Fact]
public async Task GetHistory_ShouldReturnFailedStatus_WhenTorrentHasError()
{
// Arrange
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
RdName = "NZB 1",
Type = DownloadType.Nzb,
Completed = DateTimeOffset.UtcNow,
Error = "Some error occurred",
Downloads = new List<Download>()
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = await sabnzbd.GetHistory();
// Assert
Assert.Single(result.Slots);
Assert.Equal("Failed", result.Slots[0].Status);
}
[Fact]
public void GetConfig_ShouldReturnCorrectConfig()
{
// Arrange
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = sabnzbd.GetConfig();
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Misc);
Assert.Equal("6500", result.Misc.Port);
Assert.NotEmpty(result.Categories);
Assert.Contains(result.Categories, c => c.Name == "*");
}
[Fact]
public void GetCategories_ShouldOnlyReturnSettingsCategories()
{
// Arrange
var torrentList = new List<Torrent>
{
new()
{
Hash = "hash1",
Category = "Movie",
Type = DownloadType.Nzb,
Downloads = new List<Download>()
}
};
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
Data.Data.SettingData.Get.General.Categories = "TV, Music, *";
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
// Act
var result = sabnzbd.GetCategories();
// Assert
Assert.Equal("*", result[0]);
Assert.Contains("TV", result);
Assert.Contains("Music", result);
Assert.DoesNotContain("Movie", result);
Assert.Equal(3, result.Count);
}
}

View file

@ -5,12 +5,12 @@ using Newtonsoft.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.DebridClients;
using File = AllDebridNET.File; using File = AllDebridNET.File;
namespace RdtClient.Service.Test.Services.TorrentClients; namespace RdtClient.Service.Test.Services.TorrentClients;
public class AllDebridTorrentClientTest public class AllDebridDebridClientTest
{ {
private static readonly Magnet Magnet1HalfDownloaded = new() private static readonly Magnet Magnet1HalfDownloaded = new()
{ {
@ -84,10 +84,10 @@ public class AllDebridTorrentClientTest
Fullsync = true Fullsync = true
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.GetTorrents(); var result = await allDebridTorrentClient.GetDownloads();
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
@ -114,10 +114,10 @@ public class AllDebridTorrentClientTest
Fullsync = false Fullsync = false
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.GetTorrents(); var result = await allDebridTorrentClient.GetDownloads();
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
@ -144,15 +144,15 @@ public class AllDebridTorrentClientTest
Fullsync = true Fullsync = true
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
// `GetTorrents` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`, // `GetDownloads` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`,
// so when the second call `_cache.Add`s, it also affects `firstResult`. // so when the second call `_cache.Add`s, it also affects `firstResult`.
// `.ToList()` clones it so it won't be changed by the second call // `.ToList()` clones it so it won't be changed by the second call
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList(); var firstResult = (await allDebridTorrentClient.GetDownloads()).ToList();
var secondResult = await allDebridTorrentClient.GetTorrents(); var secondResult = await allDebridTorrentClient.GetDownloads();
// Assert // Assert
Assert.Single(firstResult); Assert.Single(firstResult);
@ -180,15 +180,15 @@ public class AllDebridTorrentClientTest
Fullsync = false Fullsync = false
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
// `GetTorrents` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`, // `GetDownloads` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`,
// so when the second call `_cache.Add`s, it also affects `firstResult`. // so when the second call `_cache.Add`s, it also affects `firstResult`.
// `.ToList()` clones it so it won't be changed by the second call // `.ToList()` clones it so it won't be changed by the second call
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList(); var firstResult = (await allDebridTorrentClient.GetDownloads()).ToList();
var secondResult = await allDebridTorrentClient.GetTorrents(); var secondResult = await allDebridTorrentClient.GetDownloads();
// Assert // Assert
Assert.Single(firstResult); Assert.Single(firstResult);
@ -222,15 +222,15 @@ public class AllDebridTorrentClientTest
Fullsync = false Fullsync = false
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
// `GetTorrents` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`, // `GetDownloads` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`,
// so when the second call `_cache.Add`s, it also affects `firstResult`. // so when the second call `_cache.Add`s, it also affects `firstResult`.
// `.ToList()` clones it so it won't be changed by the second call // `.ToList()` clones it so it won't be changed by the second call
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList(); var firstResult = (await allDebridTorrentClient.GetDownloads()).ToList();
var secondResult = await allDebridTorrentClient.GetTorrents(); var secondResult = await allDebridTorrentClient.GetDownloads();
// Assert // Assert
Assert.Equal(2, firstResult.Count); Assert.Equal(2, firstResult.Count);
@ -269,12 +269,12 @@ public class AllDebridTorrentClientTest
Fullsync = true Fullsync = true
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
// We have to use `GetTorrents` since `Map` is private // We have to use `GetDownloads` since `Map` is private
var result = await allDebridTorrentClient.GetTorrents(); var result = await allDebridTorrentClient.GetDownloads();
// Assert // Assert
Assert.Equal(expectedProgress, result.First().Progress); Assert.Equal(expectedProgress, result.First().Progress);
@ -294,7 +294,7 @@ public class AllDebridTorrentClientTest
}; };
var serializedOriginal = JsonConvert.SerializeObject(torrent); var serializedOriginal = JsonConvert.SerializeObject(torrent);
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.UpdateData(torrent, null); var result = await allDebridTorrentClient.UpdateData(torrent, null);
@ -320,7 +320,7 @@ public class AllDebridTorrentClientTest
}; };
mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>())).ReturnsAsync(Magnet1Finished); mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>())).ReturnsAsync(Magnet1Finished);
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.UpdateData(torrent, null); var result = await allDebridTorrentClient.UpdateData(torrent, null);
@ -353,7 +353,7 @@ public class AllDebridTorrentClientTest
mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>())) mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>()))
.ThrowsAsync(new AllDebridException("Magnet not found", "MAGNET_INVALID_ID")); .ThrowsAsync(new AllDebridException("Magnet not found", "MAGNET_INVALID_ID"));
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.UpdateData(torrent, null); var result = await allDebridTorrentClient.UpdateData(torrent, null);
@ -388,8 +388,8 @@ public class AllDebridTorrentClientTest
Fullsync = true Fullsync = true
}); });
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
var torrentClientTorrent = (await allDebridTorrentClient.GetTorrents()).First(); var torrentClientTorrent = (await allDebridTorrentClient.GetDownloads()).First();
// Act // Act
var result = await allDebridTorrentClient.UpdateData(torrent, torrentClientTorrent); var result = await allDebridTorrentClient.UpdateData(torrent, torrentClientTorrent);
@ -415,7 +415,7 @@ public class AllDebridTorrentClientTest
RdId = null RdId = null
}; };
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.GetDownloadInfos(torrent); var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
@ -464,7 +464,7 @@ public class AllDebridTorrentClientTest
mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, "file1.txt", 123)).Returns(true); mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, "file1.txt", 123)).Returns(true);
mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, "folder/file2.txt", 180)).Returns(false); mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, "folder/file2.txt", 180)).Returns(false);
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.GetDownloadInfos(torrent); var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
@ -505,7 +505,7 @@ public class AllDebridTorrentClientTest
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(files); mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(files);
mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(false); mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(false);
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act // Act
var result = await allDebridTorrentClient.GetDownloadInfos(torrent); var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
@ -522,8 +522,8 @@ public class AllDebridTorrentClientTest
{ {
public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock; public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock;
public readonly Mock<IAllDebridNETClient> AllDebridClientMock; public readonly Mock<IAllDebridNETClient> AllDebridClientMock;
public readonly Mock<ILogger<AllDebridDebridClient>> LoggerMock;
public readonly Mock<IDownloadableFileFilter> FileFilterMock; public readonly Mock<IDownloadableFileFilter> FileFilterMock;
public readonly Mock<ILogger<AllDebridTorrentClient>> LoggerMock;
public Mocks() public Mocks()
{ {

View file

@ -0,0 +1,481 @@
using Microsoft.Extensions.Logging;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.Services;
using RdtClient.Service.Services.DebridClients;
using TorBoxNET;
namespace RdtClient.Service.Test.Services.TorrentClients;
public class TorBoxDebridClientTest
{
private readonly Mock<ILogger<TorBoxDebridClient>> _loggerMock;
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<IDownloadableFileFilter> _fileFilterMock;
public TorBoxDebridClientTest()
{
_loggerMock = new Mock<ILogger<TorBoxDebridClient>>();
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
_fileFilterMock = new Mock<IDownloadableFileFilter>();
var httpClient = new HttpClient();
_httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny<String>())).Returns(httpClient);
Settings.Get.Provider.ApiKey = "test-api-key";
Settings.Get.Provider.Timeout = 100;
}
[Fact]
public async Task GetDownloads_ReturnsTorrentsAndNzbsWithCorrectType()
{
// Arrange
var torrents = new List<TorrentInfoResult>
{
new() { Hash = "hash1", Name = "torrent1", Size = 1000, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
};
var nzbs = new List<UsenetInfoResult>
{
new() { Hash = "hash2", Name = "nzb1", Size = 2000, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
};
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
clientMock.Protected().Setup<Task<IEnumerable<TorrentInfoResult>?>>("GetCurrentTorrents").ReturnsAsync(torrents);
clientMock.Protected().Setup<Task<IEnumerable<TorrentInfoResult>?>>("GetQueuedTorrents").ReturnsAsync(new List<TorrentInfoResult>());
clientMock.Protected().Setup<Task<IEnumerable<UsenetInfoResult>?>>("GetCurrentUsenet").ReturnsAsync(nzbs);
clientMock.Protected().Setup<Task<IEnumerable<UsenetInfoResult>?>>("GetQueuedUsenet").ReturnsAsync(new List<UsenetInfoResult>());
// Act
var result = await clientMock.Object.GetDownloads();
// Assert
Assert.Equal(2, result.Count);
var torrentResult = result.FirstOrDefault(r => r.Id == "hash1");
Assert.NotNull(torrentResult);
Assert.Equal(DownloadType.Torrent, torrentResult.Type);
var nzbResult = result.FirstOrDefault(r => r.Id == "hash2");
Assert.NotNull(nzbResult);
Assert.Equal(DownloadType.Nzb, nzbResult.Type);
}
[Fact]
public async Task GetAvailableFiles_ReturnsTorrentFiles_WhenTorrentFound()
{
// Arrange
var hash = "test-hash";
var availability = new Response<List<AvailableTorrent?>>
{
Data = new List<AvailableTorrent?>
{
new()
{
Files = new List<AvailableTorrentFile>
{
new() { Name = "file1.mkv", Size = 100 },
new() { Name = "file2.txt", Size = 10 }
}
}
}
};
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(availability);
// Act
var result = await clientMock.Object.GetAvailableFiles(hash);
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("file1.mkv", result[0].Filename);
Assert.Equal(100, result[0].Filesize);
Assert.Equal("file2.txt", result[1].Filename);
Assert.Equal(10, result[1].Filesize);
}
[Fact]
public async Task GetAvailableFiles_ReturnsUsenetFiles_WhenTorrentNotFoundButUsenetFound()
{
// Arrange
var hash = "test-hash";
var torrentAvailability = new Response<List<AvailableTorrent?>> { Data = new List<AvailableTorrent?>() };
var usenetAvailability = new Response<List<AvailableUsenet?>>
{
Data = new List<AvailableUsenet?>
{
new()
{
Files = new List<AvailableUsenetFile>
{
new() { Name = "file1.nzb", Size = 200 }
}
}
}
};
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability);
clientMock.Protected().Setup<Task<Response<List<AvailableUsenet?>>>>("GetUsenetAvailability", hash).ReturnsAsync(usenetAvailability);
// Act
var result = await clientMock.Object.GetAvailableFiles(hash);
// Assert
Assert.Single(result);
Assert.Equal("file1.nzb", result[0].Filename);
Assert.Equal(200, result[0].Filesize);
}
[Fact]
public async Task GetAvailableFiles_ReturnsEmptyList_WhenNeitherFound()
{
// Arrange
var hash = "test-hash";
var torrentAvailability = new Response<List<AvailableTorrent?>> { Data = new List<AvailableTorrent?>() };
var usenetAvailability = new Response<List<AvailableUsenet?>> { Data = new List<AvailableUsenet?>() };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability);
clientMock.Protected().Setup<Task<Response<List<AvailableUsenet?>>>>("GetUsenetAvailability", hash).ReturnsAsync(usenetAvailability);
// Act
var result = await clientMock.Object.GetAvailableFiles(hash);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task Delete_CallsTorrentsControl_WhenTypeIsTorrent()
{
// Arrange
var torrent = new Torrent
{
RdId = "torrent-id",
Type = DownloadType.Torrent
};
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
// Act
await clientMock.Object.Delete(torrent);
// Assert
torrentsApiMock.Verify(m => m.ControlAsync("torrent-id", "delete", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Delete_CallsUsenetControl_WhenTypeIsNzb()
{
// Arrange
var torrent = new Torrent
{
RdId = "nzb-id",
Type = DownloadType.Nzb
};
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
// Act
await clientMock.Object.Delete(torrent);
// Assert
usenetApiMock.Verify(m => m.ControlAsync("nzb-id", "delete", false, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Unrestrict_CallsTorrentsRequestDownload_WhenTypeIsTorrent()
{
// Arrange
var torrent = new Torrent
{
RdId = "torrent-id",
Type = DownloadType.Torrent
};
var link = "https://torbox.app/d/123/456";
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://unrestricted-link" });
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
// Assert
Assert.Equal("https://unrestricted-link", result);
torrentsApiMock.Verify(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Unrestrict_CallsUsenetRequestDownload_WhenTypeIsNzb()
{
// Arrange
var torrent = new Torrent
{
RdId = "nzb-id",
Type = DownloadType.Nzb
};
var link = "https://torbox.app/d/123/456";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://unrestricted-link-nzb" });
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
// Assert
Assert.Equal("https://unrestricted-link-nzb", result);
usenetApiMock.Verify(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task AddNzbFile_CallsUsenetAddFileAsyncWithName()
{
// Arrange
var bytes = new Byte[] { 1, 2, 3 };
var name = "test-nzb";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.AddFileAsync(bytes, -1, name, null, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<UsenetAddResult> { Data = new UsenetAddResult { Hash = "new-hash" } });
// Act
var result = await clientMock.Object.AddNzbFile(bytes, name);
// Assert
Assert.Equal("new-hash", result);
usenetApiMock.Verify(m => m.AddFileAsync(bytes, -1, name, null, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetDownloadInfos_GeneratesCorrectFakedlLinks_ForIndividualFiles()
{
// Arrange
var files = new List<DebridClientFile>
{
new() { Id = 1, Path = "file1.mkv", Bytes = 1000 },
new() { Id = 2, Path = "file2.mkv", Bytes = 2000 }
};
var torrent = new Torrent
{
Hash = "test-hash",
RdFiles = JsonConvert.SerializeObject(files)
};
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult { Id = 12345 });
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
Settings.Get.Provider.PreferZippedDownloads = false;
// Act
var result = await clientMock.Object.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.Equal("https://torbox.app/fakedl/12345/1", result[0].RestrictedLink);
Assert.Equal("https://torbox.app/fakedl/12345/2", result[1].RestrictedLink);
}
[Fact]
public async Task GetDownloadInfos_GeneratesCorrectFakedlLink_ForZipDownload()
{
// Arrange
var files = new List<DebridClientFile>
{
new() { Id = 1, Path = "file1.mkv", Bytes = 1000 }
};
var torrent = new Torrent
{
Hash = "test-hash",
RdName = "TestTorrent",
RdFiles = JsonConvert.SerializeObject(files),
DownloadClient = Data.Enums.DownloadClient.Aria2c
};
Settings.Get.Provider.PreferZippedDownloads = true;
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult { Id = 12345 });
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
// Act
var result = await clientMock.Object.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("https://torbox.app/fakedl/12345/zip", result[0].RestrictedLink);
Assert.Equal("TestTorrent.zip", result[0].FileName);
}
[Fact]
public async Task Unrestrict_ParsesFakedlLinksCorrectly_ForIndividualFiles()
{
// Arrange
var torrent = new Torrent
{
Type = DownloadType.Torrent
};
var link = "https://torbox.app/fakedl/12345/6789";
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 6789, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://real-download-link" });
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
// Assert
Assert.Equal("https://real-download-link", result);
torrentsApiMock.Verify(m => m.RequestDownloadAsync(12345, 6789, false, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Unrestrict_ParsesFakedlLinksCorrectly_ForZipDownload()
{
// Arrange
var torrent = new Torrent
{
Type = DownloadType.Torrent
};
var link = "https://torbox.app/fakedl/12345/zip";
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://real-zip-download-link" });
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
// Assert
Assert.Equal("https://real-zip-download-link", result);
torrentsApiMock.Verify(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetDownloadInfos_GeneratesCorrectFakedlLinks_ForUsenet()
{
// Arrange
var files = new List<DebridClientFile>
{
new() { Id = 1, Path = "file1.nzb", Bytes = 1000 }
};
var torrent = new Torrent
{
Type = DownloadType.Nzb,
RdId = "nzb-hash",
RdFiles = JsonConvert.SerializeObject(files)
};
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.GetCurrentAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UsenetInfoResult> { new() { Hash = "nzb-hash", Id = 98765 } });
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
Settings.Get.Provider.PreferZippedDownloads = false;
// Act
var result = await clientMock.Object.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("https://torbox.app/fakedl/98765/1", result[0].RestrictedLink);
}
[Fact]
public async Task Unrestrict_ParsesFakedlLinksCorrectly_ForUsenet()
{
// Arrange
var torrent = new Torrent
{
Type = DownloadType.Nzb
};
var link = "https://torbox.app/fakedl/98765/4321";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://real-usenet-link" });
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
// Assert
Assert.Equal("https://real-usenet-link", result);
usenetApiMock.Verify(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny<CancellationToken>()), Times.Once);
}
}

View file

@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
using RdtClient.Data.Data; using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services; using RdtClient.Service.Services;
@ -316,4 +317,95 @@ public class TorrentsTest
Assert.Matches("error-line 2", exitedWithOutputMessage); Assert.Matches("error-line 2", exitedWithOutputMessage);
Assert.Matches("error-line 3", exitedWithOutputMessage); Assert.Matches("error-line 3", exitedWithOutputMessage);
} }
[Fact]
public async Task AddNzbFileToDebridQueue_ShouldSetDownloadTypeNzb()
{
// Arrange
var mocks = new Mocks();
var torrent = new Torrent
{
TorrentId = Guid.NewGuid()
};
var nzbContent = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var bytes = Encoding.UTF8.GetBytes(nzbContent);
mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny<String>(),
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
new MockFileSystem(),
mocks.EnricherMock.Object,
null!,
null!,
null!,
null!,
null!);
// Act
await torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent);
// Assert
mocks.TorrentDataMock.Verify(t => t.Add(null,
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<Torrent>()), Times.Once);
}
[Fact]
public async Task AddNzbLinkToDebridQueue_ShouldSetDownloadTypeNzb()
{
// Arrange
var mocks = new Mocks();
var torrent = new Torrent
{
TorrentId = Guid.NewGuid()
};
var link = "http://example.com/test.nzb";
mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny<String>(),
It.IsAny<String>(),
It.IsAny<String>(),
false,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
new MockFileSystem(),
mocks.EnricherMock.Object,
null!,
null!,
null!,
null!,
null!);
// Act
await torrents.AddNzbLinkToDebridQueue(link, torrent);
// Assert
mocks.TorrentDataMock.Verify(t => t.Add(null,
It.IsAny<String>(),
link,
false,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<Torrent>()), Times.Once);
}
} }

View file

@ -28,7 +28,13 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
{ {
try try
{ {
await Task.Delay(1000, stoppingToken); var nextCheck = _prevCheck.AddSeconds(Math.Max(Settings.Get.Watch.Interval, 10));
if (DateTime.Now < nextCheck)
{
var delay = nextCheck - DateTime.Now;
await Task.Delay(delay, stoppingToken);
}
_prevCheck = DateTime.Now;
if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path)) if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path))
{ {
@ -48,14 +54,6 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
errorStorePath = Settings.Get.Watch.ErrorPath; errorStorePath = Settings.Get.Watch.ErrorPath;
} }
var nextCheck = _prevCheck.AddSeconds(Settings.Get.Watch.Interval);
if (DateTime.UtcNow < nextCheck)
{
continue;
}
_prevCheck = DateTime.UtcNow;
var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.*", SearchOption.TopDirectoryOnly); var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.*", SearchOption.TopDirectoryOnly);
@ -63,7 +61,7 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
{ {
var fileInfo = new FileInfo(torrentFile); var fileInfo = new FileInfo(torrentFile);
if (fileInfo.Extension != ".magnet" && fileInfo.Extension != ".torrent") if (fileInfo.Extension != ".magnet" && fileInfo.Extension != ".torrent" && fileInfo.Extension != ".nzb")
{ {
continue; continue;
} }
@ -107,6 +105,11 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
var magnetLink = await File.ReadAllTextAsync(torrentFile, stoppingToken); var magnetLink = await File.ReadAllTextAsync(torrentFile, stoppingToken);
await torrentService.AddMagnetToDebridQueue(magnetLink, torrent); await torrentService.AddMagnetToDebridQueue(magnetLink, torrent);
} }
else if (fileInfo.Extension == ".nzb")
{
var nzbFileContents = await File.ReadAllBytesAsync(torrentFile, stoppingToken);
await torrentService.AddNzbFileToDebridQueue(nzbFileContents, fileInfo.Name, torrent);
}
if (!Directory.Exists(processedStorePath)) if (!Directory.Exists(processedStorePath))
{ {

View file

@ -8,7 +8,7 @@ using Polly.Extensions.Http;
using RdtClient.Service.BackgroundServices; using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Middleware; using RdtClient.Service.Middleware;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.DebridClients;
using RdtClient.Service.Wrappers; using RdtClient.Service.Wrappers;
namespace RdtClient.Service; namespace RdtClient.Service;
@ -23,7 +23,7 @@ public static class DiConfig
services.AddMemoryCache(); services.AddMemoryCache();
services.AddSingleton<IAllDebridNetClientFactory, AllDebridNetClientFactory>(); services.AddSingleton<IAllDebridNetClientFactory, AllDebridNetClientFactory>();
services.AddScoped<AllDebridTorrentClient>(); services.AddScoped<AllDebridDebridClient>();
services.AddSingleton<IProcessFactory, ProcessFactory>(); services.AddSingleton<IProcessFactory, ProcessFactory>();
services.AddSingleton<IFileSystem, FileSystem>(); services.AddSingleton<IFileSystem, FileSystem>();
@ -31,12 +31,13 @@ public static class DiConfig
services.AddScoped<Authentication>(); services.AddScoped<Authentication>();
services.AddScoped<IDownloads, Downloads>(); services.AddScoped<IDownloads, Downloads>();
services.AddScoped<Downloads>(); services.AddScoped<Downloads>();
services.AddScoped<PremiumizeTorrentClient>(); services.AddScoped<PremiumizeDebridClient>();
services.AddScoped<QBittorrent>(); services.AddScoped<QBittorrent>();
services.AddScoped<Sabnzbd>();
services.AddScoped<RemoteService>(); services.AddScoped<RemoteService>();
services.AddScoped<RealDebridTorrentClient>(); services.AddScoped<RealDebridDebridClient>();
services.AddScoped<Settings>(); services.AddScoped<Settings>();
services.AddScoped<TorBoxTorrentClient>(); services.AddScoped<TorBoxDebridClient>();
services.AddScoped<Torrents>(); services.AddScoped<Torrents>();
services.AddScoped<TorrentRunner>(); services.AddScoped<TorrentRunner>();
services.AddScoped<DebridLinkClient>(); services.AddScoped<DebridLinkClient>();
@ -46,6 +47,7 @@ public static class DiConfig
services.AddSingleton<IEnricher, Enricher>(); services.AddSingleton<IEnricher, Enricher>();
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>(); services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
services.AddScoped<IAuthorizationHandler, SabnzbdHandler>();
services.AddHostedService<DiskSpaceMonitor>(); services.AddHostedService<DiskSpaceMonitor>();
services.AddHostedService<ProviderUpdater>(); services.AddHostedService<ProviderUpdater>();

View file

@ -0,0 +1,66 @@
using Serilog.Core;
using Serilog.Events;
using RdtClient.Service.Services;
namespace RdtClient.Service.Helpers;
public class CredentialRedactorEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var sensitiveValues = new List<String>();
var apiKey = Settings.Get.Provider.ApiKey;
if (!String.IsNullOrWhiteSpace(apiKey) && apiKey.Length > 5)
{
sensitiveValues.Add(apiKey);
}
var aria2Secret = Settings.Get.DownloadClient.Aria2cSecret;
if (!String.IsNullOrWhiteSpace(aria2Secret) && aria2Secret.Length > 5)
{
sensitiveValues.Add(aria2Secret);
}
var dsPassword = Settings.Get.DownloadClient.DownloadStationPassword;
if (!String.IsNullOrWhiteSpace(dsPassword) && dsPassword.Length > 5)
{
sensitiveValues.Add(dsPassword);
}
if (sensitiveValues.Count == 0)
{
return;
}
foreach (var sensitiveValue in sensitiveValues)
{
// Redact in the message template
if (logEvent.MessageTemplate.Text.Contains(sensitiveValue))
{
var newText = logEvent.MessageTemplate.Text.Replace(sensitiveValue, "*****");
var field = typeof(MessageTemplate).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
.FirstOrDefault(f => f.FieldType == typeof(String));
field?.SetValue(logEvent.MessageTemplate, newText);
}
// Redact in properties
var propertiesToUpdate = new List<LogEventProperty>();
foreach (var property in logEvent.Properties)
{
if (property.Value is ScalarValue scalarValue && scalarValue.Value is String stringValue && stringValue.Contains(sensitiveValue))
{
var newValue = stringValue.Replace(sensitiveValue, "*****");
propertiesToUpdate.Add(new LogEventProperty(property.Key, new ScalarValue(newValue)));
}
}
foreach (var property in propertiesToUpdate)
{
logEvent.AddOrUpdateProperty(property);
}
}
}
}

View file

@ -78,7 +78,15 @@ public static class FileHelper
public static String RemoveInvalidFileNameChars(String filename) public static String RemoveInvalidFileNameChars(String filename)
{ {
return String.Concat(filename.Split(Path.GetInvalidFileNameChars())); var invalidChars = Path.GetInvalidFileNameChars()
.Concat(['/', '\\'])
.Distinct()
.ToArray();
var cleaned = String.Concat(filename.Split(invalidChars));
cleaned = cleaned.Replace("..", "");
cleaned = cleaned.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return cleaned;
} }
public static String GetDirectoryContents(String path) public static String GetDirectoryContents(String path)

View file

@ -0,0 +1,24 @@
namespace RdtClient.Service.Helpers;
public static class FileSizeHelper
{
public static String FormatSize(Int64? bytes)
{
if (bytes == null)
{
return "0 B";
}
String[] units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
var unitIndex = 0;
Double size = bytes.Value;
while (size >= 1024 && unitIndex < units.Length - 1)
{
size /= 1024;
unitIndex++;
}
return $"{size:0.##} {units[unitIndex]}";
}
}

View file

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using RdtClient.Data.Enums;
using RdtClient.Service.Services;
namespace RdtClient.Service.Middleware;
public class SabnzbdRequirement : IAuthorizationRequirement
{
}
public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor httpContextAccessor) : AuthorizationHandler<SabnzbdRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SabnzbdRequirement requirement)
{
var httpContext = httpContextAccessor.HttpContext;
if (context.User.Identity?.IsAuthenticated == true)
{
context.Succeed(requirement);
return;
}
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
{
context.Succeed(requirement);
return;
}
if (httpContext != null)
{
var request = httpContext.Request;
String? GetParam(String name)
{
var value = request.Query[name].ToString();
if (String.IsNullOrWhiteSpace(value) && request.HasFormContentType)
{
value = request.Form[name].ToString();
}
return value;
}
var maUsername = GetParam("ma_username");
var maPassword = GetParam("ma_password");
if (!String.IsNullOrWhiteSpace(maUsername) && !String.IsNullOrWhiteSpace(maPassword))
{
var loginResult = await authentication.Login(maUsername, maPassword);
if (loginResult.Succeeded)
{
context.Succeed(requirement);
}
}
}
}
}

View file

@ -25,7 +25,7 @@
<PackageReference Include="Synology.Api.Client" Version="0.3.93" /> <PackageReference Include="Synology.Api.Client" Version="0.3.93" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />
<PackageReference Include="TorBox.NET" Version="1.5.0" /> <PackageReference Include="TorBox.NET" Version="1.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -14,7 +14,7 @@ public class Authentication(SignInManager<IdentityUser> signInManager, UserManag
return result; return result;
} }
public async Task<SignInResult> Login(String userName, String password) public virtual async Task<SignInResult> Login(String userName, String password)
{ {
if (String.IsNullOrWhiteSpace(userName) || String.IsNullOrWhiteSpace(password)) if (String.IsNullOrWhiteSpace(userName) || String.IsNullOrWhiteSpace(password))
{ {

View file

@ -4,12 +4,12 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using File = AllDebridNET.File; using File = AllDebridNET.File;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.DebridClients;
public interface IAllDebridNetClientFactory public interface IAllDebridNetClientFactory
{ {
@ -51,14 +51,38 @@ public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger
} }
} }
public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
: ITorrentClient
{ {
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
private static List<TorrentClientTorrent> _cache = []; private static List<DebridClientTorrent> _cache = [];
private static Int64 _sessionCounter; private static Int64 _sessionCounter = 0;
public async Task<IList<TorrentClientTorrent>> GetTorrents() private static DebridClientTorrent Map(Magnet torrent)
{
var files = GetFiles(torrent.Files);
return new()
{
Id = torrent.Id.ToString(),
Filename = torrent.Filename ?? "",
OriginalFilename = torrent.Filename,
Hash = torrent.Hash ?? "",
Bytes = torrent.Size ?? 0,
OriginalBytes = torrent.Size ?? 0,
Host = null,
Split = 0,
Progress = (Int64)Math.Round((torrent.Downloaded ?? 0) * 100.0 / (torrent.Size ?? 1)),
Status = torrent.Status,
StatusCode = torrent.StatusCode ?? 0,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
Files = files,
Links = [],
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeders
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{ {
var results = await allDebridNetClientFactory.GetClient().Magnet.StatusLiveAsync(SessionId, _sessionCounter); var results = await allDebridNetClientFactory.GetClient().Magnet.StatusLiveAsync(SessionId, _sessionCounter);
@ -89,7 +113,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return _cache; return _cache;
} }
public async Task<TorrentClientUser> GetUser() public async Task<DebridClientUser> GetUser()
{ {
var user = await allDebridNetClientFactory.GetClient().User.GetAsync() ?? throw new("Unable to get user"); var user = await allDebridNetClientFactory.GetClient().User.GetAsync() ?? throw new("Unable to get user");
@ -100,7 +124,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
}; };
} }
public async Task<String> AddMagnet(String magnetLink) public async Task<String> AddTorrentMagnet(String magnetLink)
{ {
var result = await allDebridNetClientFactory.GetClient().Magnet.UploadMagnetAsync(magnetLink); var result = await allDebridNetClientFactory.GetClient().Magnet.UploadMagnetAsync(magnetLink);
@ -114,7 +138,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return resultId; return resultId;
} }
public async Task<String> AddFile(Byte[] bytes) public async Task<String> AddTorrentFile(Byte[] bytes)
{ {
var result = await allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes); var result = await allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes);
@ -128,9 +152,19 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return resultId; return resultId;
} }
public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<String> AddNzbLink(String nzbLink)
{ {
return Task.FromResult<IList<TorrentClientAvailableFile>>([]); throw new NotSupportedException();
}
public Task<String> AddNzbFile(Byte[] bytes, String? name)
{
throw new NotSupportedException();
}
public Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
{
return Task.FromResult<IList<DebridClientAvailableFile>>([]);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -139,12 +173,12 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return Task.FromResult<Int32?>(torrent.Files.Count); return Task.FromResult<Int32?>(torrent.Files.Count);
} }
public async Task Delete(String torrentId) public async Task Delete(Torrent torrent)
{ {
await allDebridNetClientFactory.GetClient().Magnet.DeleteAsync(torrentId); await allDebridNetClientFactory.GetClient().Magnet.DeleteAsync(torrent.RdId!);
} }
public async Task<String> Unrestrict(String link) public async Task<String> Unrestrict(Torrent torrent, String link)
{ {
var result = await allDebridNetClientFactory.GetClient().Links.DownloadLinkAsync(link); var result = await allDebridNetClientFactory.GetClient().Links.DownloadLinkAsync(link);
@ -156,7 +190,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return result.Link; return result.Link;
} }
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent) public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
{ {
try try
{ {
@ -255,40 +289,13 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
private static TorrentClientTorrent Map(Magnet torrent) private async Task<DebridClientTorrent> GetInfo(String torrentId)
{
var files = GetFiles(torrent.Files);
return new()
{
Id = torrent.Id.ToString(),
Filename = torrent.Filename ?? "",
OriginalFilename = torrent.Filename,
Hash = torrent.Hash ?? "",
Bytes = torrent.Size ?? 0,
OriginalBytes = torrent.Size ?? 0,
Host = null,
Split = 0,
Progress = (Int64)Math.Round(((torrent.Downloaded ?? 0) * 100.0) / (torrent.Size ?? 1)),
Status = torrent.Status,
StatusCode = torrent.StatusCode ?? 0,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
Files = files,
Links = [],
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeders
};
}
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
{ {
var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}"); var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
return Map(result); return Map(result);
} }
private static List<DebridClientFile> GetFiles(List<File>? files, String parentPath = "")
private static List<TorrentClientFile> GetFiles(List<File>? files, String parentPath = "")
{ {
if (files == null) if (files == null)
{ {
@ -301,7 +308,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
? file.FolderOrFileName ? file.FolderOrFileName
: Path.Combine(parentPath, file.FolderOrFileName); : Path.Combine(parentPath, file.FolderOrFileName);
var result = new List<TorrentClientFile>(); var result = new List<DebridClientFile>();
// If it's a file (has size) // If it's a file (has size)
if (file.Size.HasValue) if (file.Size.HasValue)

View file

@ -4,16 +4,88 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using Download = RdtClient.Data.Models.Data.Download; using Download = RdtClient.Data.Models.Data.Download;
using Torrent = DebridLinkFrNET.Models.Torrent; using Torrent = DebridLinkFrNET.Models.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.DebridClients;
public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{ {
public async Task<IList<TorrentClientTorrent>> GetTorrents() private DebridLinkFrNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("DebridLink API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
return debridLinkClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
}
private DebridClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id ?? "",
Filename = torrent.Name ?? "",
OriginalFilename = torrent.Name ?? "",
Hash = torrent.HashString ?? "",
Bytes = torrent.TotalSize,
OriginalBytes = 0,
Host = torrent.ServerId ?? "",
Split = 0,
Progress = torrent.DownloadPercent,
Status = torrent.Status.ToString(),
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
Files = (torrent.Files ?? []).Select((m, i) => new DebridClientFile
{
Path = m.Name ?? "",
Bytes = m.Size,
Id = i,
Selected = true,
DownloadLink = m.DownloadUrl
})
.ToList(),
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
Ended = null,
Speed = torrent.UploadSpeed,
Seeders = torrent.PeersConnected,
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{ {
var page = 0; var page = 0;
var results = new List<Torrent>(); var results = new List<Torrent>();
@ -35,7 +107,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return results.Select(Map).ToList(); return results.Select(Map).ToList();
} }
public async Task<TorrentClientUser> GetUser() public async Task<DebridClientUser> GetUser()
{ {
var user = await GetClient().Account.Infos(); var user = await GetClient().Account.Infos();
@ -46,23 +118,33 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
}; };
} }
public async Task<String> AddMagnet(String magnetLink) public async Task<String> AddTorrentMagnet(String magnetLink)
{ {
var result = await GetClient().Seedbox.AddTorrentAsync(magnetLink); var result = await GetClient().Seedbox.AddTorrentAsync(magnetLink);
return result.Id ?? ""; return result.Id ?? "";
} }
public async Task<String> AddFile(Byte[] bytes) public async Task<String> AddTorrentFile(Byte[] bytes)
{ {
var result = await GetClient().Seedbox.AddTorrentByFileAsync(bytes); var result = await GetClient().Seedbox.AddTorrentByFileAsync(bytes);
return result.Id ?? ""; return result.Id ?? "";
} }
public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<String> AddNzbLink(String nzbLink)
{ {
return Task.FromResult<IList<TorrentClientAvailableFile>>([]); throw new NotSupportedException();
}
public Task<String> AddNzbFile(Byte[] bytes, String? name)
{
throw new NotSupportedException();
}
public Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
{
return Task.FromResult<IList<DebridClientAvailableFile>>([]);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -71,17 +153,17 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return Task.FromResult<Int32?>(torrent.Files.Count); return Task.FromResult<Int32?>(torrent.Files.Count);
} }
public async Task Delete(String torrentId) public async Task Delete(Data.Models.Data.Torrent torrent)
{ {
await GetClient().Seedbox.DeleteAsync(torrentId); await GetClient().Seedbox.DeleteAsync(torrent.RdId!);
} }
public Task<String> Unrestrict(String link) public Task<String> Unrestrict(Data.Models.Data.Torrent torrent, String link)
{ {
return Task.FromResult(link); return Task.FromResult(link);
} }
public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, TorrentClientTorrent? torrentClientTorrent) public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, DebridClientTorrent? torrentClientTorrent)
{ {
try try
{ {
@ -197,79 +279,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
private DebridLinkFrNETClient GetClient() private async Task<DebridClientTorrent> GetInfo(String torrentId)
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("DebridLink API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
return debridLinkClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id ?? "",
Filename = torrent.Name ?? "",
OriginalFilename = torrent.Name ?? "",
Hash = torrent.HashString ?? "",
Bytes = torrent.TotalSize,
OriginalBytes = 0,
Host = torrent.ServerId ?? "",
Split = 0,
Progress = torrent.DownloadPercent,
Status = torrent.Status.ToString(),
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
Files = (torrent.Files ?? []).Select((m, i) => new TorrentClientFile
{
Path = m.Name ?? "",
Bytes = m.Size,
Id = i,
Selected = true,
DownloadLink = m.DownloadUrl
})
.ToList(),
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
Ended = null,
Speed = torrent.UploadSpeed,
Seeders = torrent.PeersConnected
};
}
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
{ {
var result = await GetClient().Seedbox.ListAsync(torrentId); var result = await GetClient().Seedbox.ListAsync(torrentId);

View file

@ -1,16 +1,17 @@
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.DebridClients;
public interface ITorrentClient public interface IDebridClient
{ {
Task<IList<TorrentClientTorrent>> GetTorrents(); Task<IList<DebridClientTorrent>> GetDownloads();
Task<TorrentClientUser> GetUser(); Task<DebridClientUser> GetUser();
Task<String> AddMagnet(String magnetLink); Task<String> AddTorrentMagnet(String magnetLink);
Task<String> AddFile(Byte[] bytes); Task<String> AddTorrentFile(Byte[] bytes);
Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash); Task<String> AddNzbLink(String nzbLink);
Task<String> AddNzbFile(Byte[] bytes, String? name);
Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash);
/// <summary> /// <summary>
/// Tell the debrid provider which files to download. /// Tell the debrid provider which files to download.
/// </summary> /// </summary>
@ -20,10 +21,9 @@ public interface ITorrentClient
/// <param name="torrent">The torrent to select files for</param> /// <param name="torrent">The torrent to select files for</param>
/// <returns>Number of files selected</returns> /// <returns>Number of files selected</returns>
Task<Int32?> SelectFiles(Torrent torrent); Task<Int32?> SelectFiles(Torrent torrent);
Task Delete(Torrent torrent);
Task Delete(String torrentId); Task<String> Unrestrict(Torrent torrent, String link);
Task<String> Unrestrict(String link); Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent);
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent); Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent);
/// <summary> /// <summary>

View file

@ -4,22 +4,82 @@ using Newtonsoft.Json;
using PremiumizeNET; using PremiumizeNET;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.DebridClients;
public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{ {
public async Task<IList<TorrentClientTorrent>> GetTorrents() private PremiumizeNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Premiumize API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var premiumizeNetClient = new PremiumizeNETClient(apiKey, httpClient);
return premiumizeNetClient;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
}
private static DebridClientTorrent Map(Transfer transfer)
{
return new()
{
Id = transfer.Id,
Filename = transfer.Name,
OriginalFilename = transfer.Name,
Hash = transfer.Src,
Bytes = 0,
OriginalBytes = 0,
Host = null,
Split = 0,
Progress = (Int64) ((transfer.Progress ?? 1.0) * 100.0),
Status = transfer.Status,
Message = transfer.Message,
StatusCode = 0,
Added = null,
Files = [],
Links =
[
transfer.FolderId
],
Ended = null,
Speed = 0,
Seeders = 0
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{ {
var results = await GetClient().Transfers.ListAsync(); var results = await GetClient().Transfers.ListAsync();
return results.Select(Map).ToList(); return results.Select(Map).ToList();
} }
public async Task<TorrentClientUser> GetUser() public async Task<DebridClientUser> GetUser()
{ {
var user = await GetClient().Account.InfoAsync() ?? throw new("Unable to get user"); var user = await GetClient().Account.InfoAsync() ?? throw new("Unable to get user");
@ -30,7 +90,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
}; };
} }
public async Task<String> AddMagnet(String magnetLink) public async Task<String> AddTorrentMagnet(String magnetLink)
{ {
var result = await GetClient().Transfers.CreateAsync(magnetLink, ""); var result = await GetClient().Transfers.CreateAsync(magnetLink, "");
@ -44,7 +104,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return resultId; return resultId;
} }
public async Task<String> AddFile(Byte[] bytes) public async Task<String> AddTorrentFile(Byte[] bytes)
{ {
var result = await GetClient().Transfers.CreateAsync(bytes, ""); var result = await GetClient().Transfers.CreateAsync(bytes, "");
@ -58,9 +118,19 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return resultId; return resultId;
} }
public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<String> AddNzbLink(String nzbLink)
{ {
return Task.FromResult<IList<TorrentClientAvailableFile>>([]); throw new NotSupportedException();
}
public Task<String> AddNzbFile(Byte[] bytes, String? name)
{
throw new NotSupportedException();
}
public Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
{
return Task.FromResult<IList<DebridClientAvailableFile>>([]);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -71,17 +141,17 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return Task.FromResult<Int32?>(1); return Task.FromResult<Int32?>(1);
} }
public async Task Delete(String id) public async Task Delete(Torrent torrent)
{ {
await GetClient().Transfers.DeleteAsync(id); await GetClient().Transfers.DeleteAsync(torrent.RdId);
} }
public Task<String> Unrestrict(String link) public Task<String> Unrestrict(Torrent torrent, String link)
{ {
return Task.FromResult(link); return Task.FromResult(link);
} }
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent) public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
{ {
try try
{ {
@ -194,67 +264,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
private PremiumizeNETClient GetClient() private async Task<DebridClientTorrent> GetInfo(String id)
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Premiumize API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var premiumizeNetClient = new PremiumizeNETClient(apiKey, httpClient);
return premiumizeNetClient;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
}
private static TorrentClientTorrent Map(Transfer transfer)
{
return new()
{
Id = transfer.Id,
Filename = transfer.Name,
OriginalFilename = transfer.Name,
Hash = transfer.Src,
Bytes = 0,
OriginalBytes = 0,
Host = null,
Split = 0,
Progress = (Int64)((transfer.Progress ?? 1.0) * 100.0),
Status = transfer.Status,
Message = transfer.Message,
StatusCode = 0,
Added = null,
Files = [],
Links =
[
transfer.FolderId
],
Ended = null,
Speed = 0,
Seeders = 0
};
}
private async Task<TorrentClientTorrent> GetInfo(String id)
{ {
var results = await GetClient().Transfers.ListAsync(); var results = await GetClient().Transfers.ListAsync();
var result = results.FirstOrDefault(m => m.Id == id) ?? throw new($"Unable to find transfer with ID {id}"); var result = results.FirstOrDefault(m => m.Id == id) ?? throw new($"Unable to find transfer with ID {id}");

View file

@ -4,18 +4,96 @@ using Newtonsoft.Json;
using RDNET; using RDNET;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using Download = RdtClient.Data.Models.Data.Download; using Download = RdtClient.Data.Models.Data.Download;
using Torrent = RDNET.Torrent; using Torrent = RDNET.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.DebridClients;
public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{ {
private TimeSpan? _offset; private TimeSpan? _offset;
public async Task<IList<TorrentClientTorrent>> GetTorrents() private RdNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
}
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 DebridClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id,
Filename = torrent.Filename,
OriginalFilename = torrent.OriginalFilename,
Hash = torrent.Hash,
Bytes = torrent.Bytes,
OriginalBytes = torrent.OriginalBytes,
Host = torrent.Host,
Split = torrent.Split,
Progress = torrent.Progress,
Status = torrent.Status,
Added = ChangeTimeZone(torrent.Added)!.Value,
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
{
Path = m.Path,
Bytes = m.Bytes,
Id = m.Id,
Selected = m.Selected
}).ToList(),
Links = torrent.Links,
Ended = ChangeTimeZone(torrent.Ended),
Speed = torrent.Speed,
Seeders = torrent.Seeders,
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{ {
var offset = 0; var offset = 0;
var results = new List<Torrent>(); var results = new List<Torrent>();
@ -37,7 +115,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return results.Select(Map).ToList(); return results.Select(Map).ToList();
} }
public async Task<TorrentClientUser> GetUser() public async Task<DebridClientUser> GetUser()
{ {
var user = await GetClient().User.GetAsync(); var user = await GetClient().User.GetAsync();
@ -48,7 +126,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
}; };
} }
public async Task<String> AddMagnet(String magnetLink) public async Task<String> AddTorrentMagnet(String magnetLink)
{ {
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout)); var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
@ -57,7 +135,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return result.Id; return result.Id;
} }
public async Task<String> AddFile(Byte[] bytes) public async Task<String> AddTorrentFile(Byte[] bytes)
{ {
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout)); var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
@ -66,15 +144,25 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return result.Id; return result.Id;
} }
public Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public Task<String> AddNzbLink(String nzbLink)
{ {
return Task.FromResult<IList<TorrentClientAvailableFile>>([]); throw new NotSupportedException();
}
public Task<String> AddNzbFile(Byte[] bytes, String? name)
{
throw new NotSupportedException();
}
public Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
{
return Task.FromResult<IList<DebridClientAvailableFile>>([]);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent) public async Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
{ {
List<TorrentClientFile> files; List<DebridClientFile> files;
Log("Seleting files", torrent); Log("Seleting files", torrent);
@ -105,12 +193,12 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return fileIds.Length; return fileIds.Length;
} }
public async Task Delete(String torrentId) public async Task Delete(Data.Models.Data.Torrent torrent)
{ {
await GetClient().Torrents.DeleteAsync(torrentId); await GetClient().Torrents.DeleteAsync(torrent.RdId!);
} }
public async Task<String> Unrestrict(String link) public async Task<String> Unrestrict(Data.Models.Data.Torrent torrent, String link)
{ {
var result = await GetClient().Unrestrict.LinkAsync(link); var result = await GetClient().Unrestrict.LinkAsync(link);
@ -122,7 +210,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return result.Download; return result.Download;
} }
public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, TorrentClientTorrent? torrentClientTorrent) public async Task<Data.Models.Data.Torrent> UpdateData(Data.Models.Data.Torrent torrent, DebridClientTorrent? torrentClientTorrent)
{ {
try try
{ {
@ -283,84 +371,6 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last())); return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
} }
private RdNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
}
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(Torrent torrent)
{
return new()
{
Id = torrent.Id,
Filename = torrent.Filename,
OriginalFilename = torrent.OriginalFilename,
Hash = torrent.Hash,
Bytes = torrent.Bytes,
OriginalBytes = torrent.OriginalBytes,
Host = torrent.Host,
Split = torrent.Split,
Progress = torrent.Progress,
Status = torrent.Status,
Added = ChangeTimeZone(torrent.Added)!.Value,
Files = (torrent.Files ?? []).Select(m => new TorrentClientFile
{
Path = m.Path,
Bytes = m.Bytes,
Id = m.Id,
Selected = m.Selected
})
.ToList(),
Links = torrent.Links,
Ended = ChangeTimeZone(torrent.Ended),
Speed = torrent.Speed,
Seeders = torrent.Seeders
};
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{ {
@ -372,7 +382,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value); return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value);
} }
private async Task<TorrentClientTorrent> GetInfo(String torrentId) private async Task<DebridClientTorrent> GetInfo(String torrentId)
{ {
var result = await GetClient().Torrents.GetInfoAsync(torrentId); var result = await GetClient().Torrents.GetInfoAsync(torrentId);

View file

@ -3,40 +3,188 @@ using Microsoft.Extensions.Logging;
using MonoTorrent; using MonoTorrent;
using Newtonsoft.Json; using Newtonsoft.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using TorBoxNET; using TorBoxNET;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.DebridClients;
public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{ {
private TimeSpan? _offset; private TimeSpan? _offset;
protected virtual ITorBoxNetClient GetClient()
public async Task<IList<TorrentClientTorrent>> GetTorrents()
{ {
var torrents = new List<TorrentInfoResult>(); try
var currentTorrents = await GetClient().Torrents.GetCurrentAsync(true);
if (currentTorrents != null)
{ {
torrents.AddRange(currentTorrents); 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)
var queuedTorrents = await GetClient().Torrents.GetQueuedAsync(true);
if (queuedTorrents != null)
{ {
torrents.AddRange(queuedTorrents); foreach (var inner in ae.InnerExceptions)
} {
logger.LogError(inner, $"The connection to TorBox has failed: {inner.Message}");
}
return torrents.Select(Map).ToList(); throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
} }
public async Task<TorrentClientUser> GetUser() protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetCurrentTorrents()
{
return await GetClient().Torrents.GetCurrentAsync(true);
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetQueuedTorrents()
{
return await GetClient().Torrents.GetQueuedAsync(true);
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetCurrentUsenet()
{
return await GetClient().Usenet.GetCurrentAsync(true);
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetQueuedUsenet()
{
return await GetClient().Usenet.GetQueuedAsync(true);
}
protected virtual async Task<Response<List<AvailableTorrent?>>> GetTorrentAvailability(String hash)
{
return await GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true);
}
protected virtual async Task<Response<List<AvailableUsenet?>>> GetUsenetAvailability(String hash)
{
return await GetClient().Usenet.GetAvailabilityAsync(hash, listFiles: true);
}
private DebridClientTorrent 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,
Type = DownloadType.Torrent,
Added = ChangeTimeZone(torrent.CreatedAt)!.Value,
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
{
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,
};
}
private DebridClientTorrent Map(UsenetInfoResult usenet)
{
return new()
{
Id = usenet.Hash,
Filename = usenet.Name,
OriginalFilename = usenet.Name,
Hash = usenet.Hash,
Bytes = usenet.Size,
OriginalBytes = usenet.Size,
Host = usenet.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(usenet.Progress * 100.0),
Status = usenet.DownloadState,
Type = DownloadType.Nzb,
Added = ChangeTimeZone(usenet.CreatedAt)!.Value,
Files = (usenet.Files ?? []).Select(m => new DebridClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
}).ToList(),
Links = [],
Ended = ChangeTimeZone(usenet.UpdatedAt),
Speed = usenet.DownloadSpeed,
Seeders = 0,
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{
var results = new List<DebridClientTorrent>();
var currentTorrents = await GetCurrentTorrents();
if (currentTorrents != null)
{
results.AddRange(currentTorrents.Select(Map));
}
var queuedTorrents = await GetQueuedTorrents();
if (queuedTorrents != null)
{
results.AddRange(queuedTorrents.Select(Map));
}
var currentNzbs = await GetCurrentUsenet();
if (currentNzbs != null)
{
results.AddRange(currentNzbs.Select(Map));
}
var queuedNzbs = await GetQueuedUsenet();
if (queuedNzbs != null)
{
results.AddRange(queuedNzbs.Select(Map));
}
return results;
}
public async Task<DebridClientUser> GetUser()
{ {
var user = await GetClient().User.GetAsync(false); var user = await GetClient().User.GetAsync(false);
@ -47,7 +195,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
}; };
} }
public async Task<String> AddMagnet(String magnetLink) public async Task<String> AddTorrentMagnet(String magnetLink)
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
@ -63,7 +211,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return result.Data!.Hash!; return result.Data!.Hash!;
} }
public async Task<String> AddFile(Byte[] bytes) public async Task<String> AddTorrentFile(Byte[] bytes)
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
@ -81,18 +229,42 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return result.Data!.Hash!; return result.Data!.Hash!;
} }
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public async Task<String> AddNzbLink(String nzbLink)
{ {
var availability = await GetClient().Torrents.GetAvailabilityAsync(hash, true); var result = await GetClient().Usenet.AddLinkAsync(nzbLink);
return result.Data!.Hash!;
}
public virtual async Task<String> AddNzbFile(Byte[] bytes, String? name)
{
var result = await GetClient().Usenet.AddFileAsync(bytes, name: name);
return result.Data!.Hash!;
}
public async Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
{
var availability = await GetTorrentAvailability(hash);
if (availability.Data != null && availability.Data.Count > 0) if (availability.Data != null && availability.Data.Count > 0)
{ {
return (availability.Data[0]?.Files ?? []).Select(file => new TorrentClientAvailableFile return (availability.Data[0]?.Files ?? []).Select(file => new DebridClientAvailableFile
{ {
Filename = file.Name, Filename = file.Name,
Filesize = file.Size Filesize = file.Size
}) }).ToList();
.ToList(); }
var usenetAvailability = await GetUsenetAvailability(hash);
if (usenetAvailability.Data != null && usenetAvailability.Data.Count > 0)
{
return (usenetAvailability.Data[0]?.Files ?? []).Select(file => new DebridClientAvailableFile
{
Filename = file.Name,
Filesize = file.Size
}).ToList();
} }
return []; return [];
@ -104,29 +276,69 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return Task.FromResult<Int32?>(torrent.Files.Count); return Task.FromResult<Int32?>(torrent.Files.Count);
} }
public async Task Delete(String torrentId) public async Task Delete(Torrent torrent)
{ {
await GetClient().Torrents.ControlAsync(torrentId, "delete"); if (torrent.RdId == null)
{
return;
}
if (torrent.Type == DownloadType.Nzb)
{
await GetClient().Usenet.ControlAsync(torrent.RdId, "delete");
}
else
{
await GetClient().Torrents.ControlAsync(torrent.RdId, "delete");
}
} }
public async Task<String> Unrestrict(String link) public async Task<String> Unrestrict(Torrent torrent, String link)
{ {
if (String.IsNullOrWhiteSpace(link))
{
throw new ArgumentException("Link cannot be null or empty", nameof(link));
}
var segments = link.Split('/'); var segments = link.Split('/');
if (segments is not [_, _, _, _, var torrentIdStr, var fileIdStrOrZip])
{
throw new ArgumentException($"Invalid link format: {link}", nameof(link));
}
var zipped = segments[5] == "zip"; var zipped = fileIdStrOrZip == "zip";
var fileId = zipped ? "0" : segments[5]; var fileIdStr = zipped ? "0" : fileIdStrOrZip;
var result = await GetClient().Torrents.RequestDownloadAsync(Convert.ToInt32(segments[4]), Convert.ToInt32(fileId), zipped); if (!Int32.TryParse(torrentIdStr, out var torrentId))
{
throw new ArgumentException($"Invalid torrent ID in link segment 4: {torrentIdStr}", nameof(link));
}
if (!Int32.TryParse(fileIdStr, out var fileId))
{
throw new ArgumentException($"Invalid file ID in link segment 5: {fileId}", nameof(link));
}
Response<String> result;
if (torrent.Type == DownloadType.Nzb)
{
result = await GetClient().Usenet.RequestDownloadAsync(torrentId, fileId, zipped);
}
else
{
result = await GetClient().Torrents.RequestDownloadAsync(torrentId, fileId, zipped);
}
if (result.Error != null) if (result.Error != null)
{ {
throw new("Unrestrict returned an invalid download"); throw new($"Unrestrict returned an invalid download: {result.Error}");
} }
return result.Data!; return result.Data!;
} }
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent) public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
{ {
try try
{ {
@ -135,7 +347,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return torrent; return torrent;
} }
var rdTorrent = torrentClientTorrent ?? await GetInfo(torrent.Hash) ?? throw new($"Resource not found"); var rdTorrent = torrentClientTorrent ?? await GetInfo(torrent.RdId, torrent.Type) ?? throw new($"Resource not found");
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename)) if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
{ {
@ -214,7 +426,29 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent) public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
{ {
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, true); Int32? id;
if (torrent.Type == DownloadType.Nzb)
{
if (torrent.RdId == null)
{
return null;
}
var usenets = await GetClient().Usenet.GetCurrentAsync(true);
var usenet = usenets?.FirstOrDefault(m => m.Hash == torrent.RdId);
id = (Int32?)usenet?.Id;
}
else
{
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true);
id = torrentId?.Id;
}
if (id == null)
{
return null;
}
var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList(); var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList();
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads) if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
@ -225,7 +459,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
[ [
new() new()
{ {
RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/zip", RestrictedLink = $"https://torbox.app/fakedl/{id}/zip",
FileName = $"{torrent.RdName}.zip" FileName = $"{torrent.RdName}.zip"
} }
]; ];
@ -235,7 +469,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return downloadableFiles.Select(file => new DownloadInfo return downloadableFiles.Select(file => new DownloadInfo
{ {
RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}", RestrictedLink = $"https://torbox.app/fakedl/{id}/{file.Id}",
FileName = Path.GetFileName(file.Path) FileName = Path.GetFileName(file.Path)
}) })
.ToList(); .ToList();
@ -250,84 +484,6 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
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 TorBox has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to TorBox 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
};
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{ {
@ -339,24 +495,39 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value); return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value);
} }
private async Task<TorrentClientTorrent> GetInfo(String torrentHash) private async Task<DebridClientTorrent?> GetInfo(String id, DownloadType type)
{ {
var result = await GetClient().Torrents.GetHashInfoAsync(torrentHash, true); if (type == DownloadType.Nzb)
{
var usenet = await GetClient().Usenet.GetHashInfoAsync(id, skipCache: true);
if (usenet != null)
{
return Map(usenet);
}
}
else
{
var result = await GetClient().Torrents.GetHashInfoAsync(id, skipCache: true);
return Map(result!); if (result != null)
{
return Map(result);
}
}
return null;
} }
public static void MoveHashDirContents(String extractPath, Torrent _torrent) public static void MoveHashDirContents(String extractPath, Torrent torrent)
{ {
var hashDir = Path.Combine(extractPath, _torrent.Hash); var hashDir = Path.Combine(extractPath, torrent.Hash);
if (Directory.Exists(hashDir)) if (Directory.Exists(hashDir))
{ {
var innerFolder = Directory.GetDirectories(hashDir)[0]; var innerFolder = Directory.GetDirectories(hashDir)[0];
var moveDir = extractPath; var moveDir = extractPath;
if (!extractPath.EndsWith(torrent.RdName!))
if (!extractPath.EndsWith(_torrent.RdName!))
{ {
moveDir = hashDir; moveDir = hashDir;
} }
@ -373,7 +544,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
Directory.Move(dir, destDir); Directory.Move(dir, destDir);
} }
if (!extractPath.Contains(_torrent.RdName!)) if (!extractPath.Contains(torrent.RdName!))
{ {
Directory.Delete(innerFolder, true); Directory.Delete(innerFolder, true);
} }

View file

@ -2,7 +2,7 @@
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services.Downloaders; using RdtClient.Service.Services.Downloaders;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.DebridClients;
namespace RdtClient.Service.Services; namespace RdtClient.Service.Services;
@ -45,7 +45,7 @@ public class DownloadClient(Download download, Torrent torrent, String destinati
if (torrent.ClientKind == Provider.AllDebrid && Type == Data.Enums.DownloadClient.Symlink) if (torrent.ClientKind == Provider.AllDebrid && Type == Data.Enums.DownloadClient.Symlink)
{ {
downloadPath = AllDebridTorrentClient.GetSymlinkPath(torrent, download); downloadPath = AllDebridDebridClient.GetSymlinkPath(torrent, download);
} }
if (torrent.ClientKind == Provider.DebridLink && Type == Data.Enums.DownloadClient.Symlink) if (torrent.ClientKind == Provider.DebridLink && Type == Data.Enums.DownloadClient.Symlink)

View file

@ -19,7 +19,6 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
public async Task AuthLogout() public async Task AuthLogout()
{ {
logger.LogDebug("Auth logout"); logger.LogDebug("Auth logout");
await authentication.Logout(); await authentication.Logout();
} }
@ -191,6 +190,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var results = new List<TorrentInfo>(); var results = new List<TorrentInfo>();
var allTorrents = await torrents.Get(); var allTorrents = await torrents.Get();
allTorrents = allTorrents.Where(m => m.Type == DownloadType.Torrent).ToList();
var prio = 0; var prio = 0;
@ -321,7 +321,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var torrent = await torrents.GetByHash(hash); var torrent = await torrents.GetByHash(hash);
if (torrent == null) if (torrent == null || torrent.Type != DownloadType.Torrent)
{ {
return null; return null;
} }
@ -345,7 +345,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var torrent = await torrents.GetByHash(hash); var torrent = await torrents.GetByHash(hash);
if (torrent == null) if (torrent == null || torrent.Type != DownloadType.Torrent)
{ {
return null; return null;
} }
@ -419,7 +419,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var torrent = await torrents.GetByHash(hash); var torrent = await torrents.GetByHash(hash);
if (torrent == null) if (torrent == null || torrent.Type != DownloadType.Torrent)
{ {
return; return;
} }
@ -511,7 +511,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
{ {
var allTorrents = await torrents.Get(); var allTorrents = await torrents.Get();
var torrentsToGroup = allTorrents.Where(m => !String.IsNullOrWhiteSpace(m.Category)) var torrentsToGroup = allTorrents.Where(m => m.Type == DownloadType.Torrent && !String.IsNullOrWhiteSpace(m.Category))
.Select(m => m.Category!.ToLower()) .Select(m => m.Category!.ToLower())
.ToList(); .ToList();
@ -592,6 +592,13 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
public async Task TorrentsTopPrio(String hash) public async Task TorrentsTopPrio(String hash)
{ {
var torrent = await torrents.GetByHash(hash);
if (torrent == null || torrent.Type != DownloadType.Torrent)
{
return;
}
await torrents.UpdatePriority(hash, 1); await torrents.UpdatePriority(hash, 1);
} }
@ -599,7 +606,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
{ {
var torrent = await torrents.GetByHash(hash); var torrent = await torrents.GetByHash(hash);
if (torrent == null) if (torrent == null || torrent.Type != DownloadType.Torrent)
{ {
return; return;
} }
@ -619,7 +626,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
{ {
var torrent = await torrents.GetByHash(hash); var torrent = await torrents.GetByHash(hash);
if (torrent == null) if (torrent == null || torrent.Type != DownloadType.Torrent)
{ {
return; return;
} }

View file

@ -10,67 +10,65 @@ public class RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
var allTorrents = await torrents.Get(); var allTorrents = await torrents.Get();
var torrentDtos = allTorrents.Select(torrent => new TorrentDto var torrentDtos = allTorrents.Select(torrent => new TorrentDto
{ {
TorrentId = torrent.TorrentId, TorrentId = torrent.TorrentId,
Hash = torrent.Hash, Hash = torrent.Hash,
Category = torrent.Category, Category = torrent.Category,
DownloadAction = torrent.DownloadAction, DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction, FinishedAction = torrent.FinishedAction,
FinishedActionDelay = torrent.FinishedActionDelay, FinishedActionDelay = torrent.FinishedActionDelay,
HostDownloadAction = torrent.HostDownloadAction, HostDownloadAction = torrent.HostDownloadAction,
DownloadMinSize = torrent.DownloadMinSize, DownloadMinSize = torrent.DownloadMinSize,
IncludeRegex = torrent.IncludeRegex, IncludeRegex = torrent.IncludeRegex,
ExcludeRegex = torrent.ExcludeRegex, ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles, DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = torrent.DownloadClient, DownloadClient = torrent.DownloadClient,
Added = torrent.Added, Added = torrent.Added,
FilesSelected = torrent.FilesSelected, FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed, Completed = torrent.Completed,
IsFile = torrent.IsFile, Type = torrent.Type,
Priority = torrent.Priority, IsFile = torrent.IsFile,
RetryCount = torrent.RetryCount, Priority = torrent.Priority,
DownloadRetryAttempts = torrent.DownloadRetryAttempts, RetryCount = torrent.RetryCount,
TorrentRetryAttempts = torrent.TorrentRetryAttempts, DownloadRetryAttempts = torrent.DownloadRetryAttempts,
DeleteOnError = torrent.DeleteOnError, TorrentRetryAttempts = torrent.TorrentRetryAttempts,
Lifetime = torrent.Lifetime, DeleteOnError = torrent.DeleteOnError,
Error = torrent.Error, Lifetime = torrent.Lifetime,
RdId = torrent.RdId, Error = torrent.Error,
RdName = torrent.RdName, RdId = torrent.RdId,
RdSize = torrent.RdSize, RdName = torrent.RdName,
RdHost = torrent.RdHost, RdSize = torrent.RdSize,
RdSplit = torrent.RdSplit, RdHost = torrent.RdHost,
RdProgress = torrent.RdProgress, RdSplit = torrent.RdSplit,
RdStatus = torrent.RdStatus, RdProgress = torrent.RdProgress,
RdStatusRaw = torrent.RdStatusRaw, RdStatus = torrent.RdStatus,
RdAdded = torrent.RdAdded, RdStatusRaw = torrent.RdStatusRaw,
RdEnded = torrent.RdEnded, RdAdded = torrent.RdAdded,
RdSpeed = torrent.RdSpeed, RdEnded = torrent.RdEnded,
RdSeeders = torrent.RdSeeders, RdSpeed = torrent.RdSpeed,
Files = torrent.Files, RdSeeders = torrent.RdSeeders,
Downloads = torrent.Downloads.Select(download => new DownloadDto Files = torrent.Files,
{ Downloads = torrent.Downloads.Select(download => new DownloadDto
DownloadId = download.DownloadId, {
TorrentId = download.TorrentId, DownloadId = download.DownloadId,
Path = download.Path, TorrentId = download.TorrentId,
Link = download.Link, Path = download.Path,
Added = download.Added, Link = download.Link,
DownloadQueued = download.DownloadQueued, Added = download.Added,
DownloadStarted = download.DownloadStarted, DownloadQueued = download.DownloadQueued,
DownloadFinished = download.DownloadFinished, DownloadStarted = download.DownloadStarted,
UnpackingQueued = download.UnpackingQueued, DownloadFinished = download.DownloadFinished,
UnpackingStarted = download.UnpackingStarted, UnpackingQueued = download.UnpackingQueued,
UnpackingFinished = download.UnpackingFinished, UnpackingStarted = download.UnpackingStarted,
Completed = download.Completed, UnpackingFinished = download.UnpackingFinished,
RetryCount = download.RetryCount, Completed = download.Completed,
Error = download.Error, RetryCount = download.RetryCount,
BytesTotal = download.BytesTotal, Error = download.Error,
BytesDone = download.BytesDone, BytesTotal = download.BytesTotal,
Speed = download.Speed BytesDone = download.BytesDone,
}) Speed = download.Speed
.ToList() }).ToList()
}) }).ToList();
.ToList();
await hub.Clients.All.SendCoreAsync("update", await hub.Clients.All.SendCoreAsync("update",
[ [
torrentDtos torrentDtos

View file

@ -0,0 +1,220 @@
using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.Sabnzbd;
using RdtClient.Service.Helpers;
namespace RdtClient.Service.Services;
public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings appSettings)
{
public virtual async Task<SabnzbdQueue> GetQueue()
{
var allTorrents = await torrents.Get();
var activeTorrents = allTorrents.Where(t => t.Type == DownloadType.Nzb && t.Completed == null).ToList();
var queue = new SabnzbdQueue
{
NoOfSlots = activeTorrents.Count,
Slots = activeTorrents.Select((t, index) =>
{
Double downloadProgress = 0;
var rdProgress = Math.Clamp(t.RdProgress ?? 0.0, 0.0, 100.0) / 100.0;
if (t.Downloads is { Count: > 0 })
{
var bytesDone = t.Downloads.Sum(m => m.BytesDone);
var bytesTotal = t.Downloads.Sum(m => m.BytesTotal);
downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0;
}
var progress = (rdProgress + downloadProgress) / 2.0;
var timeLeft = "0:00:00";
var startTime = t.Retry > t.Added ? t.Retry.Value : t.Added;
var elapsed = DateTimeOffset.UtcNow - startTime;
if (progress is > 0 and < 1.0)
{
var totalEstimatedTime = TimeSpan.FromTicks((Int64)(elapsed.Ticks / progress));
var remaining = totalEstimatedTime - elapsed;
if (remaining.TotalSeconds > 0)
{
timeLeft = $"{(Int32)remaining.TotalHours}:{remaining.Minutes:D2}:{remaining.Seconds:D2}";
}
}
return new SabnzbdQueueSlot
{
Index = index,
NzoId = t.Hash,
Filename = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal)),
SizeLeft = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal - d.BytesDone)),
Percentage = (progress * 100.0).ToString("0"),
Status = t.RdStatus switch
{
TorrentStatus.Processing => "Propagating",
TorrentStatus.Finished => "Completed",
TorrentStatus.Downloading => "Downloading",
TorrentStatus.WaitingForFileSelection => "Propagating",
TorrentStatus.Error => "Failed",
TorrentStatus.Queued => "Queued",
_ => "Downloading"
},
Category = t.Category ?? "*",
Priority = "Normal",
TimeLeft = timeLeft
};
}).ToList()
};
return queue;
}
public virtual async Task<SabnzbdHistory> GetHistory()
{
var allTorrents = await torrents.Get();
var completedTorrents = allTorrents.Where(t => t.Type == DownloadType.Nzb && t.Completed != null).ToList();
var savePath = Settings.AppDefaultSavePath;
var history = new SabnzbdHistory
{
NoOfSlots = completedTorrents.Count,
TotalSlots = completedTorrents.Count,
Slots = completedTorrents.Select(t =>
{
var path = savePath;
if (!String.IsNullOrWhiteSpace(t.Category))
{
path = Path.Combine(path, t.Category);
}
if (!String.IsNullOrWhiteSpace(t.RdName))
{
path = Path.Combine(path, t.RdName);
}
return new SabnzbdHistorySlot
{
NzoId = t.Hash,
Name = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal)),
Status = String.IsNullOrWhiteSpace(t.Error) ? "Completed" : "Failed",
Category = t.Category ?? "Default",
Path = path
};
}).ToList()
};
return history;
}
public virtual async Task<String> AddFile(Byte[] fileBytes, String? fileName, String? category, Int32? priority)
{
logger.LogDebug($"Add file {category}");
var torrent = new Torrent
{
Category = category,
DownloadClient = Settings.Get.DownloadClient.Client,
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
FinishedAction = TorrentFinishedAction.None,
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex,
ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex,
TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts,
DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts,
DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError,
Lifetime = Settings.Get.Integrations.Default.TorrentLifetime,
Priority = (priority ?? Settings.Get.Integrations.Default.Priority) > 0 ? 1 : null
};
var result = await torrents.AddNzbFileToDebridQueue(fileBytes, fileName, torrent);
return result.Hash;
}
public virtual async Task<String> AddUrl(String url, String? category, Int32? priority)
{
logger.LogDebug($"Add url {category}");
var torrent = new Torrent
{
Category = category,
DownloadClient = Settings.Get.DownloadClient.Client,
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
FinishedAction = TorrentFinishedAction.None,
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex,
ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex,
TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts,
DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts,
DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError,
Lifetime = Settings.Get.Integrations.Default.TorrentLifetime,
Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null)
};
var result = await torrents.AddNzbLinkToDebridQueue(url, torrent);
return result.Hash;
}
public virtual async Task Delete(String hash)
{
var torrent = await torrents.GetByHash(hash);
if (torrent != null)
{
await torrents.Delete(torrent.TorrentId, true, true, true);
}
}
public virtual List<String> GetCategories()
{
var categoryList = (Settings.Get.General.Categories ?? "")
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(m => m.Trim())
.Where(m => m != "*")
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.ToList();
categoryList.Insert(0, "*");
return categoryList;
}
public virtual SabnzbdConfig GetConfig()
{
var savePath = Settings.AppDefaultSavePath;
var categoryList = GetCategories();
var categories = categoryList.Select((c, i) => new SabnzbdCategory
{
Name = c,
Order = i,
Dir = c == "*" ? "" : Path.Combine(savePath, c)
}).ToList();
var config = new SabnzbdConfig
{
Misc = new SabnzbdMisc
{
CompleteDir = savePath,
DownloadDir = savePath,
Port = appSettings.Port.ToString(),
Version = "4.4.0"
},
Categories = categories
};
return config;
}
}

View file

@ -3,16 +3,18 @@ using System.IO.Abstractions;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MonoTorrent; using MonoTorrent;
using RdtClient.Data.Data; using RdtClient.Data.Data;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.DebridClient;
using RdtClient.Service.BackgroundServices; using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.DebridClients;
using RdtClient.Service.Wrappers; using RdtClient.Service.Wrappers;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
@ -25,11 +27,11 @@ public class Torrents(
IProcessFactory processFactory, IProcessFactory processFactory,
IFileSystem fileSystem, IFileSystem fileSystem,
IEnricher enricher, IEnricher enricher,
AllDebridTorrentClient allDebridTorrentClient, AllDebridDebridClient allDebridDebridClient,
PremiumizeTorrentClient premiumizeTorrentClient, PremiumizeDebridClient premiumizeDebridClient,
RealDebridTorrentClient realDebridTorrentClient, RealDebridDebridClient realDebridDebridClient,
DebridLinkClient debridLinkClient, DebridLinkClient debridLinkClient,
TorBoxTorrentClient torBoxTorrentClient) TorBoxDebridClient torBoxDebridClient)
{ {
private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
@ -38,25 +40,25 @@ public class Torrents(
ReferenceHandler = ReferenceHandler.IgnoreCycles ReferenceHandler = ReferenceHandler.IgnoreCycles
}; };
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1); private IDebridClient DebridClient
private ITorrentClient TorrentClient
{ {
get get
{ {
return Settings.Get.Provider.Provider switch return Settings.Get.Provider.Provider switch
{ {
Provider.Premiumize => premiumizeTorrentClient, Provider.Premiumize => premiumizeDebridClient,
Provider.RealDebrid => realDebridTorrentClient, Provider.RealDebrid => realDebridDebridClient,
Provider.AllDebrid => allDebridTorrentClient, Provider.AllDebrid => allDebridDebridClient,
Provider.DebridLink => debridLinkClient, Provider.DebridLink => debridLinkClient,
Provider.TorBox => torBoxTorrentClient, Provider.TorBox => torBoxDebridClient,
_ => throw new("Invalid Provider") _ => throw new("Invalid Provider")
}; };
} }
} }
public async Task<IList<Torrent>> Get() private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
public virtual async Task<IList<Torrent>> Get()
{ {
var torrents = await torrentData.Get(); var torrents = await torrentData.Get();
@ -82,7 +84,7 @@ public class Torrents(
return torrents; return torrents;
} }
public async Task<Torrent?> GetByHash(String hash) public virtual async Task<Torrent?> GetByHash(String hash)
{ {
var torrent = await torrentData.GetByHash(hash); var torrent = await torrentData.GetByHash(hash);
@ -108,7 +110,83 @@ public class Torrents(
await torrentData.UpdateCategory(torrent.TorrentId, category); await torrentData.UpdateCategory(torrent.TorrentId, category);
} }
public async Task<Torrent> AddMagnetToDebridQueue(String magnetLink, Torrent torrent) public virtual async Task<Torrent> AddNzbLinkToDebridQueue(String nzbLink, Torrent torrent)
{
torrent.RdStatus = TorrentStatus.Queued;
try
{
var uri = new Uri(nzbLink);
var lastSegment = uri.Segments.LastOrDefault()?.TrimEnd('/');
torrent.RdName = !String.IsNullOrWhiteSpace(lastSegment) ? lastSegment : "Unknown NZB";
}
catch(Exception ex)
{
logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
throw new ($"{ex.Message}, trying to parse {nzbLink}");
}
var nzbHash = ComputeMd5Hash(nzbLink);
var nzbNewTorrent = await AddQueued(nzbHash, nzbLink, false, DownloadType.Nzb, torrent);
Log($"Adding {nzbLink} with hash {nzbHash} (nzb link) to queue");
await CopyAddedTorrent(nzbNewTorrent);
return nzbNewTorrent;
}
public virtual async Task<Torrent> AddNzbFileToDebridQueue(Byte[] bytes, String? fileName, Torrent torrent)
{
torrent.RdName = fileName ?? "Unknown NZB";
torrent.RdStatus = TorrentStatus.Queued;
try
{
using var stream = new MemoryStream(bytes);
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Ignore,
XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);
var nzbNamespace = doc.Root?.GetDefaultNamespace() ?? XNamespace.None;
var title = doc.Root?
.Elements(nzbNamespace + "head")
.Elements(nzbNamespace + "meta")
.FirstOrDefault(x => x.Attribute("type")?.Value == "name")?
.Value;
if (String.IsNullOrWhiteSpace(title))
{
title = doc.Root?
.Elements(nzbNamespace + "head")
.Elements(nzbNamespace + "meta")
.FirstOrDefault(x => x.Attribute("type")?.Value == "title")?
.Value;
}
if (!String.IsNullOrWhiteSpace(title))
{
torrent.RdName = title.Trim();
}
}
catch(Exception ex)
{
logger.LogError(ex, "{ex.Message}, trying to parse NZB file contents", ex.Message);
throw new($"{ex.Message}, trying to parse NZB file contents");
}
var nzbHash = ComputeMd5HashFromBytes(bytes);
var nzbFileAsBase64 = Convert.ToBase64String(bytes);
var nzbNewTorrent = await AddQueued(nzbHash, nzbFileAsBase64, true, DownloadType.Nzb, torrent);
Log($"Adding {nzbHash} (nzb file) to queue", nzbNewTorrent);
await CopyAddedTorrent(nzbNewTorrent);
return nzbNewTorrent;
}
public virtual async Task<Torrent> AddMagnetToDebridQueue(String magnetLink, Torrent torrent)
{ {
var enriched = await enricher.EnrichMagnetLink(magnetLink); var enriched = await enricher.EnrichMagnetLink(magnetLink);
MagnetLink magnet; MagnetLink magnet;
@ -155,22 +233,22 @@ public class Torrents(
torrent.RdName = magnet.Name; torrent.RdName = magnet.Name;
var hash = magnet.InfoHashes.V1OrV2.ToHex(); var hash = magnet.InfoHashes.V1OrV2.ToHex();
var newTorrent = await AddQueued(hash, enriched, false, torrent); var newTorrent = await AddQueued(hash, enriched, false, DownloadType.Torrent, torrent);
Log($"Adding {hash} (magnet link) to queue", newTorrent); Log($"Adding {hash} (magnet link) to queue", newTorrent);
await CopyAddedTorrent(magnet.Name!, magnetLink); await CopyAddedTorrent(newTorrent);
return newTorrent; return newTorrent;
} }
public async Task<Torrent> AddFileToDebridQueue(Byte[] bytes, Torrent torrent) public virtual async Task<Torrent> AddFileToDebridQueue(Byte[] bytes, Torrent torrent)
{ {
MonoTorrent.Torrent monoTorrent;
var enriched = await enricher.EnrichTorrentBytes(bytes); var enriched = await enricher.EnrichTorrentBytes(bytes);
String fileAsBase64; String fileAsBase64;
MonoTorrent.Torrent monoTorrent;
if (enriched.SequenceEqual(bytes)) if (enriched.SequenceEqual(bytes))
{ {
fileAsBase64 = Convert.ToBase64String(bytes); fileAsBase64 = Convert.ToBase64String(bytes);
@ -228,56 +306,61 @@ public class Torrents(
var hash = monoTorrent.InfoHashes.V1OrV2.ToHex(); var hash = monoTorrent.InfoHashes.V1OrV2.ToHex();
var newTorrent = await AddQueued(hash, fileAsBase64, true, torrent); var newTorrent = await AddQueued(hash, fileAsBase64, true, DownloadType.Torrent, torrent);
Log($"Adding {hash} (torrent file) to queue", newTorrent); Log($"Adding {hash} (torrent file) to queue", newTorrent);
await CopyAddedTorrent(monoTorrent.Name, bytes); await CopyAddedTorrent(newTorrent);
return newTorrent; return newTorrent;
} }
private async Task CopyAddedTorrent(String torrentName, Object fileOrMagnet) private async Task CopyAddedTorrent(Torrent torrent)
{ {
if (!String.IsNullOrWhiteSpace(Settings.Get.General.CopyAddedTorrents)) if (String.IsNullOrWhiteSpace(Settings.Get.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName))
{ {
try return;
}
try
{
if (!fileSystem.Directory.Exists(Settings.Get.General.CopyAddedTorrents))
{ {
if (!Directory.Exists(Settings.Get.General.CopyAddedTorrents)) fileSystem.Directory.CreateDirectory(Settings.Get.General.CopyAddedTorrents);
{
Directory.CreateDirectory(Settings.Get.General.CopyAddedTorrents);
}
var copyFileName = Path.Combine(Settings.Get.General.CopyAddedTorrents, FileHelper.RemoveInvalidFileNameChars(torrentName));
copyFileName = fileOrMagnet switch
{
String => $"{copyFileName}.magnet",
Byte[] => $"{copyFileName}.torrent",
_ => throw new ArgumentException("Unexpected type for fileOrMagnet")
};
if (File.Exists(copyFileName))
{
File.Delete(copyFileName);
}
switch (fileOrMagnet)
{
case String magnetLink:
await File.WriteAllTextAsync(copyFileName, magnetLink);
break;
case Byte[] torrentFile:
await File.WriteAllBytesAsync(copyFileName, torrentFile);
break;
}
} }
catch (Exception ex)
var extension = torrent.Type switch
{ {
logger.LogError(ex, $"Unable to create torrent blackhole directory: {Settings.Get.General.CopyAddedTorrents}: {ex.Message}"); DownloadType.Nzb => ".nzb",
DownloadType.Torrent => torrent.IsFile ? ".torrent" : ".magnet",
_ => throw new ArgumentException("Unexpected DownloadType")
};
var copyFileName = Path.Combine(Settings.Get.General.CopyAddedTorrents, FileHelper.RemoveInvalidFileNameChars(torrent.RdName));
if (!copyFileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
{
copyFileName += extension;
} }
if (fileSystem.File.Exists(copyFileName))
{
fileSystem.File.Delete(copyFileName);
}
if (torrent.IsFile)
{
var bytes = Convert.FromBase64String(torrent.FileOrMagnet);
await fileSystem.File.WriteAllBytesAsync(copyFileName, bytes);
}
else
{
await fileSystem.File.WriteAllTextAsync(copyFileName, torrent.FileOrMagnet);
}
}
catch (Exception ex)
{
logger.LogError(ex, $"Unable to create torrent blackhole directory: {Settings.Get.General.CopyAddedTorrents}: {ex.Message}");
} }
} }
@ -305,9 +388,20 @@ public class Torrents(
try try
{ {
var id = torrent.IsFile String id;
? await TorrentClient.AddFile(Convert.FromBase64String(torrent.FileOrMagnet))
: await TorrentClient.AddMagnet(torrent.FileOrMagnet); if (torrent.Type == DownloadType.Nzb)
{
id = torrent.IsFile
? await DebridClient.AddNzbFile(Convert.FromBase64String(torrent.FileOrMagnet), torrent.RdName)
: await DebridClient.AddNzbLink(torrent.FileOrMagnet);
}
else
{
id = torrent.IsFile
? await DebridClient.AddTorrentFile(Convert.FromBase64String(torrent.FileOrMagnet))
: await DebridClient.AddTorrentMagnet(torrent.FileOrMagnet);
}
await torrentData.UpdateRdId(torrent, id); await torrentData.UpdateRdId(torrent, id);
@ -319,9 +413,9 @@ public class Torrents(
} }
} }
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public async Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
{ {
var result = await TorrentClient.GetAvailableFiles(hash); var result = await DebridClient.GetAvailableFiles(hash);
return result; return result;
} }
@ -335,7 +429,7 @@ public class Torrents(
return; return;
} }
var selected = await TorrentClient.SelectFiles(torrent); var selected = await DebridClient.SelectFiles(torrent);
if (selected == 0) if (selected == 0)
{ {
@ -352,7 +446,7 @@ public class Torrents(
return; return;
} }
var downloadInfos = await TorrentClient.GetDownloadInfos(torrent); var downloadInfos = await DebridClient.GetDownloadInfos(torrent);
if (downloadInfos == null) if (downloadInfos == null)
{ {
@ -394,7 +488,7 @@ public class Torrents(
await torrentData.UpdateComplete(torrent.TorrentId, "All files excluded", DateTimeOffset.Now, false); await torrentData.UpdateComplete(torrent.TorrentId, "All files excluded", DateTimeOffset.Now, false);
} }
public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles) public virtual async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles)
{ {
var torrent = await GetById(torrentId); var torrent = await GetById(torrentId);
@ -460,7 +554,7 @@ public class Torrents(
try try
{ {
await TorrentClient.Delete(torrent.RdId); await DebridClient.Delete(torrent);
} }
catch catch
{ {
@ -509,25 +603,21 @@ public class Torrents(
Log("Unrestricting link", download, download.Torrent); Log("Unrestricting link", download, download.Torrent);
var unrestrictedLink = await TorrentClient.Unrestrict(download.Path); var unrestrictedLink = await DebridClient.Unrestrict(download.Torrent!, download.Path);
await downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink); await downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink);
return unrestrictedLink; return unrestrictedLink;
} }
/// <summary> /// <inheritdoc />
/// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" />
/// is not set by
/// <see cref="ITorrentClient.GetDownloadInfos" />
/// </summary>
public async Task<String> RetrieveFileName(Guid downloadId) public async Task<String> RetrieveFileName(Guid downloadId)
{ {
var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found"); var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found");
Log($"Retrieving filename for", download, download.Torrent); Log($"Retrieving filename for", download, download.Torrent!);
var fileName = await TorrentClient.GetFileName(download); var fileName = await DebridClient.GetFileName(download);
await downloads.UpdateFileName(downloadId, fileName); await downloads.UpdateFileName(downloadId, fileName);
@ -536,7 +626,7 @@ public class Torrents(
public async Task<Profile> GetProfile() public async Task<Profile> GetProfile()
{ {
var user = await TorrentClient.GetUser(); var user = await DebridClient.GetUser();
var profile = new Profile var profile = new Profile
{ {
@ -560,7 +650,7 @@ public class Torrents(
try try
{ {
var rdTorrents = await TorrentClient.GetTorrents(); var rdTorrents = await DebridClient.GetDownloads();
foreach (var rdTorrent in rdTorrents) foreach (var rdTorrent in rdTorrents)
{ {
@ -594,7 +684,7 @@ public class Torrents(
continue; continue;
} }
torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, Settings.Get.DownloadClient.Client, newTorrent); torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, DownloadType.Torrent, Settings.Get.DownloadClient.Client, newTorrent);
await UpdateTorrentClientData(torrent, rdTorrent); await UpdateTorrentClientData(torrent, rdTorrent);
} }
@ -670,15 +760,31 @@ public class Torrents(
Torrent newTorrent; Torrent newTorrent;
if (torrent.IsFile) if (torrent.Type == DownloadType.Nzb)
{ {
var bytes = Convert.FromBase64String(torrent.FileOrMagnet); if (torrent.IsFile)
{
var bytes = Convert.FromBase64String(torrent.FileOrMagnet!);
newTorrent = await AddFileToDebridQueue(bytes, torrent); newTorrent = await AddNzbFileToDebridQueue(bytes, torrent.RdName, torrent);
}
else
{
newTorrent = await AddNzbLinkToDebridQueue(torrent.FileOrMagnet!, torrent);
}
} }
else else
{ {
newTorrent = await AddMagnetToDebridQueue(torrent.FileOrMagnet, torrent); if (torrent.IsFile)
{
var bytes = Convert.FromBase64String(torrent.FileOrMagnet!);
newTorrent = await AddFileToDebridQueue(bytes, torrent);
}
else
{
newTorrent = await AddMagnetToDebridQueue(torrent.FileOrMagnet!, torrent);
}
} }
await torrentData.UpdateRetry(newTorrent.TorrentId, null, retryCount); await torrentData.UpdateRetry(newTorrent.TorrentId, null, retryCount);
@ -809,6 +915,7 @@ public class Torrents(
private async Task<Torrent> AddQueued(String infoHash, private async Task<Torrent> AddQueued(String infoHash,
String fileOrMagnetContents, String fileOrMagnetContents,
Boolean isFile, Boolean isFile,
DownloadType downloadType,
Torrent torrent) Torrent torrent)
{ {
var existingTorrent = await torrentData.GetByHash(infoHash); var existingTorrent = await torrentData.GetByHash(infoHash);
@ -822,6 +929,7 @@ public class Torrents(
infoHash, infoHash,
fileOrMagnetContents, fileOrMagnetContents,
isFile, isFile,
downloadType,
torrent.DownloadClient, torrent.DownloadClient,
torrent); torrent);
@ -931,13 +1039,13 @@ public class Torrents(
} }
} }
private async Task UpdateTorrentClientData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent = null) private async Task UpdateTorrentClientData(Torrent torrent, DebridClientTorrent? torrentClientTorrent = null)
{ {
try try
{ {
var originalTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions); var originalTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
await TorrentClient.UpdateData(torrent, torrentClientTorrent); await DebridClient.UpdateData(torrent, torrentClientTorrent);
var newTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions); var newTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
@ -976,4 +1084,26 @@ public class Torrents(
logger.LogDebug(message); logger.LogDebug(message);
} }
private static String ComputeSha1Hash(String input)
{
using var sha1 = System.Security.Cryptography.SHA1.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = sha1.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
private static String ComputeMd5Hash(String input)
{
using var md5 = System.Security.Cryptography.MD5.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = md5.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
private static String ComputeMd5HashFromBytes(Byte[] bytes)
{
using var md5 = System.Security.Cryptography.MD5.Create();
var hashBytes = md5.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
} }

View file

@ -1,7 +1,7 @@
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.DebridClients;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Rar; using SharpCompress.Archives.Rar;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
@ -106,7 +106,7 @@ public class UnpackClient(Download download, String destinationPath)
if (_torrent.ClientKind == Provider.TorBox) if (_torrent.ClientKind == Provider.TorBox)
{ {
TorBoxTorrentClient.MoveHashDirContents(extractPath, _torrent); TorBoxDebridClient.MoveHashDirContents(extractPath, _torrent);
} }
} }
catch (Exception ex) catch (Exception ex)

View file

@ -0,0 +1,255 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Models.Sabnzbd;
using RdtClient.Service.Services;
using RdtClient.Web.Controllers;
namespace RdtClient.Web.Test.Controllers;
public class SabnzbdControllerTest
{
private readonly Mock<Sabnzbd> _sabnzbdMock;
private readonly Mock<Authentication> _authenticationMock;
private readonly SabnzbdController _controller;
public SabnzbdControllerTest()
{
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.None;
Data.Data.SettingData.Get.Provider.ApiKey = "test-api-key";
var torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
var sabnzbdLoggerMock = new Mock<ILogger<Sabnzbd>>();
_sabnzbdMock = new Mock<Sabnzbd>(sabnzbdLoggerMock.Object, torrentsMock.Object, null!);
var loggerMock = new Mock<ILogger<SabnzbdController>>();
_authenticationMock = new Mock<Authentication>(null!, null!, null!);
_controller = new SabnzbdController(loggerMock.Object, _sabnzbdMock.Object);
var httpContext = new DefaultHttpContext();
_controller.ControllerContext = new ControllerContext
{
HttpContext = httpContext
};
}
[Fact]
public async Task GetQueue_ReturnsOk()
{
// Arrange
var queue = new SabnzbdQueue { NoOfSlots = 1 };
_sabnzbdMock.Setup(s => s.GetQueue()).ReturnsAsync(queue);
// Act
var result = await _controller.Queue();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.NotNull(response.Queue);
Assert.Equal(1, response.Queue.NoOfSlots);
}
[Fact]
public async Task GetQueue_Unauthorized_ReturnsOk_BecauseFilterIsSkippedInUnitTests()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.UserNamePassword;
var queue = new SabnzbdQueue { NoOfSlots = 1 };
_sabnzbdMock.Setup(s => s.GetQueue()).ReturnsAsync(queue);
// Act
var result = await _controller.Queue();
// Assert
// In unit tests, filters are not executed.
// We test the filter separately in SabnzbdAuthFilterTest.cs
Assert.IsType<OkObjectResult>(result);
}
[Fact]
public async Task GetQueue_WithMaAuth_ReturnsOk()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new QueryString("?ma_username=user&ma_password=pass");
_controller.ControllerContext.HttpContext = httpContext;
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
var queue = new SabnzbdQueue { NoOfSlots = 1 };
_sabnzbdMock.Setup(s => s.GetQueue()).ReturnsAsync(queue);
// Act
var result = await _controller.Queue();
// Assert
Assert.IsType<OkObjectResult>(result);
}
[Fact]
public async Task GetHistory_ReturnsOk()
{
// Arrange
var history = new SabnzbdHistory { NoOfSlots = 1 };
_sabnzbdMock.Setup(s => s.GetHistory()).ReturnsAsync(history);
// Act
var result = await _controller.History();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.NotNull(response.History);
Assert.Equal(1, response.History.NoOfSlots);
}
[Fact]
public void GetVersion_HasAllowAnonymousAttribute()
{
// Arrange
var type = typeof(SabnzbdController);
var method = type.GetMethod(nameof(SabnzbdController.Version));
// Act
var attribute = method?.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).FirstOrDefault();
// Assert
Assert.NotNull(attribute);
}
[Fact]
public void GetVersion_ReturnsOk()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.UserNamePassword;
// Act
var result = _controller.Version();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.Equal("4.4.0", response.Version);
}
[Fact]
public void GetConfig_ReturnsOk()
{
// Arrange
var config = new SabnzbdConfig { Misc = new SabnzbdMisc { Port = "6500" } };
_sabnzbdMock.Setup(s => s.GetConfig()).Returns(config);
// Act
var result = _controller.GetConfig();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.NotNull(response.Config);
Assert.Equal("6500", response.Config.Misc.Port);
}
[Fact]
public void GetCategories_ReturnsOk()
{
// Arrange
var categories = new List<String> { "*", "Default" };
_sabnzbdMock.Setup(s => s.GetCategories()).Returns(categories);
// Act
var result = _controller.GetCats();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.NotNull(response.Categories);
Assert.Equal(2, response.Categories.Count);
Assert.Equal("*", response.Categories[0]);
}
[Fact]
public void Get_NoMode_ReturnsBadRequest()
{
// Act
var result = _controller.Get(null);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(badRequestResult.Value);
Assert.Equal("No mode specified", response.Error);
}
[Fact]
public void Get_UnknownMode_ReturnsNotFound()
{
// Act
var result = _controller.Get("unknown");
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public void Get_ModeInForm_ReturnsNotFound()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
httpContext.Request.Form = new FormCollection(new Dictionary<String, Microsoft.Extensions.Primitives.StringValues>
{
{ "mode", "unknown_form" }
});
_controller.ControllerContext.HttpContext = httpContext;
// Act
var result = _controller.Get(null);
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public async Task AddFile_WithQueryParameters_SetsCategoryAndPriority()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
httpContext.Request.QueryString = new QueryString("?cat=radarr&priority=-100");
// Mocking multipart form data
var fileMock = new Mock<IFormFile>();
var content = "test content";
var fileName = "test.nzb";
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
writer.Write(content);
writer.Flush();
ms.Position = 0;
fileMock.Setup(_ => _.OpenReadStream()).Returns(ms);
fileMock.Setup(_ => _.FileName).Returns(fileName);
fileMock.Setup(_ => _.Length).Returns(ms.Length);
fileMock.Setup(_ => _.CopyToAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.Callback<Stream, CancellationToken>((s, _) => ms.CopyTo(s))
.Returns(Task.CompletedTask);
httpContext.Request.ContentType = "multipart/form-data; boundary=something";
httpContext.Request.Form = new FormCollection(new Dictionary<String, Microsoft.Extensions.Primitives.StringValues>(), new FormFileCollection { fileMock.Object });
_controller.ControllerContext.HttpContext = httpContext;
_sabnzbdMock.Setup(s => s.AddFile(It.IsAny<Byte[]>(), fileName, "radarr", -100)).ReturnsAsync("nzo_id_123");
// Act
var result = await _controller.AddFile();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.True(response.Status);
Assert.Contains("nzo_id_123", response.NzoIds!);
_sabnzbdMock.Verify(s => s.AddFile(It.IsAny<Byte[]>(), fileName, "radarr", -100), Times.Once);
}
}

View file

@ -0,0 +1,118 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using RdtClient.Data.Enums;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
namespace RdtClient.Web.Test.Controllers;
public class SabnzbdHandlerTest
{
private readonly Mock<Authentication> _authenticationMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessorMock;
private readonly SabnzbdHandler _handler;
public SabnzbdHandlerTest()
{
_authenticationMock = new Mock<Authentication>(null!, null!, null!);
_httpContextAccessorMock = new Mock<IHttpContextAccessor>();
_handler = new SabnzbdHandler(_authenticationMock.Object, _httpContextAccessorMock.Object);
Data.Data.SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
}
[Fact]
public async Task HandleAsync_AuthNone_Succeeds()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = AuthenticationType.None;
var context = CreateContext();
// Act
await _handler.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded, "HasSucceeded should be true because AuthenticationType is None");
}
[Fact]
public async Task HandleAsync_ValidCredentials_Succeeds()
{
// Arrange
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new QueryString("?ma_username=user&ma_password=pass");
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
var context = CreateContext(httpContext);
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid credentials");
}
[Fact]
public async Task HandleAsync_AlreadyAuthenticated_Succeeds()
{
// Arrange
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity("TestAuth"));
httpContext.User = claimsPrincipal;
var context = CreateContext(httpContext);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded, "HasSucceeded should be true for already authenticated user");
_authenticationMock.Verify(a => a.Login(It.IsAny<String>(), It.IsAny<String>()), Times.Never);
}
[Fact]
public async Task HandleAsync_InvalidCredentials_DoesNotSucceed()
{
// Arrange
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new QueryString("?ma_username=user&ma_password=wrong");
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
var context = CreateContext(httpContext);
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Failed);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid credentials");
}
[Fact]
public async Task HandleAsync_MissingCredentials_DoesNotSucceed()
{
// Arrange
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
var context = CreateContext(httpContext);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded, "HasSucceeded should be false when credentials are missing");
}
private AuthorizationHandlerContext CreateContext(HttpContext? httpContext = null)
{
var requirement = new SabnzbdRequirement();
var user = httpContext?.User ?? new System.Security.Claims.ClaimsPrincipal();
return new AuthorizationHandlerContext([requirement], user, null);
}
}

View file

@ -0,0 +1,111 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Services;
using RdtClient.Web.Controllers;
namespace RdtClient.Web.Test.Controllers;
public class TorrentsControllerNzbTest
{
private readonly Mock<Torrents> _torrentsMock;
private readonly Mock<ILogger<TorrentsController>> _loggerMock;
private readonly TorrentsController _controller;
public TorrentsControllerNzbTest()
{
_torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
_loggerMock = new Mock<ILogger<TorrentsController>>();
_controller = new TorrentsController(_loggerMock.Object, _torrentsMock.Object, null!);
}
[Fact]
public async Task UploadNzbLink_ValidRequest_ReturnsOk()
{
// Arrange
var request = new TorrentControllerUploadNzbLinkRequest
{
NzbLink = "http://example.com/test.nzb",
Torrent = new Torrent()
};
// Act
var result = await _controller.UploadNzbLink(request);
// Assert
Assert.IsType<OkResult>(result);
_torrentsMock.Verify(t => t.AddNzbLinkToDebridQueue(request.NzbLink, request.Torrent), Times.Once);
}
[Fact]
public async Task UploadNzbLink_NullRequest_ReturnsBadRequest()
{
// Act
var result = await _controller.UploadNzbLink(null);
// Assert
Assert.IsType<BadRequestResult>(result);
}
[Fact]
public async Task UploadNzbLink_EmptyLink_ReturnsBadRequest()
{
// Arrange
var request = new TorrentControllerUploadNzbLinkRequest
{
NzbLink = "",
Torrent = new Torrent()
};
// Act
var result = await _controller.UploadNzbLink(request);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal("Invalid nzb link", badRequest.Value);
}
[Fact]
public async Task UploadNzbFile_ValidRequest_ReturnsOk()
{
// Arrange
var fileMock = new Mock<IFormFile>();
var content = "nzb content";
var fileName = "test.nzb";
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
writer.Write(content);
writer.Flush();
ms.Position = 0;
fileMock.Setup(_ => _.OpenReadStream()).Returns(ms);
fileMock.Setup(_ => _.FileName).Returns(fileName);
fileMock.Setup(_ => _.Length).Returns(ms.Length);
var formData = new TorrentControllerUploadFileRequest
{
Torrent = new Torrent()
};
// Act
var result = await _controller.UploadNzbFile(fileMock.Object, formData);
// Assert
Assert.IsType<OkResult>(result);
_torrentsMock.Verify(t => t.AddNzbFileToDebridQueue(It.IsAny<Byte[]>(), fileName, formData.Torrent), Times.Once);
Assert.Equal(fileName, formData.Torrent.RdName);
}
[Fact]
public async Task UploadNzbFile_NoFile_ReturnsBadRequest()
{
// Act
var result = await _controller.UploadNzbFile(null, new TorrentControllerUploadFileRequest());
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal("Invalid nzb file", badRequest.Value);
}
}

View file

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<AssemblyName>RdtClient.Web.Test</AssemblyName>
<RootNamespace>RdtClient.Web.Test</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RdtClient.Web\RdtClient.Web.csproj" />
<ProjectReference Include="..\RdtClient.Service\RdtClient.Service.csproj" />
<ProjectReference Include="..\RdtClient.Data\RdtClient.Data.csproj" />
</ItemGroup>
</Project>

View file

@ -3,10 +3,11 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "3.1.3", "version": "9.0.9",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ],
"rollForward": false
} }
} }
} }

View file

@ -13,6 +13,7 @@ namespace RdtClient.Web.Controllers;
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/v2")] [Route("api/v2")]
[Route("qbittorrent/api/v2")]
public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent) : Controller public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent) : Controller
{ {
[AllowAnonymous] [AllowAnonymous]

View file

@ -0,0 +1,151 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RdtClient.Data.Models.Sabnzbd;
using RdtClient.Service.Services;
namespace RdtClient.Web.Controllers;
[ApiController]
[Route("api")]
[Route("sabnzbd/api")]
[Authorize(Policy = "Sabnzbd")]
public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzbd) : Controller
{
[AllowAnonymous]
[HttpGet]
[HttpPost]
public ActionResult Get([FromQuery] String? mode)
{
if (Request.HasFormContentType)
{
mode ??= Request.Form["mode"].ToString();
}
if (String.IsNullOrWhiteSpace(mode))
{
return BadRequest(new SabnzbdResponse { Error = "No mode specified" });
}
logger.LogWarning($"Sabnzbd API called (not implemented) - Method: {Request.Method}, Query: {Request.QueryString}");
return NotFound(new SabnzbdResponse());
}
[AllowAnonymous]
[HttpGet]
[SabnzbdMode("version")]
public ActionResult Version()
{
logger.LogDebug("Sabnzbd mode: version");
return Ok(new SabnzbdResponse { Version = "4.4.0" });
}
[HttpGet]
[SabnzbdMode("queue")]
public async Task<ActionResult> Queue()
{
logger.LogDebug("Sabnzbd mode: queue");
var name = GetParam("name");
if (name == "delete")
{
var value = GetParam("value");
if (String.IsNullOrWhiteSpace(value))
{
return BadRequest(new SabnzbdResponse
{
Error = "No value specified for delete operation"
});
}
await sabnzbd.Delete(value ?? "");
return Ok(new SabnzbdResponse { Status = true });
}
return Ok(new SabnzbdResponse { Queue = await sabnzbd.GetQueue() });
}
[HttpGet]
[SabnzbdMode("history")]
public async Task<ActionResult> History()
{
logger.LogDebug("Sabnzbd mode: history");
return Ok(new SabnzbdResponse { History = await sabnzbd.GetHistory() });
}
[HttpGet]
[SabnzbdMode("get_config")]
public ActionResult GetConfig()
{
logger.LogDebug("Sabnzbd mode: get_config");
return Ok(new SabnzbdResponse { Config = sabnzbd.GetConfig() });
}
[HttpGet]
[SabnzbdMode("get_cats")]
public ActionResult GetCats()
{
logger.LogDebug("Sabnzbd mode: get_cats");
return Ok(new SabnzbdResponse { Categories = sabnzbd.GetCategories() });
}
[HttpGet]
[HttpPost]
[SabnzbdMode("addurl")]
public async Task<ActionResult> AddUrl()
{
logger.LogDebug("Sabnzbd mode: addurl");
var url = GetParam("name");
var category = GetParam("cat");
var priorityStr = GetParam("priority");
Int32? priority = Int32.TryParse(priorityStr, out var p) ? p : null;
var result = await sabnzbd.AddUrl(url ?? "", category, priority);
return Ok(new SabnzbdResponse { Status = true, NzoIds = [result] });
}
[HttpPost]
[SabnzbdMode("addfile")]
public async Task<ActionResult> AddFile()
{
logger.LogDebug("Sabnzbd mode: addfile");
if (!Request.HasFormContentType)
{
return BadRequest("Expected multipart/form-data");
}
var file = Request.Form.Files.FirstOrDefault();
if (file == null)
{
return BadRequest("No file uploaded");
}
var category = GetParam("cat");
var priorityStr = GetParam("priority");
Int32? priority = Int32.TryParse(priorityStr, out var p) ? p : null;
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
var result = await sabnzbd.AddFile(ms.ToArray(), file.FileName, category, priority);
return Ok(new SabnzbdResponse { Status = true, NzoIds = [result] });
}
[HttpGet]
[SabnzbdMode("fullstatus")]
public async Task<ActionResult> FullStatus()
{
logger.LogDebug("Sabnzbd mode: fullstatus");
return Ok(new SabnzbdResponse { Version = "4.4.0", Queue = await sabnzbd.GetQueue() });
}
private String? GetParam(String name)
{
var value = Request.Query[name].ToString();
if (String.IsNullOrWhiteSpace(value) && Request.HasFormContentType)
{
value = Request.Form[name].ToString();
}
return value;
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc.ActionConstraints;
namespace RdtClient.Web.Controllers;
[AttributeUsage(AttributeTargets.Method)]
public class SabnzbdModeAttribute(String mode) : Attribute, IActionConstraint
{
public Int32 Order => 0;
public Boolean Accept(ActionConstraintContext context)
{
var request = context.RouteContext.HttpContext.Request;
String? modeValue = request.Query["mode"];
if (String.IsNullOrWhiteSpace(modeValue) && request.HasFormContentType)
{
modeValue = request.Form["mode"];
}
return String.Equals(modeValue, mode, StringComparison.OrdinalIgnoreCase);
}
}

View file

@ -2,8 +2,8 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MonoTorrent; using MonoTorrent;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.BackgroundServices; using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services; using RdtClient.Service.Services;
@ -22,66 +22,65 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
var results = await torrents.Get(); var results = await torrents.Get();
var torrentDtos = results.Select(torrent => new TorrentDto var torrentDtos = results.Select(torrent => new TorrentDto
{ {
TorrentId = torrent.TorrentId, TorrentId = torrent.TorrentId,
Hash = torrent.Hash, Hash = torrent.Hash,
Category = torrent.Category, Category = torrent.Category,
DownloadAction = torrent.DownloadAction, DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction, FinishedAction = torrent.FinishedAction,
FinishedActionDelay = torrent.FinishedActionDelay, FinishedActionDelay = torrent.FinishedActionDelay,
HostDownloadAction = torrent.HostDownloadAction, HostDownloadAction = torrent.HostDownloadAction,
DownloadMinSize = torrent.DownloadMinSize, DownloadMinSize = torrent.DownloadMinSize,
IncludeRegex = torrent.IncludeRegex, IncludeRegex = torrent.IncludeRegex,
ExcludeRegex = torrent.ExcludeRegex, ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles, DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = torrent.DownloadClient, DownloadClient = torrent.DownloadClient,
Added = torrent.Added, Added = torrent.Added,
FilesSelected = torrent.FilesSelected, FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed, Completed = torrent.Completed,
IsFile = torrent.IsFile, Type = torrent.Type,
Priority = torrent.Priority, IsFile = torrent.IsFile,
RetryCount = torrent.RetryCount, Priority = torrent.Priority,
DownloadRetryAttempts = torrent.DownloadRetryAttempts, RetryCount = torrent.RetryCount,
TorrentRetryAttempts = torrent.TorrentRetryAttempts, DownloadRetryAttempts = torrent.DownloadRetryAttempts,
DeleteOnError = torrent.DeleteOnError, TorrentRetryAttempts = torrent.TorrentRetryAttempts,
Lifetime = torrent.Lifetime, DeleteOnError = torrent.DeleteOnError,
Error = torrent.Error, Lifetime = torrent.Lifetime,
RdId = torrent.RdId, Error = torrent.Error,
RdName = torrent.RdName, RdId = torrent.RdId,
RdSize = torrent.RdSize, RdName = torrent.RdName,
RdHost = torrent.RdHost, RdSize = torrent.RdSize,
RdSplit = torrent.RdSplit, RdHost = torrent.RdHost,
RdProgress = torrent.RdProgress, RdSplit = torrent.RdSplit,
RdStatus = torrent.RdStatus, RdProgress = torrent.RdProgress,
RdStatusRaw = torrent.RdStatusRaw, RdStatus = torrent.RdStatus,
RdAdded = torrent.RdAdded, RdStatusRaw = torrent.RdStatusRaw,
RdEnded = torrent.RdEnded, RdAdded = torrent.RdAdded,
RdSpeed = torrent.RdSpeed, RdEnded = torrent.RdEnded,
RdSeeders = torrent.RdSeeders, RdSpeed = torrent.RdSpeed,
Files = torrent.Files, RdSeeders = torrent.RdSeeders,
Downloads = torrent.Downloads.Select(download => new DownloadDto Files = torrent.Files,
{ Downloads = torrent.Downloads.Select(download => new DownloadDto
DownloadId = download.DownloadId, {
TorrentId = download.TorrentId, DownloadId = download.DownloadId,
Path = download.Path, TorrentId = download.TorrentId,
Link = download.Link, Path = download.Path,
Added = download.Added, Link = download.Link,
DownloadQueued = download.DownloadQueued, Added = download.Added,
DownloadStarted = download.DownloadStarted, DownloadQueued = download.DownloadQueued,
DownloadFinished = download.DownloadFinished, DownloadStarted = download.DownloadStarted,
UnpackingQueued = download.UnpackingQueued, DownloadFinished = download.DownloadFinished,
UnpackingStarted = download.UnpackingStarted, UnpackingQueued = download.UnpackingQueued,
UnpackingFinished = download.UnpackingFinished, UnpackingStarted = download.UnpackingStarted,
Completed = download.Completed, UnpackingFinished = download.UnpackingFinished,
RetryCount = download.RetryCount, Completed = download.Completed,
Error = download.Error, RetryCount = download.RetryCount,
BytesTotal = download.BytesTotal, Error = download.Error,
BytesDone = download.BytesDone, BytesTotal = download.BytesTotal,
Speed = download.Speed BytesDone = download.BytesDone,
}) Speed = download.Speed
.ToList() }).ToList()
}) }).ToList();
.ToList();
return Ok(torrentDtos); return Ok(torrentDtos);
} }
@ -119,6 +118,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
Added = torrent.Added, Added = torrent.Added,
FilesSelected = torrent.FilesSelected, FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed, Completed = torrent.Completed,
Type = torrent.Type,
IsFile = torrent.IsFile, IsFile = torrent.IsFile,
Priority = torrent.Priority, Priority = torrent.Priority,
RetryCount = torrent.RetryCount, RetryCount = torrent.RetryCount,
@ -141,26 +141,25 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
RdSeeders = torrent.RdSeeders, RdSeeders = torrent.RdSeeders,
Files = torrent.Files, Files = torrent.Files,
Downloads = torrent.Downloads.Select(download => new DownloadDto Downloads = torrent.Downloads.Select(download => new DownloadDto
{ {
DownloadId = download.DownloadId, DownloadId = download.DownloadId,
TorrentId = download.TorrentId, TorrentId = download.TorrentId,
Path = download.Path, Path = download.Path,
Link = download.Link, Link = download.Link,
Added = download.Added, Added = download.Added,
DownloadQueued = download.DownloadQueued, DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted, DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished, DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued, UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted, UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished, UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed, Completed = download.Completed,
RetryCount = download.RetryCount, RetryCount = download.RetryCount,
Error = download.Error, Error = download.Error,
BytesTotal = download.BytesTotal, BytesTotal = download.BytesTotal,
BytesDone = download.BytesDone, BytesDone = download.BytesDone,
Speed = download.Speed Speed = download.Speed
}) }).ToList()
.ToList()
}; };
return Ok(torrentDto); return Ok(torrentDto);
@ -170,8 +169,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
[Route("DiskSpaceStatus")] [Route("DiskSpaceStatus")]
public ActionResult<DiskSpaceStatus?> GetDiskSpaceStatus() public ActionResult<DiskSpaceStatus?> GetDiskSpaceStatus()
{ {
var status = DiskSpaceMonitor.GetCurrentStatus(); var status = Service.BackgroundServices.DiskSpaceMonitor.GetCurrentStatus();
return Ok(status); return Ok(status);
} }
@ -245,6 +243,68 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
return Ok(); return Ok();
} }
[HttpPost]
[Route("UploadNzbFile")]
public async Task<ActionResult> UploadNzbFile([FromForm] IFormFile? file,
[ModelBinder(BinderType = typeof(JsonModelBinder))]
TorrentControllerUploadFileRequest? formData)
{
if (file == null || file.Length <= 0)
{
return BadRequest("Invalid nzb file");
}
if (formData?.Torrent == null)
{
return BadRequest("Invalid Torrent");
}
logger.LogDebug($"Add nzb file");
if (String.IsNullOrWhiteSpace(formData.Torrent.RdName))
{
formData.Torrent.RdName = file.FileName;
}
var fileStream = file.OpenReadStream();
await using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();
await torrents.AddNzbFileToDebridQueue(bytes, file.FileName, formData.Torrent);
return Ok();
}
[HttpPost]
[Route("UploadNzbLink")]
public async Task<ActionResult> UploadNzbLink([FromBody] TorrentControllerUploadNzbLinkRequest? request)
{
if (request == null)
{
return BadRequest();
}
if (String.IsNullOrEmpty(request.NzbLink))
{
return BadRequest("Invalid nzb link");
}
if (request.Torrent == null)
{
return BadRequest("Invalid Torrent");
}
logger.LogDebug($"Add nzb link {request.NzbLink}");
await torrents.AddNzbLinkToDebridQueue(request.NzbLink, request.Torrent);
return Ok();
}
[HttpPost] [HttpPost]
[Route("CheckFiles")] [Route("CheckFiles")]
public async Task<ActionResult> CheckFiles([FromForm] IFormFile? file) public async Task<ActionResult> CheckFiles([FromForm] IFormFile? file)
@ -355,7 +415,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
var includeError = ""; var includeError = "";
var excludeError = ""; var excludeError = "";
IList<TorrentClientAvailableFile> availableFiles; IList<DebridClientAvailableFile> availableFiles;
if (!String.IsNullOrWhiteSpace(request.MagnetLink)) if (!String.IsNullOrWhiteSpace(request.MagnetLink))
{ {
@ -382,7 +442,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
return BadRequest(); return BadRequest();
} }
var selectedFiles = new List<TorrentClientAvailableFile>(); var selectedFiles = new List<DebridClientAvailableFile>();
if (!String.IsNullOrWhiteSpace(request.IncludeRegex)) if (!String.IsNullOrWhiteSpace(request.IncludeRegex))
{ {
@ -443,6 +503,12 @@ public class TorrentControllerUploadMagnetRequest
public Torrent? Torrent { get; set; } public Torrent? Torrent { get; set; }
} }
public class TorrentControllerUploadNzbLinkRequest
{
public String? NzbLink { get; set; }
public Torrent? Torrent { get; set; }
}
public class TorrentControllerDeleteRequest public class TorrentControllerDeleteRequest
{ {
public Boolean DeleteData { get; set; } public Boolean DeleteData { get; set; }

View file

@ -7,6 +7,7 @@ using RdtClient.Data.Models.Internal;
using RdtClient.Service; using RdtClient.Service;
using RdtClient.Service.Middleware; using RdtClient.Service.Middleware;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using RdtClient.Service.Helpers;
using Serilog; using Serilog;
using Serilog.Debugging; using Serilog.Debugging;
using Serilog.Events; using Serilog.Events;
@ -41,6 +42,7 @@ builder.WebHost.ConfigureKestrel(options =>
if (appSettings.Logging?.File?.Path != null) if (appSettings.Logging?.File?.Path != null)
{ {
builder.Host.UseSerilog((_, lc) => lc.Enrich.FromLogContext() builder.Host.UseSerilog((_, lc) => lc.Enrich.FromLogContext()
.Enrich.With<CredentialRedactorEnricher>()
.WriteTo.File(appSettings.Logging.File.Path, .WriteTo.File(appSettings.Logging.File.Path,
rollOnFileSizeLimit: true, rollOnFileSizeLimit: true,
fileSizeLimitBytes: appSettings.Logging.File.FileSizeLimitBytes, fileSizeLimitBytes: appSettings.Logging.File.FileSizeLimitBytes,
@ -72,11 +74,8 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
}); });
builder.Services.AddAuthorizationBuilder() builder.Services.AddAuthorizationBuilder()
.AddPolicy("AuthSetting", .AddPolicy("AuthSetting", policyCorrectUser => { policyCorrectUser.Requirements.Add(new AuthSettingRequirement()); })
policyCorrectUser => .AddPolicy("Sabnzbd", policyCorrectUser => { policyCorrectUser.Requirements.Add(new SabnzbdRequirement()); });
{
policyCorrectUser.Requirements.Add(new AuthSettingRequirement());
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{ {

View file

@ -11,28 +11,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Data", "RdtClient
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Service.Test", "RdtClient.Service.Test\RdtClient.Service.Test.csproj", "{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Service.Test", "RdtClient.Service.Test\RdtClient.Service.Test.csproj", "{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Web.Test", "RdtClient.Web.Test\RdtClient.Web.Test.csproj", "{C9517091-EC0A-4146-85BA-58D95D0C597E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|x64.ActiveCfg = Debug|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|x64.Build.0 = Debug|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|x86.ActiveCfg = Debug|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Debug|x86.Build.0 = Debug|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|Any CPU.Build.0 = Release|Any CPU {A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|Any CPU.Build.0 = Release|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|x64.ActiveCfg = Release|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|x64.Build.0 = Release|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|x86.ActiveCfg = Release|Any CPU
{A8C7F095-89C6-4CD1-AFB2-27106F470D62}.Release|x86.Build.0 = Release|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|Any CPU.Build.0 = Debug|Any CPU {06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|x64.ActiveCfg = Debug|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|x64.Build.0 = Debug|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|x86.ActiveCfg = Debug|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Debug|x86.Build.0 = Debug|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Release|Any CPU.ActiveCfg = Release|Any CPU {06D27710-AE9A-4FA2-B043-1A4303561716}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Release|Any CPU.Build.0 = Release|Any CPU {06D27710-AE9A-4FA2-B043-1A4303561716}.Release|Any CPU.Build.0 = Release|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Release|x64.ActiveCfg = Release|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Release|x64.Build.0 = Release|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Release|x86.ActiveCfg = Release|Any CPU
{06D27710-AE9A-4FA2-B043-1A4303561716}.Release|x86.Build.0 = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.Build.0 = Debug|Any CPU {92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|x64.ActiveCfg = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|x64.Build.0 = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|x86.ActiveCfg = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|x86.Build.0 = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.ActiveCfg = Release|Any CPU {92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.Build.0 = Release|Any CPU {92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.Build.0 = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|x64.ActiveCfg = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|x64.Build.0 = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|x86.ActiveCfg = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|x86.Build.0 = Release|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|x64.ActiveCfg = Debug|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|x64.Build.0 = Debug|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|x86.ActiveCfg = Debug|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|x86.Build.0 = Debug|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|Any CPU.Build.0 = Release|Any CPU {4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|Any CPU.Build.0 = Release|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|x64.ActiveCfg = Release|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|x64.Build.0 = Release|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|x86.ActiveCfg = Release|Any CPU
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|x86.Build.0 = Release|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Debug|x64.ActiveCfg = Debug|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Debug|x64.Build.0 = Debug|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Debug|x86.ActiveCfg = Debug|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Debug|x86.Build.0 = Debug|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Release|Any CPU.Build.0 = Release|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Release|x64.ActiveCfg = Release|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Release|x64.Build.0 = Release|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Release|x86.ActiveCfg = Release|Any CPU
{C9517091-EC0A-4146-85BA-58D95D0C597E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE