Merge pull request #911 from omgbeez/usenet
Adding usenet support (Torbox only implementation)
This commit is contained in:
commit
d0242f235c
71 changed files with 4784 additions and 868 deletions
|
|
@ -1,5 +1,24 @@
|
|||
<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">
|
||||
<label class="label">Torrent file</label>
|
||||
<div class="file has-name">
|
||||
|
|
@ -31,15 +50,53 @@
|
|||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [hidden]="type !== 'nzb'">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
|
||||
<span>Add Torrent</span>
|
||||
</button>
|
||||
<label class="label">NZB file</label>
|
||||
<div class="file has-name">
|
||||
<label class="file-label">
|
||||
<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 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="field">
|
||||
|
|
@ -53,7 +110,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -66,7 +123,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -142,10 +199,10 @@
|
|||
<select [(ngModel)]="finishedAction">
|
||||
<option [ngValue]="0">Do nothing</option>
|
||||
@if (downloadClient !== 2) {
|
||||
<option [ngValue]="1">Remove torrent from provider and client</option>
|
||||
<option [ngValue]="2">Remove torrent from provider</option>
|
||||
<option [ngValue]="1">Remove from provider and client</option>
|
||||
<option [ngValue]="2">Remove from provider</option>
|
||||
}
|
||||
<option [ngValue]="3">Remove torrent from client</option>
|
||||
<option [ngValue]="3">Remove from client</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -156,7 +213,7 @@
|
|||
<input class="input" type="number" [(ngModel)]="finishedActionDelay" />
|
||||
</div>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -175,9 +232,14 @@
|
|||
<input class="input" type="number" step="1" [(ngModel)]="priority" />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div>
|
||||
<div class="field">
|
||||
<label class="label">Automatic retry downloads</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Automatic retry torrent</label>
|
||||
<label class="label">Automatic retry download</label>
|
||||
<div class="control">
|
||||
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="torrentRetryAttempts" />
|
||||
</div>
|
||||
<p class="help">
|
||||
When a single download has failed multiple times (see setting above) or when the torrent itself received an
|
||||
error it will retry the full torrent this many times before marking it failed.
|
||||
When a single download has failed multiple times (see setting above) or when the download itself received an
|
||||
error it will retry the full download this many times before marking it failed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
@ -206,13 +268,13 @@
|
|||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Torrent maximum lifetime</label>
|
||||
<label class="label">Maximum lifetime</label>
|
||||
<div class="control">
|
||||
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" />
|
||||
</div>
|
||||
<p class="help">
|
||||
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the
|
||||
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.
|
||||
The maximum lifetime of a download in minutes. When this time has passed, mark it as error. If the
|
||||
download is completed, the lifetime setting will not apply. 0 to disable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -226,10 +288,3 @@
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TorrentService } from 'src/app/torrent.service';
|
||||
import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||
import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
|
@ -15,8 +15,10 @@ import { NgClass } from '@angular/common';
|
|||
standalone: true,
|
||||
})
|
||||
export class AddNewTorrentComponent implements OnInit {
|
||||
public type: 'torrent' | 'nzb' = 'torrent';
|
||||
public fileName: string;
|
||||
public magnetLink: string;
|
||||
public nzbLink: string;
|
||||
private currentTorrentFile: string;
|
||||
|
||||
public provider: string;
|
||||
|
|
@ -58,8 +60,19 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.queryParams.subscribe((params) => {
|
||||
if (params['type'] === 'nzb') {
|
||||
this.type = 'nzb';
|
||||
} else if (params['type'] === 'torrent') {
|
||||
this.type = 'torrent';
|
||||
}
|
||||
|
||||
if (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) => {
|
||||
|
|
@ -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 {
|
||||
const files = (evt.target as HTMLInputElement).files;
|
||||
|
||||
|
|
@ -152,25 +171,49 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
torrent.lifetime = this.torrentLifetime;
|
||||
torrent.downloadClient = this.downloadClient;
|
||||
|
||||
if (this.magnetLink) {
|
||||
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
});
|
||||
} else if (this.selectedFile) {
|
||||
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
});
|
||||
if (this.type === 'torrent') {
|
||||
torrent.type = DownloadType.Torrent;
|
||||
if (this.magnetLink) {
|
||||
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
});
|
||||
} else if (this.selectedFile) {
|
||||
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({
|
||||
next: () => this.router.navigate(['/']),
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
this.saving = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.error = 'No magnet or file uploaded';
|
||||
this.saving = false;
|
||||
}
|
||||
} else {
|
||||
this.error = 'No magnet or file uploaded';
|
||||
this.saving = false;
|
||||
if (this.nzbLink) {
|
||||
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 {
|
||||
if (this.type === 'nzb') {
|
||||
return;
|
||||
}
|
||||
if (this.magnetLink && this.magnetLink === this.currentTorrentFile) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export class Torrent {
|
|||
public lifetime: number;
|
||||
|
||||
public priority: number;
|
||||
public type: DownloadType;
|
||||
public error: string;
|
||||
|
||||
public rdId: string;
|
||||
|
|
@ -73,3 +74,8 @@ export enum RealDebridStatus {
|
|||
|
||||
Error = 99,
|
||||
}
|
||||
|
||||
export enum DownloadType {
|
||||
Torrent = 0,
|
||||
Nzb = 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@
|
|||
<span class="icon">
|
||||
<i class="fas fa-table" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>Torrents</span>
|
||||
<span>Torrents/NZBs</span>
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="/add">
|
||||
<a class="navbar-item" routerLink="/add" [queryParams]="{ type: 'torrent' }">
|
||||
<span class="icon">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>Add New Torrent</span>
|
||||
<span>Add Torrent/NZB</span>
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="/settings">
|
||||
<span class="icon">
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export class TorrentStatusPipe implements PipeTransform {
|
|||
case RealDebridStatus.Queued:
|
||||
return 'Not Yet Added to Provider';
|
||||
case RealDebridStatus.Downloading:
|
||||
if (torrent.rdSeeders < 1) {
|
||||
if (torrent.rdSeeders < 1 && torrent.type !== 1) {
|
||||
return `Torrent stalled`;
|
||||
}
|
||||
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
|
||||
|
|
|
|||
|
|
@ -79,6 +79,20 @@ export class TorrentService {
|
|||
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[]> {
|
||||
return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFilesMagnet`, {
|
||||
magnetLink,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ public interface ITorrentData
|
|||
String hash,
|
||||
String? fileOrMagnetContents,
|
||||
Boolean isFile,
|
||||
DownloadType downloadType,
|
||||
DownloadClient downloadClient,
|
||||
Torrent torrent);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace RdtClient.Data.Data;
|
|||
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ public class TorrentData(DataContext dataContext) : ITorrentData
|
|||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(m => m.Downloads)
|
||||
.OrderBy(m => m.Priority ?? 9999)
|
||||
.ThenBy(m => m.Added)
|
||||
.ToListAsync();
|
||||
|
||||
return torrents;
|
||||
return torrents.OrderBy(m => m.Priority ?? 9999)
|
||||
.ThenBy(m => m.Added)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<Torrent?> GetById(Guid torrentId)
|
||||
|
|
@ -65,6 +65,7 @@ public class TorrentData(DataContext dataContext) : ITorrentData
|
|||
String hash,
|
||||
String? fileOrMagnetContents,
|
||||
Boolean isFile,
|
||||
DownloadType downloadType,
|
||||
DownloadClient downloadClient,
|
||||
Torrent torrent)
|
||||
{
|
||||
|
|
@ -84,6 +85,7 @@ public class TorrentData(DataContext dataContext) : ITorrentData
|
|||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = downloadClient,
|
||||
Type = downloadType,
|
||||
FileOrMagnet = fileOrMagnetContents,
|
||||
IsFile = isFile,
|
||||
Priority = torrent.Priority,
|
||||
|
|
@ -141,6 +143,20 @@ public class TorrentData(DataContext dataContext) : ITorrentData
|
|||
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)
|
||||
{
|
||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
||||
|
|
|
|||
7
server/RdtClient.Data/Enums/DownloadType.cs
Normal file
7
server/RdtClient.Data/Enums/DownloadType.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace RdtClient.Data.Enums;
|
||||
|
||||
public enum DownloadType
|
||||
{
|
||||
Torrent = 0,
|
||||
Nzb = 1
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
public enum TorrentStatus
|
||||
{
|
||||
// Queued by RDTClient, not the provider.
|
||||
Queued = 0,
|
||||
|
||||
Processing = 1,
|
||||
|
|
|
|||
485
server/RdtClient.Data/Migrations/20251224022148_AddDownloadTypeToTorrent.Designer.cs
generated
Normal file
485
server/RdtClient.Data/Migrations/20251224022148_AddDownloadTypeToTorrent.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -402,6 +402,9 @@ namespace RdtClient.Data.Migrations
|
|||
b.Property<int>("TorrentRetryAttempts")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("TorrentId");
|
||||
|
||||
b.ToTable("Torrents");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
|
||||
namespace RdtClient.Data.Models.Data;
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ public class Torrent
|
|||
public DateTimeOffset? Completed { get; set; }
|
||||
public DateTimeOffset? Retry { get; set; }
|
||||
|
||||
public DownloadType Type { get; set; }
|
||||
public String? FileOrMagnet { get; set; }
|
||||
public Boolean IsFile { get; set; }
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ public class Torrent
|
|||
public String? RdFiles { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public IList<TorrentClientFile> Files
|
||||
public IList<DebridClientFile> Files
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -72,7 +73,7 @@ public class Torrent
|
|||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<TorrentClientFile>>(RdFiles) ?? [];
|
||||
return JsonSerializer.Deserialize<List<DebridClientFile>>(RdFiles) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
||||
|
|
@ -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 String Path { get; set; } = default!;
|
||||
|
|
@ -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 Filename { get; set; } = default!;
|
||||
|
|
@ -15,7 +17,9 @@ public class TorrentClientTorrent
|
|||
public String? Message { get; set; }
|
||||
public Int64 StatusCode { 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 DateTimeOffset? Ended { get; set; }
|
||||
public Int64? Speed { get; set; }
|
||||
|
|
@ -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 DateTimeOffset? Expiration { get; set; }
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -263,6 +264,7 @@ public class DbSettingsWatch
|
|||
|
||||
[DisplayName("Check Interval")]
|
||||
[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;
|
||||
|
||||
[DisplayName("Import Defaults")]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
|
||||
namespace RdtClient.Data.Models.Internal;
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ public class TorrentDto
|
|||
public DateTimeOffset Added { get; set; }
|
||||
public DateTimeOffset? FilesSelected { get; set; }
|
||||
public DateTimeOffset? Completed { get; set; }
|
||||
public DownloadType Type { get; set; }
|
||||
public Boolean IsFile { get; set; }
|
||||
public Int32? Priority { get; set; }
|
||||
public Int32 RetryCount { get; set; }
|
||||
|
|
@ -40,7 +41,7 @@ public class TorrentDto
|
|||
public DateTimeOffset? RdEnded { get; set; }
|
||||
public Int64? RdSpeed { 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; } = [];
|
||||
}
|
||||
|
||||
|
|
|
|||
24
server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs
Normal file
24
server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs
Normal 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";
|
||||
}
|
||||
15
server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs
Normal file
15
server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs
Normal 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();
|
||||
}
|
||||
15
server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs
Normal file
15
server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs
Normal 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();
|
||||
}
|
||||
24
server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs
Normal file
24
server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs
Normal 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; } = "";
|
||||
}
|
||||
18
server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs
Normal file
18
server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs
Normal 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";
|
||||
}
|
||||
27
server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs
Normal file
27
server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs
Normal 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();
|
||||
}
|
||||
36
server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs
Normal file
36
server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs
Normal 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";
|
||||
}
|
||||
30
server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs
Normal file
30
server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Text.Json;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.Helpers;
|
||||
|
||||
namespace RdtClient.Service.Test.Helpers;
|
||||
|
|
@ -183,7 +183,7 @@ public class DownloadHelperTest
|
|||
|
||||
var fileRelativePath = "inside/lots/of/subdirectories/file.txt";
|
||||
|
||||
IList<TorrentClientFile> files =
|
||||
IList<DebridClientFile> files =
|
||||
[
|
||||
new()
|
||||
{
|
||||
|
|
@ -219,7 +219,7 @@ public class DownloadHelperTest
|
|||
|
||||
var fileRelativePath = "inside/lots/of/subdirectories/file.txt";
|
||||
|
||||
IList<TorrentClientFile> files =
|
||||
IList<DebridClientFile> files =
|
||||
[
|
||||
new()
|
||||
{
|
||||
|
|
|
|||
101
server/RdtClient.Service.Test/Helpers/FileHelperTest.cs
Normal file
101
server/RdtClient.Service.Test/Helpers/FileHelperTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
<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.Wrappers" Version="22.1.0" />
|
||||
<PackageReference Include="TorBox.NET" Version="1.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
@ -35,4 +36,10 @@
|
|||
<ProjectReference Include="..\RdtClient.Service\RdtClient.Service.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="TorBoxNET">
|
||||
<HintPath>..\..\..\torbox-net\TorBoxNET\bin\Release\netstandard2.0\TorBoxNET.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
135
server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs
Normal file
135
server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
57
server/RdtClient.Service.Test/Services/QBittorrentTest.cs
Normal file
57
server/RdtClient.Service.Test/Services/QBittorrentTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
400
server/RdtClient.Service.Test/Services/SabnzbdTest.cs
Normal file
400
server/RdtClient.Service.Test/Services/SabnzbdTest.cs
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
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);
|
||||
_torrentsMock.Setup(t => t.GetDownloadStats(It.IsAny<Guid>())).Returns((0, 1000, 500));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,12 @@ using Newtonsoft.Json;
|
|||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Service.Services.TorrentClients;
|
||||
using RdtClient.Service.Services.DebridClients;
|
||||
using File = AllDebridNET.File;
|
||||
|
||||
namespace RdtClient.Service.Test.Services.TorrentClients;
|
||||
|
||||
public class AllDebridTorrentClientTest
|
||||
public class AllDebridDebridClientTest
|
||||
{
|
||||
private static readonly Magnet Magnet1HalfDownloaded = new()
|
||||
{
|
||||
|
|
@ -84,10 +84,10 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
var result = await allDebridTorrentClient.GetTorrents();
|
||||
var result = await allDebridTorrentClient.GetDownloads();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
|
@ -114,10 +114,10 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
var result = await allDebridTorrentClient.GetTorrents();
|
||||
var result = await allDebridTorrentClient.GetDownloads();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
|
@ -144,15 +144,15 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
|
||||
// `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`.
|
||||
// `.ToList()` clones it so it won't be changed by the second call
|
||||
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList();
|
||||
var secondResult = await allDebridTorrentClient.GetTorrents();
|
||||
var firstResult = (await allDebridTorrentClient.GetDownloads()).ToList();
|
||||
var secondResult = await allDebridTorrentClient.GetDownloads();
|
||||
|
||||
// Assert
|
||||
Assert.Single(firstResult);
|
||||
|
|
@ -180,15 +180,15 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
|
||||
// `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`.
|
||||
// `.ToList()` clones it so it won't be changed by the second call
|
||||
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList();
|
||||
var secondResult = await allDebridTorrentClient.GetTorrents();
|
||||
var firstResult = (await allDebridTorrentClient.GetDownloads()).ToList();
|
||||
var secondResult = await allDebridTorrentClient.GetDownloads();
|
||||
|
||||
// Assert
|
||||
Assert.Single(firstResult);
|
||||
|
|
@ -222,15 +222,15 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
|
||||
// `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`.
|
||||
// `.ToList()` clones it so it won't be changed by the second call
|
||||
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList();
|
||||
var secondResult = await allDebridTorrentClient.GetTorrents();
|
||||
var firstResult = (await allDebridTorrentClient.GetDownloads()).ToList();
|
||||
var secondResult = await allDebridTorrentClient.GetDownloads();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, firstResult.Count);
|
||||
|
|
@ -269,12 +269,12 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
|
||||
// We have to use `GetTorrents` since `Map` is private
|
||||
var result = await allDebridTorrentClient.GetTorrents();
|
||||
// We have to use `GetDownloads` since `Map` is private
|
||||
var result = await allDebridTorrentClient.GetDownloads();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedProgress, result.First().Progress);
|
||||
|
|
@ -294,7 +294,7 @@ public class AllDebridTorrentClientTest
|
|||
};
|
||||
|
||||
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
|
||||
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);
|
||||
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
|
||||
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>()))
|
||||
.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
|
||||
var result = await allDebridTorrentClient.UpdateData(torrent, null);
|
||||
|
|
@ -388,8 +388,8 @@ public class AllDebridTorrentClientTest
|
|||
Fullsync = true
|
||||
});
|
||||
|
||||
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
||||
var torrentClientTorrent = (await allDebridTorrentClient.GetTorrents()).First();
|
||||
var allDebridTorrentClient = new AllDebridDebridClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
||||
var torrentClientTorrent = (await allDebridTorrentClient.GetDownloads()).First();
|
||||
|
||||
// Act
|
||||
var result = await allDebridTorrentClient.UpdateData(torrent, torrentClientTorrent);
|
||||
|
|
@ -415,7 +415,7 @@ public class AllDebridTorrentClientTest
|
|||
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
|
||||
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, "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
|
||||
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.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
|
||||
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
|
||||
|
|
@ -522,8 +522,8 @@ public class AllDebridTorrentClientTest
|
|||
{
|
||||
public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock;
|
||||
public readonly Mock<IAllDebridNETClient> AllDebridClientMock;
|
||||
public readonly Mock<ILogger<AllDebridDebridClient>> LoggerMock;
|
||||
public readonly Mock<IDownloadableFileFilter> FileFilterMock;
|
||||
public readonly Mock<ILogger<AllDebridTorrentClient>> LoggerMock;
|
||||
|
||||
public Mocks()
|
||||
{
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateData_LogsWarning_WhenTorBoxStatusIsUnmapped()
|
||||
{
|
||||
// Arrange
|
||||
var torrent = new Torrent
|
||||
{
|
||||
RdId = "test-rd-id",
|
||||
RdStatus = TorrentStatus.Downloading,
|
||||
RdName = "test-torrent"
|
||||
};
|
||||
var torrentClientTorrent = new DebridClientTorrent
|
||||
{
|
||||
Status = "some-unknown-status",
|
||||
Filename = "test-torrent"
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
|
||||
|
||||
// Act
|
||||
await clientMock.Object.UpdateData(torrent, torrentClientTorrent);
|
||||
|
||||
// Assert
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
Microsoft.Extensions.Logging.LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unmapped status") && v.ToString()!.Contains("some-unknown-status")),
|
||||
It.IsAny<Exception>(),
|
||||
It.Is<Func<It.IsAnyType, Exception?, String>>((v, t) => true)),
|
||||
Times.Once);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Services;
|
||||
|
|
@ -316,4 +317,95 @@ public class TorrentsTest
|
|||
Assert.Matches("error-line 2", 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,13 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
{
|
||||
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))
|
||||
{
|
||||
|
|
@ -48,14 +54,6 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
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);
|
||||
|
||||
|
|
@ -63,7 +61,7 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
{
|
||||
var fileInfo = new FileInfo(torrentFile);
|
||||
|
||||
if (fileInfo.Extension != ".magnet" && fileInfo.Extension != ".torrent")
|
||||
if (fileInfo.Extension != ".magnet" && fileInfo.Extension != ".torrent" && fileInfo.Extension != ".nzb")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -107,6 +105,11 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
var magnetLink = await File.ReadAllTextAsync(torrentFile, stoppingToken);
|
||||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Polly.Extensions.Http;
|
|||
using RdtClient.Service.BackgroundServices;
|
||||
using RdtClient.Service.Middleware;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Service.Services.TorrentClients;
|
||||
using RdtClient.Service.Services.DebridClients;
|
||||
using RdtClient.Service.Wrappers;
|
||||
|
||||
namespace RdtClient.Service;
|
||||
|
|
@ -23,7 +23,7 @@ public static class DiConfig
|
|||
services.AddMemoryCache();
|
||||
|
||||
services.AddSingleton<IAllDebridNetClientFactory, AllDebridNetClientFactory>();
|
||||
services.AddScoped<AllDebridTorrentClient>();
|
||||
services.AddScoped<AllDebridDebridClient>();
|
||||
|
||||
services.AddSingleton<IProcessFactory, ProcessFactory>();
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
|
|
@ -31,12 +31,13 @@ public static class DiConfig
|
|||
services.AddScoped<Authentication>();
|
||||
services.AddScoped<IDownloads, Downloads>();
|
||||
services.AddScoped<Downloads>();
|
||||
services.AddScoped<PremiumizeTorrentClient>();
|
||||
services.AddScoped<PremiumizeDebridClient>();
|
||||
services.AddScoped<QBittorrent>();
|
||||
services.AddScoped<Sabnzbd>();
|
||||
services.AddScoped<RemoteService>();
|
||||
services.AddScoped<RealDebridTorrentClient>();
|
||||
services.AddScoped<RealDebridDebridClient>();
|
||||
services.AddScoped<Settings>();
|
||||
services.AddScoped<TorBoxTorrentClient>();
|
||||
services.AddScoped<TorBoxDebridClient>();
|
||||
services.AddScoped<Torrents>();
|
||||
services.AddScoped<TorrentRunner>();
|
||||
services.AddScoped<DebridLinkClient>();
|
||||
|
|
@ -46,6 +47,7 @@ public static class DiConfig
|
|||
services.AddSingleton<IEnricher, Enricher>();
|
||||
|
||||
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SabnzbdHandler>();
|
||||
|
||||
services.AddHostedService<DiskSpaceMonitor>();
|
||||
services.AddHostedService<ProviderUpdater>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,15 @@ public static class FileHelper
|
|||
|
||||
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)
|
||||
|
|
|
|||
24
server/RdtClient.Service/Helpers/FileSizeHelper.cs
Normal file
24
server/RdtClient.Service/Helpers/FileSizeHelper.cs
Normal 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]}";
|
||||
}
|
||||
}
|
||||
58
server/RdtClient.Service/Middleware/SabnzbdHandler.cs
Normal file
58
server/RdtClient.Service/Middleware/SabnzbdHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<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.Wrappers" Version="22.1.0" />
|
||||
<PackageReference Include="TorBox.NET" Version="1.5.0" />
|
||||
<PackageReference Include="TorBox.NET" Version="1.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public class Authentication(SignInManager<IdentityUser> signInManager, UserManag
|
|||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ using Microsoft.Extensions.Logging;
|
|||
using Newtonsoft.Json;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.Helpers;
|
||||
using File = AllDebridNET.File;
|
||||
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||
|
||||
namespace RdtClient.Service.Services.TorrentClients;
|
||||
namespace RdtClient.Service.Services.DebridClients;
|
||||
|
||||
public interface IAllDebridNetClientFactory
|
||||
{
|
||||
|
|
@ -51,14 +51,38 @@ public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger
|
|||
}
|
||||
}
|
||||
|
||||
public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter)
|
||||
: ITorrentClient
|
||||
public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
||||
{
|
||||
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
private static List<TorrentClientTorrent> _cache = [];
|
||||
private static Int64 _sessionCounter;
|
||||
private static List<DebridClientTorrent> _cache = [];
|
||||
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);
|
||||
|
||||
|
|
@ -89,7 +113,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
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");
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -114,7 +138,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
return resultId;
|
||||
}
|
||||
|
||||
public async Task<String> AddFile(Byte[] bytes)
|
||||
public async Task<String> AddTorrentFile(Byte[] bytes)
|
||||
{
|
||||
var result = await allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes);
|
||||
|
||||
|
|
@ -128,9 +152,19 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
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 />
|
||||
|
|
@ -139,12 +173,12 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
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);
|
||||
|
||||
|
|
@ -156,7 +190,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
return result.Link;
|
||||
}
|
||||
|
||||
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent)
|
||||
public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -255,40 +289,13 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
return Task.FromResult(download.FileName);
|
||||
}
|
||||
|
||||
private static TorrentClientTorrent 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
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
|
||||
private async Task<DebridClientTorrent> GetInfo(String torrentId)
|
||||
{
|
||||
var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
|
||||
|
||||
return Map(result);
|
||||
}
|
||||
|
||||
private static List<TorrentClientFile> GetFiles(List<File>? files, String parentPath = "")
|
||||
private static List<DebridClientFile> GetFiles(List<File>? files, String parentPath = "")
|
||||
{
|
||||
if (files == null)
|
||||
{
|
||||
|
|
@ -301,7 +308,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
|||
? 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 (file.Size.HasValue)
|
||||
|
|
@ -4,16 +4,88 @@ using Microsoft.Extensions.Logging;
|
|||
using Newtonsoft.Json;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.Helpers;
|
||||
using Download = RdtClient.Data.Models.Data.Download;
|
||||
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 results = new List<Torrent>();
|
||||
|
|
@ -35,7 +107,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
|||
return results.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<TorrentClientUser> GetUser()
|
||||
public async Task<DebridClientUser> GetUser()
|
||||
{
|
||||
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);
|
||||
|
||||
return result.Id ?? "";
|
||||
}
|
||||
|
||||
public async Task<String> AddFile(Byte[] bytes)
|
||||
public async Task<String> AddTorrentFile(Byte[] bytes)
|
||||
{
|
||||
var result = await GetClient().Seedbox.AddTorrentByFileAsync(bytes);
|
||||
|
||||
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 />
|
||||
|
|
@ -71,17 +153,17 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
|||
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);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
|
@ -197,79 +279,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
|||
return Task.FromResult(download.FileName);
|
||||
}
|
||||
|
||||
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 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)
|
||||
private async Task<DebridClientTorrent> GetInfo(String torrentId)
|
||||
{
|
||||
var result = await GetClient().Seedbox.ListAsync(torrentId);
|
||||
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
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<TorrentClientUser> GetUser();
|
||||
Task<String> AddMagnet(String magnetLink);
|
||||
Task<String> AddFile(Byte[] bytes);
|
||||
Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash);
|
||||
|
||||
Task<IList<DebridClientTorrent>> GetDownloads();
|
||||
Task<DebridClientUser> GetUser();
|
||||
Task<String> AddTorrentMagnet(String magnetLink);
|
||||
Task<String> AddTorrentFile(Byte[] bytes);
|
||||
Task<String> AddNzbLink(String nzbLink);
|
||||
Task<String> AddNzbFile(Byte[] bytes, String? name);
|
||||
Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash);
|
||||
/// <summary>
|
||||
/// Tell the debrid provider which files to download.
|
||||
/// </summary>
|
||||
|
|
@ -20,10 +21,9 @@ public interface ITorrentClient
|
|||
/// <param name="torrent">The torrent to select files for</param>
|
||||
/// <returns>Number of files selected</returns>
|
||||
Task<Int32?> SelectFiles(Torrent torrent);
|
||||
|
||||
Task Delete(String torrentId);
|
||||
Task<String> Unrestrict(String link);
|
||||
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
|
||||
Task Delete(Torrent torrent);
|
||||
Task<String> Unrestrict(Torrent torrent, String link);
|
||||
Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent);
|
||||
Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -4,22 +4,82 @@ using Newtonsoft.Json;
|
|||
using PremiumizeNET;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.Helpers;
|
||||
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();
|
||||
|
||||
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");
|
||||
|
||||
|
|
@ -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, "");
|
||||
|
||||
|
|
@ -44,7 +104,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
|||
return resultId;
|
||||
}
|
||||
|
||||
public async Task<String> AddFile(Byte[] bytes)
|
||||
public async Task<String> AddTorrentFile(Byte[] bytes)
|
||||
{
|
||||
var result = await GetClient().Transfers.CreateAsync(bytes, "");
|
||||
|
||||
|
|
@ -58,9 +118,19 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
|||
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 />
|
||||
|
|
@ -71,17 +141,17 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
|||
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);
|
||||
}
|
||||
|
||||
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent)
|
||||
public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -194,67 +264,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
|||
return Task.FromResult(download.FileName);
|
||||
}
|
||||
|
||||
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 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)
|
||||
private async Task<DebridClientTorrent> GetInfo(String id)
|
||||
{
|
||||
var results = await GetClient().Transfers.ListAsync();
|
||||
var result = results.FirstOrDefault(m => m.Id == id) ?? throw new($"Unable to find transfer with ID {id}");
|
||||
|
|
@ -4,18 +4,96 @@ using Newtonsoft.Json;
|
|||
using RDNET;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.Helpers;
|
||||
using Download = RdtClient.Data.Models.Data.Download;
|
||||
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;
|
||||
|
||||
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 results = new List<Torrent>();
|
||||
|
|
@ -37,7 +115,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
return results.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<TorrentClientUser> GetUser()
|
||||
public async Task<DebridClientUser> GetUser()
|
||||
{
|
||||
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));
|
||||
|
||||
|
|
@ -57,7 +135,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
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));
|
||||
|
||||
|
|
@ -66,15 +144,25 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
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 />
|
||||
public async Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
|
||||
{
|
||||
List<TorrentClientFile> files;
|
||||
List<DebridClientFile> files;
|
||||
|
||||
Log("Seleting files", torrent);
|
||||
|
||||
|
|
@ -105,12 +193,12 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
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);
|
||||
|
||||
|
|
@ -122,7 +210,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
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
|
||||
{
|
||||
|
|
@ -283,84 +371,6 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
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)
|
||||
{
|
||||
|
|
@ -372,7 +382,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
|||
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);
|
||||
|
||||
|
|
@ -3,40 +3,188 @@ using Microsoft.Extensions.Logging;
|
|||
using MonoTorrent;
|
||||
using Newtonsoft.Json;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Service.Helpers;
|
||||
using TorBoxNET;
|
||||
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;
|
||||
|
||||
public async Task<IList<TorrentClientTorrent>> GetTorrents()
|
||||
protected virtual ITorBoxNetClient GetClient()
|
||||
{
|
||||
var torrents = new List<TorrentInfoResult>();
|
||||
|
||||
var currentTorrents = await GetClient().Torrents.GetCurrentAsync(true);
|
||||
|
||||
if (currentTorrents != null)
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
var queuedTorrents = await GetClient().Torrents.GetQueuedAsync(true);
|
||||
|
||||
if (queuedTorrents != null)
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
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);
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -63,7 +211,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
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);
|
||||
|
||||
|
|
@ -81,18 +229,42 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
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)
|
||||
{
|
||||
return (availability.Data[0]?.Files ?? []).Select(file => new TorrentClientAvailableFile
|
||||
{
|
||||
Filename = file.Name,
|
||||
Filesize = file.Size
|
||||
})
|
||||
.ToList();
|
||||
return (availability.Data[0]?.Files ?? []).Select(file => new DebridClientAvailableFile
|
||||
{
|
||||
Filename = file.Name,
|
||||
Filesize = file.Size
|
||||
}).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 [];
|
||||
|
|
@ -104,29 +276,69 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
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('/');
|
||||
if (segments is not [_, _, _, _, var torrentIdStr, var fileIdStrOrZip])
|
||||
{
|
||||
throw new ArgumentException($"Invalid link format: {link}", nameof(link));
|
||||
}
|
||||
|
||||
var zipped = segments[5] == "zip";
|
||||
var fileId = zipped ? "0" : segments[5];
|
||||
var zipped = fileIdStrOrZip == "zip";
|
||||
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)
|
||||
{
|
||||
throw new("Unrestrict returned an invalid download");
|
||||
throw new($"Unrestrict returned an invalid download: {result.Error}");
|
||||
}
|
||||
|
||||
return result.Data!;
|
||||
}
|
||||
|
||||
public async Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent)
|
||||
public async Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -135,7 +347,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
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))
|
||||
{
|
||||
|
|
@ -177,13 +389,17 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
}
|
||||
else
|
||||
{
|
||||
logger.LogTrace("Updating status for {TorrentName} from {OldStatus} to {NewStatus}", torrent.RdName, torrent.RdStatus, rdTorrent.Status);
|
||||
torrent.RdStatus = rdTorrent.Status switch
|
||||
{
|
||||
"allocating" => TorrentStatus.Processing,
|
||||
"queued" => TorrentStatus.Processing,
|
||||
"queuedDL" => TorrentStatus.Processing,
|
||||
"metaDL" => TorrentStatus.Processing,
|
||||
"checking" => TorrentStatus.Processing,
|
||||
"checkingResumeData" => TorrentStatus.Processing,
|
||||
"paused" => TorrentStatus.Downloading,
|
||||
"pausedDL" => TorrentStatus.Downloading,
|
||||
"stalledDL" => TorrentStatus.Downloading,
|
||||
"downloading" => TorrentStatus.Downloading,
|
||||
"completed" => TorrentStatus.Downloading,
|
||||
|
|
@ -192,8 +408,11 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
"stalled" => TorrentStatus.Downloading,
|
||||
"stalled (no seeds)" => TorrentStatus.Downloading,
|
||||
"processing" => TorrentStatus.Downloading,
|
||||
"checkingDL" => TorrentStatus.Downloading,
|
||||
"cached" => TorrentStatus.Finished,
|
||||
_ => TorrentStatus.Error
|
||||
"error" => TorrentStatus.Error,
|
||||
_ when rdTorrent.Status != null && rdTorrent.Status.StartsWith("failed") => TorrentStatus.Error,
|
||||
_ => LogUnmappedStatus(rdTorrent.Status, torrent)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -214,7 +433,29 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
|
||||
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();
|
||||
|
||||
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
|
||||
|
|
@ -225,7 +466,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
[
|
||||
new()
|
||||
{
|
||||
RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/zip",
|
||||
RestrictedLink = $"https://torbox.app/fakedl/{id}/zip",
|
||||
FileName = $"{torrent.RdName}.zip"
|
||||
}
|
||||
];
|
||||
|
|
@ -235,7 +476,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
|
||||
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)
|
||||
})
|
||||
.ToList();
|
||||
|
|
@ -250,84 +491,6 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
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)
|
||||
{
|
||||
|
|
@ -339,24 +502,39 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
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))
|
||||
{
|
||||
var innerFolder = Directory.GetDirectories(hashDir)[0];
|
||||
|
||||
var moveDir = extractPath;
|
||||
|
||||
if (!extractPath.EndsWith(_torrent.RdName!))
|
||||
if (!extractPath.EndsWith(torrent.RdName!))
|
||||
{
|
||||
moveDir = hashDir;
|
||||
}
|
||||
|
|
@ -373,7 +551,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
Directory.Move(dir, destDir);
|
||||
}
|
||||
|
||||
if (!extractPath.Contains(_torrent.RdName!))
|
||||
if (!extractPath.Contains(torrent.RdName!))
|
||||
{
|
||||
Directory.Delete(innerFolder, true);
|
||||
}
|
||||
|
|
@ -384,6 +562,16 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
|||
}
|
||||
}
|
||||
|
||||
private TorrentStatus LogUnmappedStatus(String? status, Torrent torrent)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
logger.LogInformation("TorBoxDebridClient encountered an unmapped status: {Status} for torrent {TorrentName}", status, torrent.RdName);
|
||||
}
|
||||
|
||||
return torrent.RdStatus ?? TorrentStatus.Processing;
|
||||
}
|
||||
|
||||
private void Log(String message, Torrent? torrent = null)
|
||||
{
|
||||
if (torrent != null)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Service.Helpers;
|
||||
using RdtClient.Service.Services.Downloaders;
|
||||
using RdtClient.Service.Services.TorrentClients;
|
||||
using RdtClient.Service.Services.DebridClients;
|
||||
|
||||
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)
|
||||
{
|
||||
downloadPath = AllDebridTorrentClient.GetSymlinkPath(torrent, download);
|
||||
downloadPath = AllDebridDebridClient.GetSymlinkPath(torrent, download);
|
||||
}
|
||||
|
||||
if (torrent.ClientKind == Provider.DebridLink && Type == Data.Enums.DownloadClient.Symlink)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
public async Task AuthLogout()
|
||||
{
|
||||
logger.LogDebug("Auth logout");
|
||||
|
||||
await authentication.Logout();
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +190,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
var results = new List<TorrentInfo>();
|
||||
|
||||
var allTorrents = await torrents.Get();
|
||||
allTorrents = allTorrents.Where(m => m.Type == DownloadType.Torrent).ToList();
|
||||
|
||||
var prio = 0;
|
||||
|
||||
|
|
@ -223,29 +223,28 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
var speed = torrent.RdSpeed ?? 0;
|
||||
var bytesDone = (Int64)(bytesTotal * rdProgress);
|
||||
|
||||
Double downloadProgress = 0;
|
||||
|
||||
Double progress;
|
||||
if (torrent.Downloads is { Count: > 0 })
|
||||
{
|
||||
var dlBytesDone = torrent.Downloads.Sum(m => m.BytesDone);
|
||||
var dlBytesTotal = torrent.Downloads.Sum(m => m.BytesTotal);
|
||||
speed = (Int32)torrent.Downloads.Average(m => m.Speed);
|
||||
downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)dlBytesDone / dlBytesTotal, 0.0, 1.0) : 0;
|
||||
var dlStats = torrent.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
|
||||
var dlBytesDone = dlStats.Sum(m => m.BytesDone);
|
||||
var dlBytesTotal = dlStats.Sum(m => m.BytesTotal);
|
||||
speed = (Int32)(dlStats.Any() ? dlStats.Average(m => m.Speed) : 0);
|
||||
var downloadProgress = dlBytesTotal > 0 ? Math.Clamp((Double)dlBytesDone / dlBytesTotal, 0.0, 1.0) : 0;
|
||||
progress = (rdProgress + downloadProgress) / 2.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = rdProgress;
|
||||
}
|
||||
|
||||
var progress = (rdProgress + downloadProgress) / 2.0;
|
||||
var remaining = TimeSpan.Zero;
|
||||
var bytesRemaining = bytesTotal - bytesDone;
|
||||
|
||||
if (speed > 0 && bytesRemaining > 0)
|
||||
if (progress > 0 && progress < 1.0)
|
||||
{
|
||||
remaining = TimeSpan.FromSeconds(bytesRemaining / (Double)speed);
|
||||
|
||||
// In case there is clock skew
|
||||
if (remaining < TimeSpan.Zero)
|
||||
{
|
||||
remaining = TimeSpan.Zero;
|
||||
}
|
||||
var startTime = torrent.Retry > torrent.Added ? torrent.Retry.Value : torrent.Added;
|
||||
var elapsed = DateTimeOffset.UtcNow - startTime;
|
||||
var totalEstimatedTime = TimeSpan.FromTicks((Int64)(elapsed.Ticks / progress));
|
||||
remaining = totalEstimatedTime - elapsed;
|
||||
}
|
||||
|
||||
var result = new TorrentInfo
|
||||
|
|
@ -321,7 +320,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
if (torrent == null)
|
||||
if (torrent == null || torrent.Type != DownloadType.Torrent)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -345,7 +344,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
if (torrent == null)
|
||||
if (torrent == null || torrent.Type != DownloadType.Torrent)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -361,9 +360,10 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
|
||||
if (torrent.Downloads.Count > 0)
|
||||
{
|
||||
bytesDone = torrent.Downloads.Sum(m => m.BytesDone);
|
||||
bytesTotal = torrent.Downloads.Sum(m => m.BytesTotal);
|
||||
speed = (Int32)torrent.Downloads.Average(m => m.Speed);
|
||||
var dlStats = torrent.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
|
||||
bytesDone = dlStats.Sum(m => m.BytesDone);
|
||||
bytesTotal = dlStats.Sum(m => m.BytesTotal);
|
||||
speed = (Int32)(dlStats.Any() ? dlStats.Average(m => m.Speed) : 0);
|
||||
}
|
||||
|
||||
var result = new TorrentProperties
|
||||
|
|
@ -419,7 +419,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
if (torrent == null)
|
||||
if (torrent == null || torrent.Type != DownloadType.Torrent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -511,7 +511,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
{
|
||||
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())
|
||||
.ToList();
|
||||
|
||||
|
|
@ -592,6 +592,13 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -599,7 +606,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
{
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
if (torrent == null)
|
||||
if (torrent == null || torrent.Type != DownloadType.Torrent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -619,7 +626,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
{
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
if (torrent == null)
|
||||
if (torrent == null || torrent.Type != DownloadType.Torrent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,67 +10,65 @@ public class RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
|
|||
var allTorrents = await torrents.Get();
|
||||
|
||||
var torrentDtos = allTorrents.Select(torrent => new TorrentDto
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download => new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = download.BytesTotal,
|
||||
BytesDone = download.BytesDone,
|
||||
Speed = download.Speed
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download => new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = download.BytesTotal,
|
||||
BytesDone = download.BytesDone,
|
||||
Speed = download.Speed
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
await hub.Clients.All.SendCoreAsync("update",
|
||||
[
|
||||
torrentDtos
|
||||
|
|
|
|||
225
server/RdtClient.Service/Services/Sabnzbd.cs
Normal file
225
server/RdtClient.Service/Services/Sabnzbd.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
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) =>
|
||||
{
|
||||
var rdProgress = Math.Clamp(t.RdProgress ?? 0.0, 0.0, 100.0) / 100.0;
|
||||
Double progress;
|
||||
|
||||
var dlStats = t.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
|
||||
if (dlStats.Count > 0)
|
||||
{
|
||||
var bytesDone = dlStats.Sum(m => m.BytesDone);
|
||||
var bytesTotal = dlStats.Sum(m => m.BytesTotal);
|
||||
var downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0;
|
||||
progress = (rdProgress + downloadProgress) / 2.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = rdProgress;
|
||||
}
|
||||
|
||||
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(dlStats.Sum(d => d.BytesTotal)),
|
||||
SizeLeft = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal - d.BytesDone)),
|
||||
Percentage = (progress * 100.0).ToString("0"),
|
||||
|
||||
Status = t.RdStatus switch
|
||||
{
|
||||
TorrentStatus.Queued => "Queued",
|
||||
TorrentStatus.Processing => "Downloading",
|
||||
TorrentStatus.WaitingForFileSelection => "Downloading",
|
||||
TorrentStatus.Downloading => "Downloading",
|
||||
TorrentStatus.Uploading => "Downloading",
|
||||
TorrentStatus.Finished => "Completed",
|
||||
TorrentStatus.Error => "Failed",
|
||||
_ => "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;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,21 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
|||
|
||||
public static Boolean IsPausedForLowDiskSpace { get; set; }
|
||||
|
||||
public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId)
|
||||
{
|
||||
if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient))
|
||||
{
|
||||
return (downloadClient.Speed, downloadClient.BytesTotal, downloadClient.BytesDone);
|
||||
}
|
||||
|
||||
if (ActiveUnpackClients.TryGetValue(downloadId, out var unpackClient))
|
||||
{
|
||||
return (0, 100, unpackClient.Progess);
|
||||
}
|
||||
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
Log("Initializing TorrentRunner");
|
||||
|
|
@ -623,9 +638,9 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
|||
Log($"Torrent reported an error: {torrent.RdStatusRaw}", torrent);
|
||||
Log($"Torrent retry count {torrent.RetryCount}/{torrent.TorrentRetryAttempts}", torrent);
|
||||
|
||||
Log($"Received RealDebrid error: {torrent.RdStatusRaw}, not processing further", torrent);
|
||||
Log($"Received provider error: {torrent.RdStatusRaw}, not processing further", torrent);
|
||||
|
||||
await torrents.UpdateComplete(torrent.TorrentId, $"Received RealDebrid error: {torrent.RdStatusRaw}.", DateTimeOffset.UtcNow, true);
|
||||
await torrents.UpdateComplete(torrent.TorrentId, $"Debrid error: {torrent.RdStatusRaw}.", DateTimeOffset.UtcNow, true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
|
@ -665,8 +680,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
|||
|
||||
var completePerc = 0;
|
||||
|
||||
var totalDownloadBytes = torrent.Downloads.Sum(m => m.BytesTotal);
|
||||
var totalDoneBytes = torrent.Downloads.Sum(m => m.BytesDone);
|
||||
var totalDownloadBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesTotal);
|
||||
var totalDoneBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesDone);
|
||||
|
||||
if (totalDownloadBytes > 0)
|
||||
{
|
||||
|
|
@ -697,7 +712,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
|||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex.Message, "Torrent processing result in an unexpected exception: {Message}", ex.Message);
|
||||
await torrents.UpdateComplete(torrent.TorrentId, ex.Message, DateTimeOffset.UtcNow, true);
|
||||
await torrents.UpdateComplete(torrent.TorrentId, $"Runner error: {ex.Message}", DateTimeOffset.UtcNow, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ using System.IO.Abstractions;
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MonoTorrent;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.BackgroundServices;
|
||||
using RdtClient.Service.Helpers;
|
||||
using RdtClient.Service.Services.TorrentClients;
|
||||
using RdtClient.Service.Services.DebridClients;
|
||||
using RdtClient.Service.Wrappers;
|
||||
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||
|
||||
|
|
@ -25,11 +27,11 @@ public class Torrents(
|
|||
IProcessFactory processFactory,
|
||||
IFileSystem fileSystem,
|
||||
IEnricher enricher,
|
||||
AllDebridTorrentClient allDebridTorrentClient,
|
||||
PremiumizeTorrentClient premiumizeTorrentClient,
|
||||
RealDebridTorrentClient realDebridTorrentClient,
|
||||
AllDebridDebridClient allDebridDebridClient,
|
||||
PremiumizeDebridClient premiumizeDebridClient,
|
||||
RealDebridDebridClient realDebridDebridClient,
|
||||
DebridLinkClient debridLinkClient,
|
||||
TorBoxTorrentClient torBoxTorrentClient)
|
||||
TorBoxDebridClient torBoxDebridClient)
|
||||
{
|
||||
private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
|
||||
|
||||
|
|
@ -38,51 +40,37 @@ public class Torrents(
|
|||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
|
||||
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
|
||||
|
||||
private ITorrentClient TorrentClient
|
||||
private IDebridClient DebridClient
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.Get.Provider.Provider switch
|
||||
{
|
||||
Provider.Premiumize => premiumizeTorrentClient,
|
||||
Provider.RealDebrid => realDebridTorrentClient,
|
||||
Provider.AllDebrid => allDebridTorrentClient,
|
||||
Provider.Premiumize => premiumizeDebridClient,
|
||||
Provider.RealDebrid => realDebridDebridClient,
|
||||
Provider.AllDebrid => allDebridDebridClient,
|
||||
Provider.DebridLink => debridLinkClient,
|
||||
Provider.TorBox => torBoxTorrentClient,
|
||||
Provider.TorBox => torBoxDebridClient,
|
||||
_ => throw new("Invalid Provider")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<Torrent>> Get()
|
||||
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
|
||||
|
||||
public virtual (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetDownloadStats(Guid downloadId)
|
||||
{
|
||||
return TorrentRunner.GetStats(downloadId);
|
||||
}
|
||||
|
||||
public virtual async Task<IList<Torrent>> Get()
|
||||
{
|
||||
var torrents = await torrentData.Get();
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
foreach (var download in torrent.Downloads)
|
||||
{
|
||||
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
download.Speed = downloadClient.Speed;
|
||||
download.BytesTotal = downloadClient.BytesTotal;
|
||||
download.BytesDone = downloadClient.BytesDone;
|
||||
}
|
||||
|
||||
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
|
||||
{
|
||||
download.BytesTotal = 100;
|
||||
download.BytesDone = unpackClient.Progess;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return torrents;
|
||||
}
|
||||
|
||||
public async Task<Torrent?> GetByHash(String hash)
|
||||
public virtual async Task<Torrent?> GetByHash(String hash)
|
||||
{
|
||||
var torrent = await torrentData.GetByHash(hash);
|
||||
|
||||
|
|
@ -108,7 +96,83 @@ public class Torrents(
|
|||
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);
|
||||
MagnetLink magnet;
|
||||
|
|
@ -155,22 +219,22 @@ public class Torrents(
|
|||
torrent.RdName = magnet.Name;
|
||||
|
||||
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);
|
||||
await CopyAddedTorrent(magnet.Name!, magnetLink);
|
||||
await CopyAddedTorrent(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);
|
||||
|
||||
String fileAsBase64;
|
||||
|
||||
MonoTorrent.Torrent monoTorrent;
|
||||
|
||||
if (enriched.SequenceEqual(bytes))
|
||||
{
|
||||
fileAsBase64 = Convert.ToBase64String(bytes);
|
||||
|
|
@ -228,56 +292,61 @@ public class Torrents(
|
|||
|
||||
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);
|
||||
|
||||
await CopyAddedTorrent(monoTorrent.Name, bytes);
|
||||
await CopyAddedTorrent(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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
fileSystem.Directory.CreateDirectory(Settings.Get.General.CopyAddedTorrents);
|
||||
}
|
||||
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 +374,20 @@ public class Torrents(
|
|||
|
||||
try
|
||||
{
|
||||
var id = torrent.IsFile
|
||||
? await TorrentClient.AddFile(Convert.FromBase64String(torrent.FileOrMagnet))
|
||||
: await TorrentClient.AddMagnet(torrent.FileOrMagnet);
|
||||
String id;
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -319,9 +399,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;
|
||||
}
|
||||
|
|
@ -335,7 +415,7 @@ public class Torrents(
|
|||
return;
|
||||
}
|
||||
|
||||
var selected = await TorrentClient.SelectFiles(torrent);
|
||||
var selected = await DebridClient.SelectFiles(torrent);
|
||||
|
||||
if (selected == 0)
|
||||
{
|
||||
|
|
@ -352,7 +432,7 @@ public class Torrents(
|
|||
return;
|
||||
}
|
||||
|
||||
var downloadInfos = await TorrentClient.GetDownloadInfos(torrent);
|
||||
var downloadInfos = await DebridClient.GetDownloadInfos(torrent);
|
||||
|
||||
if (downloadInfos == null)
|
||||
{
|
||||
|
|
@ -394,7 +474,7 @@ public class Torrents(
|
|||
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);
|
||||
|
||||
|
|
@ -460,7 +540,7 @@ public class Torrents(
|
|||
|
||||
try
|
||||
{
|
||||
await TorrentClient.Delete(torrent.RdId);
|
||||
await DebridClient.Delete(torrent);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -509,25 +589,21 @@ public class Torrents(
|
|||
|
||||
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);
|
||||
|
||||
return unrestrictedLink;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <inheritdoc />
|
||||
public async Task<String> RetrieveFileName(Guid downloadId)
|
||||
{
|
||||
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);
|
||||
|
||||
|
|
@ -536,7 +612,7 @@ public class Torrents(
|
|||
|
||||
public async Task<Profile> GetProfile()
|
||||
{
|
||||
var user = await TorrentClient.GetUser();
|
||||
var user = await DebridClient.GetUser();
|
||||
|
||||
var profile = new Profile
|
||||
{
|
||||
|
|
@ -560,7 +636,7 @@ public class Torrents(
|
|||
|
||||
try
|
||||
{
|
||||
var rdTorrents = await TorrentClient.GetTorrents();
|
||||
var rdTorrents = await DebridClient.GetDownloads();
|
||||
|
||||
foreach (var rdTorrent in rdTorrents)
|
||||
{
|
||||
|
|
@ -573,8 +649,7 @@ public class Torrents(
|
|||
{
|
||||
Category = Settings.Get.Provider.Default.Category,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
DownloadAction =
|
||||
Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
|
||||
FinishedAction = Settings.Get.Provider.Default.FinishedAction,
|
||||
|
|
@ -594,7 +669,7 @@ public class Torrents(
|
|||
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);
|
||||
}
|
||||
|
|
@ -670,15 +745,31 @@ public class Torrents(
|
|||
|
||||
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
|
||||
{
|
||||
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);
|
||||
|
|
@ -775,22 +866,6 @@ public class Torrents(
|
|||
|
||||
await UpdateTorrentClientData(torrent);
|
||||
|
||||
foreach (var download in torrent.Downloads)
|
||||
{
|
||||
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
download.Speed = downloadClient.Speed;
|
||||
download.BytesTotal = downloadClient.BytesTotal;
|
||||
download.BytesDone = downloadClient.BytesDone;
|
||||
}
|
||||
|
||||
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
|
||||
{
|
||||
download.BytesTotal = 100;
|
||||
download.BytesDone = unpackClient.Progess;
|
||||
}
|
||||
}
|
||||
|
||||
return torrent;
|
||||
}
|
||||
|
||||
|
|
@ -809,6 +884,7 @@ public class Torrents(
|
|||
private async Task<Torrent> AddQueued(String infoHash,
|
||||
String fileOrMagnetContents,
|
||||
Boolean isFile,
|
||||
DownloadType downloadType,
|
||||
Torrent torrent)
|
||||
{
|
||||
var existingTorrent = await torrentData.GetByHash(infoHash);
|
||||
|
|
@ -822,6 +898,7 @@ public class Torrents(
|
|||
infoHash,
|
||||
fileOrMagnetContents,
|
||||
isFile,
|
||||
downloadType,
|
||||
torrent.DownloadClient,
|
||||
torrent);
|
||||
|
||||
|
|
@ -931,13 +1008,13 @@ public class Torrents(
|
|||
}
|
||||
}
|
||||
|
||||
private async Task UpdateTorrentClientData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent = null)
|
||||
private async Task UpdateTorrentClientData(Torrent torrent, DebridClientTorrent? torrentClientTorrent = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var originalTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
|
||||
|
||||
await TorrentClient.UpdateData(torrent, torrentClientTorrent);
|
||||
await DebridClient.UpdateData(torrent, torrentClientTorrent);
|
||||
|
||||
var newTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
|
||||
|
||||
|
|
@ -976,4 +1053,27 @@ public class Torrents(
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Service.Helpers;
|
||||
using RdtClient.Service.Services.TorrentClients;
|
||||
using RdtClient.Service.Services.DebridClients;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Archives.Zip;
|
||||
|
|
@ -106,7 +106,7 @@ public class UnpackClient(Download download, String destinationPath)
|
|||
|
||||
if (_torrent.ClientKind == Provider.TorBox)
|
||||
{
|
||||
TorBoxTorrentClient.MoveHashDirContents(extractPath, _torrent);
|
||||
TorBoxDebridClient.MoveHashDirContents(extractPath, _torrent);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
255
server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs
Normal file
255
server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
118
server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs
Normal file
118
server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
32
server/RdtClient.Web.Test/RdtClient.Web.Test.csproj
Normal file
32
server/RdtClient.Web.Test/RdtClient.Web.Test.csproj
Normal 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>
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "3.1.3",
|
||||
"version": "9.0.9",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ namespace RdtClient.Web.Controllers;
|
|||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v2")]
|
||||
[Route("qbittorrent/api/v2")]
|
||||
public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent) : Controller
|
||||
{
|
||||
[AllowAnonymous]
|
||||
|
|
|
|||
151
server/RdtClient.Web/Controllers/SabnzbdController.cs
Normal file
151
server/RdtClient.Web/Controllers/SabnzbdController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
23
server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs
Normal file
23
server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MonoTorrent;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Data.Models.TorrentClient;
|
||||
using RdtClient.Service.BackgroundServices;
|
||||
using RdtClient.Service.Helpers;
|
||||
using RdtClient.Service.Services;
|
||||
|
|
@ -22,66 +22,65 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
var results = await torrents.Get();
|
||||
|
||||
var torrentDtos = results.Select(torrent => new TorrentDto
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download => new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = download.BytesTotal,
|
||||
BytesDone = download.BytesDone,
|
||||
Speed = download.Speed
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download => new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = download.BytesTotal,
|
||||
BytesDone = download.BytesDone,
|
||||
Speed = download.Speed
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
|
||||
return Ok(torrentDtos);
|
||||
}
|
||||
|
|
@ -119,6 +118,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
|
|
@ -141,26 +141,25 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download => new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = download.BytesTotal,
|
||||
BytesDone = download.BytesDone,
|
||||
Speed = download.Speed
|
||||
})
|
||||
.ToList()
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = download.BytesTotal,
|
||||
BytesDone = download.BytesDone,
|
||||
Speed = download.Speed
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return Ok(torrentDto);
|
||||
|
|
@ -171,7 +170,6 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
public ActionResult<DiskSpaceStatus?> GetDiskSpaceStatus()
|
||||
{
|
||||
var status = DiskSpaceMonitor.GetCurrentStatus();
|
||||
|
||||
return Ok(status);
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +243,68 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
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]
|
||||
[Route("CheckFiles")]
|
||||
public async Task<ActionResult> CheckFiles([FromForm] IFormFile? file)
|
||||
|
|
@ -355,7 +415,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
var includeError = "";
|
||||
var excludeError = "";
|
||||
|
||||
IList<TorrentClientAvailableFile> availableFiles;
|
||||
IList<DebridClientAvailableFile> availableFiles;
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(request.MagnetLink))
|
||||
{
|
||||
|
|
@ -382,7 +442,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
return BadRequest();
|
||||
}
|
||||
|
||||
var selectedFiles = new List<TorrentClientAvailableFile>();
|
||||
var selectedFiles = new List<DebridClientAvailableFile>();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(request.IncludeRegex))
|
||||
{
|
||||
|
|
@ -443,6 +503,12 @@ public class TorrentControllerUploadMagnetRequest
|
|||
public Torrent? Torrent { get; set; }
|
||||
}
|
||||
|
||||
public class TorrentControllerUploadNzbLinkRequest
|
||||
{
|
||||
public String? NzbLink { get; set; }
|
||||
public Torrent? Torrent { get; set; }
|
||||
}
|
||||
|
||||
public class TorrentControllerDeleteRequest
|
||||
{
|
||||
public Boolean DeleteData { get; set; }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using RdtClient.Data.Models.Internal;
|
|||
using RdtClient.Service;
|
||||
using RdtClient.Service.Middleware;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Service.Helpers;
|
||||
using Serilog;
|
||||
using Serilog.Debugging;
|
||||
using Serilog.Events;
|
||||
|
|
@ -41,6 +42,7 @@ builder.WebHost.ConfigureKestrel(options =>
|
|||
if (appSettings.Logging?.File?.Path != null)
|
||||
{
|
||||
builder.Host.UseSerilog((_, lc) => lc.Enrich.FromLogContext()
|
||||
.Enrich.With<CredentialRedactorEnricher>()
|
||||
.WriteTo.File(appSettings.Logging.File.Path,
|
||||
rollOnFileSizeLimit: true,
|
||||
fileSizeLimitBytes: appSettings.Logging.File.FileSizeLimitBytes,
|
||||
|
|
@ -72,11 +74,8 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
|
|||
});
|
||||
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy("AuthSetting",
|
||||
policyCorrectUser =>
|
||||
{
|
||||
policyCorrectUser.Requirements.Add(new AuthSettingRequirement());
|
||||
});
|
||||
.AddPolicy("AuthSetting", policyCorrectUser => { policyCorrectUser.Requirements.Add(new AuthSettingRequirement()); })
|
||||
.AddPolicy("Sabnzbd", policyCorrectUser => { policyCorrectUser.Requirements.Add(new SabnzbdRequirement()); });
|
||||
|
||||
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,28 +11,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Data", "RdtClient
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Service.Test", "RdtClient.Service.Test\RdtClient.Service.Test.csproj", "{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Web.Test", "RdtClient.Web.Test\RdtClient.Web.Test.csproj", "{C9517091-EC0A-4146-85BA-58D95D0C597E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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|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.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.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.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.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.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.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.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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
Loading…
Reference in a new issue