Merge branch 'main' of https://github.com/rogerfar/rdt-client
This commit is contained in:
commit
5d09053327
16 changed files with 839 additions and 503 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { TorrentService } from 'src/app/torrent.service';
|
||||
import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||
|
|
@ -15,6 +16,7 @@ import { NgClass } from '@angular/common';
|
|||
standalone: true,
|
||||
})
|
||||
export class AddNewTorrentComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private router = inject(Router);
|
||||
private torrentService = inject(TorrentService);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
|
@ -29,8 +31,9 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
public provider: string;
|
||||
public downloadClient: number;
|
||||
|
||||
public category: string;
|
||||
private _category = '';
|
||||
public categories: string[] = [];
|
||||
public filteredCategories: string[] = [];
|
||||
public categoryDropdownOpen = false;
|
||||
public hostDownloadAction: number = 0;
|
||||
public downloadAction: number = 0;
|
||||
|
|
@ -56,10 +59,19 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
public excludeRegexError: string;
|
||||
public regexSelected: TorrentFileAvailability[];
|
||||
|
||||
private selectedFile: File;
|
||||
private selectedFile: File | null = null;
|
||||
|
||||
public get category(): string {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
public set category(value: string) {
|
||||
this._category = value;
|
||||
this.updateFilteredCategories();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.queryParams.subscribe((params) => {
|
||||
this.activatedRoute.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
|
||||
if (params['type'] === 'nzb') {
|
||||
this.type = 'nzb';
|
||||
} else if (params['type'] === 'torrent') {
|
||||
|
|
@ -75,45 +87,55 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
this.type = 'nzb';
|
||||
}
|
||||
});
|
||||
this.settingsService.get().subscribe((settings) => {
|
||||
const providerSetting = settings.find((m) => m.key === 'Provider:Provider');
|
||||
this.provider = providerSetting.enumValues[providerSetting.value as number];
|
||||
this.downloadClient = settings.find((m) => m.key === 'DownloadClient:Client')?.value as number;
|
||||
|
||||
this.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
|
||||
const categoriesSetting = settings.find((m) => m.key === 'General:Categories')?.value as string;
|
||||
this.categories = (categoriesSetting ?? '')
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0)
|
||||
.filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i);
|
||||
const matchedCategory = this.categories.find((c) => c.toLowerCase() === (this.category ?? '').toLowerCase());
|
||||
if (matchedCategory) {
|
||||
this.category = matchedCategory;
|
||||
}
|
||||
this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
|
||||
?.value as number;
|
||||
this.downloadAction =
|
||||
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
||||
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
||||
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
||||
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
||||
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
||||
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
||||
this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
||||
this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')?.value as number;
|
||||
this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
||||
this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
||||
this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
||||
this.settingsService
|
||||
.get()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((settings) => {
|
||||
const providerSetting = settings.find((m) => m.key === 'Provider:Provider');
|
||||
this.provider = providerSetting.enumValues[providerSetting.value as number];
|
||||
this.downloadClient = settings.find((m) => m.key === 'DownloadClient:Client')?.value as number;
|
||||
|
||||
this.setFinishAction();
|
||||
});
|
||||
this.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
|
||||
const categoriesSetting = settings.find((m) => m.key === 'General:Categories')?.value as string;
|
||||
this.categories = (categoriesSetting ?? '')
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0)
|
||||
.filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i);
|
||||
const matchedCategory = this.categories.find((c) => c.toLowerCase() === (this.category ?? '').toLowerCase());
|
||||
if (matchedCategory) {
|
||||
this.category = matchedCategory;
|
||||
} else {
|
||||
this.updateFilteredCategories();
|
||||
}
|
||||
this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
|
||||
?.value as number;
|
||||
this.downloadAction =
|
||||
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
||||
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
||||
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
||||
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
||||
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
||||
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
||||
this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
||||
this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')?.value as number;
|
||||
this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
||||
this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
||||
this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
||||
|
||||
this.setFinishAction();
|
||||
});
|
||||
}
|
||||
|
||||
get filteredCategories(): string[] {
|
||||
if (!this.category) return this.categories;
|
||||
private updateFilteredCategories(): void {
|
||||
if (!this.category) {
|
||||
this.filteredCategories = this.categories;
|
||||
return;
|
||||
}
|
||||
|
||||
const search = this.category.toLowerCase();
|
||||
return this.categories.filter((c) => c.toLowerCase().includes(search));
|
||||
this.filteredCategories = this.categories.filter((value) => value.toLowerCase().includes(search));
|
||||
}
|
||||
|
||||
public selectCategory(cat: string): void {
|
||||
|
|
@ -144,7 +166,7 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
public pickFile(evt: Event): void {
|
||||
const files = (evt.target as HTMLInputElement).files;
|
||||
|
||||
if (files.length === 0) {
|
||||
if (files == null || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,9 +44,12 @@ export class Torrent {
|
|||
public rdSpeed: number;
|
||||
public rdSeeders: number;
|
||||
public rdFiles: string;
|
||||
public statusText?: string;
|
||||
public filesCount?: number;
|
||||
public downloadsCount?: number;
|
||||
|
||||
public files: TorrentFile[];
|
||||
public downloads: Download[];
|
||||
public files?: TorrentFile[];
|
||||
public downloads?: Download[];
|
||||
}
|
||||
|
||||
export class TorrentFile {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { NavigationEnd, Router, RouterLink } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Profile } from '../models/profile.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { NgClass, DatePipe } from '@angular/common';
|
||||
import { filter } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
|
|
@ -13,6 +15,7 @@ import { NgClass, DatePipe } from '@angular/common';
|
|||
standalone: true,
|
||||
})
|
||||
export class NavbarComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private settingsService = inject(SettingsService);
|
||||
private authService = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
|
|
@ -24,39 +27,48 @@ export class NavbarComponent implements OnInit {
|
|||
public version: string;
|
||||
|
||||
constructor() {
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.showMobileMenu = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getProfile().subscribe((result) => {
|
||||
this.profile = result;
|
||||
this.settingsService
|
||||
.getProfile()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
this.profile = result;
|
||||
|
||||
switch (result.provider) {
|
||||
case 'RealDebrid':
|
||||
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||
break;
|
||||
case 'AllDebrid':
|
||||
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||
break;
|
||||
case 'Premiumize':
|
||||
this.providerLink = 'https://www.premiumize.me/';
|
||||
break;
|
||||
case 'TorBox':
|
||||
this.providerLink = 'https://torbox.app/';
|
||||
break;
|
||||
case 'DebridLink':
|
||||
this.providerLink = 'https://debrid-link.com/';
|
||||
break;
|
||||
}
|
||||
});
|
||||
switch (result.provider) {
|
||||
case 'RealDebrid':
|
||||
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||
break;
|
||||
case 'AllDebrid':
|
||||
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||
break;
|
||||
case 'Premiumize':
|
||||
this.providerLink = 'https://www.premiumize.me/';
|
||||
break;
|
||||
case 'TorBox':
|
||||
this.providerLink = 'https://torbox.app/';
|
||||
break;
|
||||
case 'DebridLink':
|
||||
this.providerLink = 'https://debrid-link.com/';
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.settingsService.getVersion().subscribe((result) => {
|
||||
this.version = result.version;
|
||||
});
|
||||
this.settingsService
|
||||
.getVersion()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
this.version = result.version;
|
||||
});
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,78 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export function getSortFieldValue(item: unknown, field: string): unknown {
|
||||
if (item == null || !field) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return field.split('.').reduce<unknown>((value, key) => {
|
||||
if (value == null || typeof value !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (value as Record<string, unknown>)[key];
|
||||
}, item);
|
||||
}
|
||||
|
||||
function compareSortValues(left: unknown, right: unknown): number {
|
||||
if (left === right) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (typeof left === 'string' && typeof right === 'string') {
|
||||
return left.localeCompare(right, undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
if (left instanceof Date && right instanceof Date) {
|
||||
return left.getTime() - right.getTime();
|
||||
}
|
||||
|
||||
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
||||
return Number(left) - Number(right);
|
||||
}
|
||||
|
||||
const comparableLeft = left as string | number | bigint;
|
||||
const comparableRight = right as string | number | bigint;
|
||||
|
||||
if (comparableLeft < comparableRight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (comparableLeft > comparableRight) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function sortItems<T>(
|
||||
array: readonly T[],
|
||||
field: string,
|
||||
order: SortDirection = 'asc',
|
||||
accessor: (item: T, field: string) => unknown = getSortFieldValue,
|
||||
): T[] {
|
||||
if (!Array.isArray(array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const direction = order === 'asc' ? 1 : -1;
|
||||
|
||||
return [...array].sort((left, right) => compareSortValues(accessor(left, field), accessor(right, field)) * direction);
|
||||
}
|
||||
|
||||
@Pipe({ name: 'sort' })
|
||||
export class SortPipe implements PipeTransform {
|
||||
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {
|
||||
if (!Array.isArray(array)) {
|
||||
return [];
|
||||
}
|
||||
const sortedArray = array.sort((a, b) => {
|
||||
if (a[field] < b[field]) {
|
||||
return -1;
|
||||
} else if (a[field] > b[field]) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return order === 'asc' ? sortedArray : sortedArray.reverse();
|
||||
transform(array: unknown[], field: string, order: SortDirection = 'asc'): unknown[] {
|
||||
return sortItems(array, field, order);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +1,130 @@
|
|||
import { Pipe, PipeTransform, inject } from '@angular/core';
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
||||
import { FileSizePipe } from './filesize.pipe';
|
||||
|
||||
@Pipe({ name: 'status' })
|
||||
export class TorrentStatusPipe implements PipeTransform {
|
||||
private pipe = inject(FileSizePipe);
|
||||
const fileSizePipe = new FileSizePipe();
|
||||
|
||||
transform(torrent: Torrent): string {
|
||||
if (torrent.error) {
|
||||
return torrent.error;
|
||||
}
|
||||
export function getTorrentStatus(torrent: Torrent): string {
|
||||
if (torrent.error) {
|
||||
return torrent.error;
|
||||
}
|
||||
|
||||
if (torrent.downloads.length > 0) {
|
||||
const allFinished = torrent.downloads.every((m) => m.completed != null);
|
||||
const downloads = torrent.downloads ?? [];
|
||||
|
||||
if (allFinished) {
|
||||
return 'Finished';
|
||||
if (downloads.length > 0) {
|
||||
let allFinished = true;
|
||||
let downloadingCount = 0;
|
||||
let downloadedCount = 0;
|
||||
let downloadingBytesDone = 0;
|
||||
let downloadingBytesTotal = 0;
|
||||
let downloadingSpeed = 0;
|
||||
let unpackingCount = 0;
|
||||
let unpackedCount = 0;
|
||||
let unpackingBytesDone = 0;
|
||||
let unpackingBytesTotal = 0;
|
||||
let queuedForUnpackingCount = 0;
|
||||
let queuedForDownloadingCount = 0;
|
||||
|
||||
for (const download of downloads) {
|
||||
if (download.completed == null) {
|
||||
allFinished = false;
|
||||
}
|
||||
|
||||
const downloading = torrent.downloads.filter((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
|
||||
const downloaded = torrent.downloads.filter((m) => m.downloadFinished != null);
|
||||
|
||||
if (downloading.length > 0) {
|
||||
const bytesDone = downloading.reduce((sum, m) => sum + m.bytesDone, 0);
|
||||
const bytesTotal = downloading.reduce((sum, m) => sum + m.bytesTotal, 0);
|
||||
const progress = (bytesDone / bytesTotal || 0) * 100;
|
||||
|
||||
const allSpeeds = downloading.reduce((sum, m) => sum + m.speed, 0);
|
||||
|
||||
const speed: string | string[] = this.pipe.transform(allSpeeds, 'filesize');
|
||||
|
||||
return `Downloading file ${downloading.length + downloaded.length}/${
|
||||
torrent.downloads.length
|
||||
} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||
if (download.downloadFinished != null) {
|
||||
downloadedCount += 1;
|
||||
}
|
||||
|
||||
const unpacking = torrent.downloads.filter((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
|
||||
const unpacked = torrent.downloads.filter((m) => m.unpackingFinished != null);
|
||||
|
||||
if (unpacking.length > 0) {
|
||||
const bytesDone = unpacking.reduce((sum, m) => sum + m.bytesDone, 0);
|
||||
const bytesTotal = unpacking.reduce((sum, m) => sum + m.bytesTotal, 0);
|
||||
const progress = (bytesDone / bytesTotal || 0) * 100;
|
||||
|
||||
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
|
||||
2,
|
||||
)}%)`;
|
||||
if (download.downloadStarted && !download.downloadFinished && download.bytesDone > 0) {
|
||||
downloadingCount += 1;
|
||||
downloadingBytesDone += download.bytesDone;
|
||||
downloadingBytesTotal += download.bytesTotal;
|
||||
downloadingSpeed += download.speed;
|
||||
}
|
||||
|
||||
const queuedForUnpacking = torrent.downloads.filter((m) => m.unpackingQueued && !m.unpackingStarted);
|
||||
|
||||
if (queuedForUnpacking.length > 0) {
|
||||
return `Queued for unpacking`;
|
||||
if (download.unpackingFinished != null) {
|
||||
unpackedCount += 1;
|
||||
}
|
||||
|
||||
const queuedForDownload = torrent.downloads.filter((m) => !m.downloadStarted && !m.downloadFinished);
|
||||
|
||||
if (queuedForDownload.length > 0) {
|
||||
return `Queued for downloading`;
|
||||
if (download.unpackingStarted && !download.unpackingFinished && download.bytesDone > 0) {
|
||||
unpackingCount += 1;
|
||||
unpackingBytesDone += download.bytesDone;
|
||||
unpackingBytesTotal += download.bytesTotal;
|
||||
}
|
||||
|
||||
if (unpacked.length > 0) {
|
||||
return `Files unpacked`;
|
||||
if (download.unpackingQueued && !download.unpackingStarted) {
|
||||
queuedForUnpackingCount += 1;
|
||||
}
|
||||
|
||||
if (downloaded.length > 0) {
|
||||
return `Files downloaded to host`;
|
||||
if (!download.downloadStarted && !download.downloadFinished) {
|
||||
queuedForDownloadingCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent.completed) {
|
||||
if (allFinished) {
|
||||
return 'Finished';
|
||||
}
|
||||
|
||||
switch (torrent.rdStatus) {
|
||||
case RealDebridStatus.Queued:
|
||||
return 'Not Yet Added to Provider';
|
||||
case RealDebridStatus.Downloading:
|
||||
if (torrent.rdSeeders < 1 && torrent.type !== 1) {
|
||||
return `Torrent stalled`;
|
||||
}
|
||||
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
|
||||
return `Torrent downloading (${torrent.rdProgress}% - ${speed}/s)`;
|
||||
case RealDebridStatus.Processing:
|
||||
return `Torrent processing`;
|
||||
case RealDebridStatus.WaitingForFileSelection:
|
||||
return `Torrent waiting for file selection`;
|
||||
case RealDebridStatus.Error:
|
||||
return `Torrent error: ${torrent.rdStatusRaw}`;
|
||||
case RealDebridStatus.Finished:
|
||||
return `Torrent finished, waiting for download links`;
|
||||
case RealDebridStatus.Uploading:
|
||||
return `Torrent uploading`;
|
||||
default:
|
||||
return 'Unknown status';
|
||||
if (downloadingCount > 0) {
|
||||
const progress = ((downloadingBytesDone / downloadingBytesTotal) || 0) * 100;
|
||||
const speed = fileSizePipe.transform(downloadingSpeed, 'filesize') as string;
|
||||
|
||||
return `Downloading file ${downloadingCount + downloadedCount}/${downloads.length} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||
}
|
||||
|
||||
if (unpackingCount > 0) {
|
||||
const progress = ((unpackingBytesDone / unpackingBytesTotal) || 0) * 100;
|
||||
|
||||
return `Extracting file ${unpackingCount + unpackedCount}/${downloads.length} (${progress.toFixed(2)}%)`;
|
||||
}
|
||||
|
||||
if (queuedForUnpackingCount > 0) {
|
||||
return 'Queued for unpacking';
|
||||
}
|
||||
|
||||
if (queuedForDownloadingCount > 0) {
|
||||
return 'Queued for downloading';
|
||||
}
|
||||
|
||||
if (unpackedCount > 0) {
|
||||
return 'Files unpacked';
|
||||
}
|
||||
|
||||
if (downloadedCount > 0) {
|
||||
return 'Files downloaded to host';
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent.completed) {
|
||||
return 'Finished';
|
||||
}
|
||||
|
||||
switch (torrent.rdStatus) {
|
||||
case RealDebridStatus.Queued:
|
||||
return 'Not Yet Added to Provider';
|
||||
case RealDebridStatus.Downloading:
|
||||
if (torrent.rdSeeders < 1 && torrent.type !== 1) {
|
||||
return 'Torrent stalled';
|
||||
}
|
||||
|
||||
return `Torrent downloading (${torrent.rdProgress}% - ${fileSizePipe.transform(torrent.rdSpeed, 'filesize') as string}/s)`;
|
||||
case RealDebridStatus.Processing:
|
||||
return 'Torrent processing';
|
||||
case RealDebridStatus.WaitingForFileSelection:
|
||||
return 'Torrent waiting for file selection';
|
||||
case RealDebridStatus.Error:
|
||||
return `Torrent error: ${torrent.rdStatusRaw}`;
|
||||
case RealDebridStatus.Finished:
|
||||
return 'Torrent finished, waiting for download links';
|
||||
case RealDebridStatus.Uploading:
|
||||
return 'Torrent uploading';
|
||||
default:
|
||||
return 'Unknown status';
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({ name: 'status' })
|
||||
export class TorrentStatusPipe implements PipeTransform {
|
||||
transform(torrent: Torrent): string {
|
||||
return getTorrentStatus(torrent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,14 +47,10 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (torrent of torrents | sort: sortProperty : sortDirection; track torrent.torrentId) {
|
||||
@for (torrent of sortedTorrents; track torrent.torrentId) {
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelect(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.includes(torrent.torrentId)"
|
||||
/>
|
||||
<input type="checkbox" (click)="toggleSelect(torrent.torrentId)" [checked]="isSelected(torrent.torrentId)" />
|
||||
</td>
|
||||
<td (click)="openTorrent(torrent.torrentId)" class="break-all">
|
||||
{{ torrent.rdName }}
|
||||
|
|
@ -69,10 +65,10 @@
|
|||
{{ torrent.rdSeeders }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.files.length | number }}
|
||||
{{ (torrent.filesCount ?? torrent.files?.length ?? 0) | number }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.downloads.length | number }}
|
||||
{{ (torrent.downloadsCount ?? torrent.downloads?.length ?? 0) | number }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.rdSize | filesize }}
|
||||
|
|
@ -81,7 +77,7 @@
|
|||
{{ torrent.added | date: "medium" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent | status }}
|
||||
{{ torrent.statusText ?? (torrent | status) }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
|
@ -89,19 +85,19 @@
|
|||
</table>
|
||||
} @else {
|
||||
<div class="mobile-cards">
|
||||
@for (torrent of torrents | sort: sortProperty : sortDirection; track torrent.torrentId) {
|
||||
<div class="box mobile-card" [class.is-selected]="selectedTorrents.includes(torrent.torrentId)">
|
||||
@for (torrent of sortedTorrents; track torrent.torrentId) {
|
||||
<div class="box mobile-card" [class.is-selected]="isSelected(torrent.torrentId)">
|
||||
<div class="mobile-card-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelect(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.includes(torrent.torrentId)"
|
||||
[checked]="isSelected(torrent.torrentId)"
|
||||
/>
|
||||
</div>
|
||||
<div class="mobile-card-body" (click)="openTorrent(torrent.torrentId)">
|
||||
<p class="mobile-card-name">{{ torrent.rdName }}</p>
|
||||
<div class="tags mb-2">
|
||||
<span class="tag is-info is-light">{{ torrent | status }}</span>
|
||||
<span class="tag is-info is-light">{{ torrent.statusText ?? (torrent | status) }}</span>
|
||||
@if (torrent.category) {
|
||||
<span class="tag is-light">{{ torrent.category }}</span>
|
||||
}
|
||||
|
|
@ -112,10 +108,10 @@
|
|||
>
|
||||
<span class="mobile-card-detail"><span class="detail-label">Seeders</span> {{ torrent.rdSeeders }}</span>
|
||||
<span class="mobile-card-detail"
|
||||
><span class="detail-label">Files</span> {{ torrent.files.length | number }}</span
|
||||
><span class="detail-label">Files</span> {{ (torrent.filesCount ?? torrent.files?.length ?? 0) | number }}</span
|
||||
>
|
||||
<span class="mobile-card-detail"
|
||||
><span class="detail-label">Downloads</span> {{ torrent.downloads.length | number }}</span
|
||||
><span class="detail-label">Downloads</span> {{ (torrent.downloadsCount ?? torrent.downloads?.length ?? 0) | number }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { Torrent } from '../models/torrent.model';
|
||||
import { DiskSpaceStatus } from '../models/disk-space-status.model';
|
||||
|
|
@ -7,26 +8,29 @@ import { TorrentService } from '../torrent.service';
|
|||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass, DecimalPipe, DatePipe } from '@angular/common';
|
||||
import { TorrentStatusPipe } from '../torrent-status.pipe';
|
||||
import { SortPipe } from '../sort.pipe';
|
||||
import { getTorrentStatus, TorrentStatusPipe } from '../torrent-status.pipe';
|
||||
import { SortDirection, getSortFieldValue, sortItems } from '../sort.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent-table',
|
||||
templateUrl: './torrent-table.component.html',
|
||||
styleUrls: ['./torrent-table.component.scss'],
|
||||
imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, SortPipe, FileSizePipe],
|
||||
imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, FileSizePipe],
|
||||
standalone: true,
|
||||
})
|
||||
export class TorrentTableComponent implements OnInit, OnDestroy {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private router = inject(Router);
|
||||
private torrentService = inject(TorrentService);
|
||||
private selectedTorrentIds = new Set<string>();
|
||||
|
||||
public torrents: Torrent[] = [];
|
||||
public sortedTorrents: Torrent[] = [];
|
||||
public selectedTorrents: string[] = [];
|
||||
public error: string;
|
||||
public sortProperty = 'added';
|
||||
public sortDirection: 'asc' | 'desc' = 'desc';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
|
||||
public isDeleteModalActive: boolean;
|
||||
public deleteError: string;
|
||||
|
|
@ -82,38 +86,47 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
// Ignore storage errors (e.g., disabled storage)
|
||||
}
|
||||
|
||||
this.torrentService.getDiskSpaceStatus().subscribe({
|
||||
next: (status) => {
|
||||
this.diskSpaceStatus = status;
|
||||
},
|
||||
});
|
||||
this.torrentService
|
||||
.getDiskSpaceStatus()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (status) => {
|
||||
this.diskSpaceStatus = status;
|
||||
},
|
||||
});
|
||||
|
||||
this.torrentService.diskSpaceStatus$.subscribe((status) => {
|
||||
this.torrentService.diskSpaceStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((status) => {
|
||||
this.diskSpaceStatus = status;
|
||||
});
|
||||
|
||||
this.torrentService.getRateLimitStatus().subscribe({
|
||||
next: (status) => {
|
||||
this.rateLimitStatus = status;
|
||||
},
|
||||
});
|
||||
this.torrentService
|
||||
.getRateLimitStatus()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (status) => {
|
||||
this.rateLimitStatus = status;
|
||||
},
|
||||
});
|
||||
|
||||
this.torrentService.rateLimitStatus$.subscribe((status) => {
|
||||
this.torrentService.rateLimitStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((status) => {
|
||||
this.rateLimitStatus = status;
|
||||
});
|
||||
|
||||
this.torrentService.update$.subscribe((result) => {
|
||||
this.torrents = result;
|
||||
this.torrentService.update$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((result) => {
|
||||
this.setTorrents(result);
|
||||
});
|
||||
|
||||
this.torrentService.getList().subscribe({
|
||||
next: (result) => {
|
||||
this.torrents = result;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
},
|
||||
});
|
||||
this.torrentService
|
||||
.getList()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.setTorrents(result);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public sort(property: string): void {
|
||||
|
|
@ -131,30 +144,33 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
} catch (_) {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
this.applySorting();
|
||||
}
|
||||
|
||||
public openTorrent(torrentId: string): void {
|
||||
this.router.navigate([`/torrent/${torrentId}`]);
|
||||
}
|
||||
|
||||
public toggleDeleteSelectAll(event: any) {
|
||||
this.selectedTorrents = [];
|
||||
public toggleDeleteSelectAll(event: Event) {
|
||||
const checked = (event.target as HTMLInputElement).checked;
|
||||
|
||||
if (event.target.checked) {
|
||||
this.torrents.map((torrent) => {
|
||||
this.selectedTorrents.push(torrent.torrentId);
|
||||
});
|
||||
}
|
||||
this.selectedTorrentIds = checked ? new Set(this.torrents.map((torrent) => torrent.torrentId)) : new Set<string>();
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
public toggleSelect(torrentId: string) {
|
||||
const index = this.selectedTorrents.indexOf(torrentId);
|
||||
|
||||
if (index > -1) {
|
||||
this.selectedTorrents.splice(index, 1);
|
||||
if (this.selectedTorrentIds.has(torrentId)) {
|
||||
this.selectedTorrentIds.delete(torrentId);
|
||||
} else {
|
||||
this.selectedTorrents.push(torrentId);
|
||||
this.selectedTorrentIds.add(torrentId);
|
||||
}
|
||||
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
public isSelected(torrentId: string): boolean {
|
||||
return this.selectedTorrentIds.has(torrentId);
|
||||
}
|
||||
|
||||
public showDeleteModal(): void {
|
||||
|
|
@ -184,7 +200,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
this.isDeleteModalActive = false;
|
||||
this.deleting = false;
|
||||
|
||||
this.selectedTorrents = [];
|
||||
this.clearSelectedTorrents();
|
||||
},
|
||||
error: (err) => {
|
||||
this.deleteError = err.error;
|
||||
|
|
@ -217,7 +233,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
this.isRetryModalActive = false;
|
||||
this.retrying = false;
|
||||
|
||||
this.selectedTorrents = [];
|
||||
this.clearSelectedTorrents();
|
||||
},
|
||||
error: (err) => {
|
||||
this.retryError = err.error;
|
||||
|
|
@ -229,7 +245,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
public changeSettingsModal(): void {
|
||||
this.changeSettingsError = null;
|
||||
|
||||
const selectedTorrents = this.torrents.filter((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
const selectedTorrents = this.getSelectedTorrentModels();
|
||||
|
||||
this.updateSettingsDownloadClient = selectedTorrents.every(
|
||||
(m, _, arr) => m.downloadClient === arr[0].downloadClient,
|
||||
|
|
@ -276,7 +292,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
|
||||
const calls: Observable<void>[] = [];
|
||||
|
||||
const selectedTorrents = this.torrents.filter((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
const selectedTorrents = this.getSelectedTorrentModels();
|
||||
|
||||
selectedTorrents.forEach((torrent) => {
|
||||
if (this.updateSettingsDownloadClient != null) {
|
||||
|
|
@ -292,7 +308,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
torrent.priority = this.updateSettingsPriority;
|
||||
}
|
||||
if (this.updateSettingsDownloadRetryAttempts != null) {
|
||||
torrent.retryCount = this.updateSettingsDownloadRetryAttempts;
|
||||
torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
|
||||
}
|
||||
if (this.updateSettingsTorrentRetryAttempts != null) {
|
||||
torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
|
||||
|
|
@ -312,7 +328,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
this.isChangeSettingsModalActive = false;
|
||||
this.changingSettings = false;
|
||||
|
||||
this.selectedTorrents = [];
|
||||
this.clearSelectedTorrents();
|
||||
},
|
||||
error: (err) => {
|
||||
this.changeSettingsError = err.error;
|
||||
|
|
@ -333,4 +349,52 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
ngOnDestroy(): void {
|
||||
this.mobileQuery?.removeEventListener('change', this.mobileQueryListener);
|
||||
}
|
||||
|
||||
private setTorrents(torrents: Torrent[]): void {
|
||||
this.torrents = torrents;
|
||||
this.pruneSelectedTorrents();
|
||||
this.applySorting();
|
||||
}
|
||||
|
||||
private applySorting(): void {
|
||||
this.sortedTorrents = sortItems(this.torrents, this.sortProperty, this.sortDirection, (torrent, field) => {
|
||||
switch (field) {
|
||||
case 'files.length':
|
||||
return torrent.filesCount ?? torrent.files?.length ?? 0;
|
||||
case 'downloads.length':
|
||||
return torrent.downloadsCount ?? torrent.downloads?.length ?? 0;
|
||||
case 'status':
|
||||
return torrent.statusText ?? getTorrentStatus(torrent);
|
||||
default:
|
||||
return getSortFieldValue(torrent, field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSelectedTorrentModels(): Torrent[] {
|
||||
return this.torrents.filter((torrent) => this.selectedTorrentIds.has(torrent.torrentId));
|
||||
}
|
||||
|
||||
private pruneSelectedTorrents(): void {
|
||||
const torrentIds = new Set(this.torrents.map((torrent) => torrent.torrentId));
|
||||
|
||||
for (const torrentId of this.selectedTorrentIds) {
|
||||
if (!torrentIds.has(torrentId)) {
|
||||
this.selectedTorrentIds.delete(torrentId);
|
||||
}
|
||||
}
|
||||
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
private clearSelectedTorrents(): void {
|
||||
this.selectedTorrentIds.clear();
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
private syncSelectedTorrents(): void {
|
||||
this.selectedTorrents = this.torrents
|
||||
.filter((torrent) => this.selectedTorrentIds.has(torrent.torrentId))
|
||||
.map((torrent) => torrent.torrentId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { saveAs } from 'file-saver-es';
|
||||
import { Torrent } from '../models/torrent.model';
|
||||
|
|
@ -10,6 +11,7 @@ import { TorrentStatusPipe } from '../torrent-status.pipe';
|
|||
import { DownloadStatusPipe } from '../download-status.pipe';
|
||||
import { DecodeURIPipe } from '../decode-uri.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
import { EMPTY, distinctUntilChanged, map, switchMap, tap, catchError } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent',
|
||||
|
|
@ -28,9 +30,11 @@ import { FileSizePipe } from '../filesize.pipe';
|
|||
standalone: true,
|
||||
})
|
||||
export class TorrentComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private torrentService = inject(TorrentService);
|
||||
private currentTorrentId: string | null = null;
|
||||
|
||||
public torrent: Torrent;
|
||||
|
||||
|
|
@ -71,41 +75,65 @@ export class TorrentComponent implements OnInit {
|
|||
public updating: boolean;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.params.subscribe((params) => {
|
||||
const torrentId = params['id'];
|
||||
|
||||
this.torrentService.get(torrentId).subscribe({
|
||||
next: (torrent) => {
|
||||
this.torrent = torrent;
|
||||
|
||||
this.torrentService.update$.subscribe((result) => {
|
||||
this.update(result);
|
||||
});
|
||||
},
|
||||
error: () => this.router.navigate(['/']),
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
map((params) => params['id'] as string),
|
||||
distinctUntilChanged(),
|
||||
tap((torrentId) => {
|
||||
this.currentTorrentId = torrentId;
|
||||
}),
|
||||
switchMap((torrentId) =>
|
||||
this.torrentService.get(torrentId).pipe(
|
||||
catchError(() => {
|
||||
this.router.navigate(['/']);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((torrent) => {
|
||||
this.torrent = torrent;
|
||||
this.currentTorrentId = torrent.torrentId;
|
||||
});
|
||||
|
||||
this.torrentService.update$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((result) => {
|
||||
this.update(result);
|
||||
});
|
||||
}
|
||||
|
||||
public update(torrents: Torrent[]): void {
|
||||
const updatedTorrent = torrents.find((m) => m.torrentId === this.torrent.torrentId);
|
||||
const torrentId = this.currentTorrentId ?? this.torrent?.torrentId;
|
||||
|
||||
if (!torrentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTorrent = torrents.find((m) => m.torrentId === torrentId);
|
||||
|
||||
if (updatedTorrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.torrent = updatedTorrent;
|
||||
const currentTorrent = this.torrent;
|
||||
const hasIncomingFiles = Array.isArray(updatedTorrent.files) && updatedTorrent.files.length > 0;
|
||||
|
||||
this.torrent = {
|
||||
...currentTorrent,
|
||||
...updatedTorrent,
|
||||
fileOrMagnet: updatedTorrent.fileOrMagnet ?? currentTorrent?.fileOrMagnet,
|
||||
files: hasIncomingFiles ? updatedTorrent.files : currentTorrent?.files ?? [],
|
||||
downloads: updatedTorrent.downloads ?? currentTorrent?.downloads ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
public download(): void {
|
||||
const byteArray = new Uint8Array(
|
||||
window
|
||||
.atob(this.torrent.fileOrMagnet)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return c.charCodeAt(0);
|
||||
}),
|
||||
);
|
||||
const binaryString = window.atob(this.torrent.fileOrMagnet);
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
|
||||
for (let index = 0; index < binaryString.length; index += 1) {
|
||||
byteArray[index] = binaryString.charCodeAt(index);
|
||||
}
|
||||
|
||||
const blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
|
||||
saveAs(blob, `${this.torrent.rdName}.torrent`);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
|||
public async Task<IList<Torrent>> Get()
|
||||
{
|
||||
var torrents = await dataContext.Torrents
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(m => m.Downloads)
|
||||
.ToListAsync();
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(m => m.Downloads)
|
||||
.OrderBy(m => m.Priority ?? 9999)
|
||||
.ToListAsync();
|
||||
|
||||
return torrents.OrderBy(m => m.Priority ?? 9999)
|
||||
.ThenBy(m => m.Added)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ namespace RdtClient.Data.Models.Data;
|
|||
|
||||
public class Torrent
|
||||
{
|
||||
private String? _rdFiles;
|
||||
private IList<DebridClientFile> _filesCache = [];
|
||||
private Boolean _filesCacheInitialized;
|
||||
|
||||
[Key]
|
||||
public Guid TorrentId { get; set; }
|
||||
|
||||
|
|
@ -59,26 +63,51 @@ public class Torrent
|
|||
public DateTimeOffset? RdEnded { get; set; }
|
||||
public Int64? RdSpeed { get; set; }
|
||||
public Int64? RdSeeders { get; set; }
|
||||
public String? RdFiles { get; set; }
|
||||
public String? RdFiles
|
||||
{
|
||||
get => _rdFiles;
|
||||
set
|
||||
{
|
||||
if (_rdFiles == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rdFiles = value;
|
||||
_filesCache = [];
|
||||
_filesCacheInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public IList<DebridClientFile> Files
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filesCacheInitialized)
|
||||
{
|
||||
return _filesCache;
|
||||
}
|
||||
|
||||
_filesCacheInitialized = true;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(RdFiles))
|
||||
{
|
||||
return [];
|
||||
_filesCache = [];
|
||||
|
||||
return _filesCache;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<DebridClientFile>>(RdFiles) ?? [];
|
||||
_filesCache = JsonSerializer.Deserialize<List<DebridClientFile>>(RdFiles) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
_filesCache = [];
|
||||
}
|
||||
|
||||
return _filesCache;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public class TorrentDto
|
|||
public DateTimeOffset? FilesSelected { get; set; }
|
||||
public DateTimeOffset? Completed { get; set; }
|
||||
public DownloadType Type { get; set; }
|
||||
public String? FileOrMagnet { get; set; }
|
||||
public Boolean IsFile { get; set; }
|
||||
public Int32? Priority { get; set; }
|
||||
public Int32 RetryCount { get; set; }
|
||||
|
|
@ -41,6 +42,9 @@ public class TorrentDto
|
|||
public DateTimeOffset? RdEnded { get; set; }
|
||||
public Int64? RdSpeed { get; set; }
|
||||
public Int64? RdSeeders { get; set; }
|
||||
public String StatusText { get; set; } = null!;
|
||||
public Int32 FilesCount { get; set; }
|
||||
public Int32 DownloadsCount { get; set; }
|
||||
public IList<DebridClientFile> Files { get; set; } = [];
|
||||
public IList<DownloadDto> Downloads { get; set; } = [];
|
||||
}
|
||||
|
|
|
|||
230
server/RdtClient.Service/Helpers/TorrentDtoMapper.cs
Normal file
230
server/RdtClient.Service/Helpers/TorrentDtoMapper.cs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
|
||||
namespace RdtClient.Service.Helpers;
|
||||
|
||||
public static class TorrentDtoMapper
|
||||
{
|
||||
public static TorrentDto ToListDto(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
return ToDto(torrent, getDownloadStats, includeDownloads: false, includeFiles: false, includeFileOrMagnet: false);
|
||||
}
|
||||
|
||||
public static TorrentDto ToUpdateDto(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
return ToDto(torrent, getDownloadStats, includeDownloads: true, includeFiles: false, includeFileOrMagnet: false);
|
||||
}
|
||||
|
||||
public static TorrentDto ToDetailDto(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
return ToDto(torrent, getDownloadStats, includeDownloads: true, includeFiles: true, includeFileOrMagnet: true);
|
||||
}
|
||||
|
||||
private static TorrentDto ToDto(Torrent torrent,
|
||||
Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats,
|
||||
Boolean includeDownloads,
|
||||
Boolean includeFiles,
|
||||
Boolean includeFileOrMagnet)
|
||||
{
|
||||
var downloads = includeDownloads ? torrent.Downloads.Select(download => ToDto(download, getDownloadStats)).ToList() : [];
|
||||
|
||||
return new()
|
||||
{
|
||||
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,
|
||||
FileOrMagnet = includeFileOrMagnet ? torrent.FileOrMagnet : null,
|
||||
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,
|
||||
StatusText = GetStatusText(torrent, getDownloadStats),
|
||||
FilesCount = torrent.Files.Count,
|
||||
DownloadsCount = torrent.Downloads.Count,
|
||||
Files = includeFiles ? torrent.Files : [],
|
||||
Downloads = downloads
|
||||
};
|
||||
}
|
||||
|
||||
private static DownloadDto ToDto(Download download, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = getDownloadStats(download.DownloadId);
|
||||
|
||||
return new()
|
||||
{
|
||||
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 = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
}
|
||||
|
||||
private static String GetStatusText(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.Error))
|
||||
{
|
||||
return torrent.Error;
|
||||
}
|
||||
|
||||
if (torrent.Downloads.Count > 0)
|
||||
{
|
||||
var allFinished = true;
|
||||
var downloadingCount = 0;
|
||||
var downloadedCount = 0;
|
||||
Int64 downloadingBytesDone = 0;
|
||||
Int64 downloadingBytesTotal = 0;
|
||||
Int64 downloadingSpeed = 0;
|
||||
var unpackingCount = 0;
|
||||
var unpackedCount = 0;
|
||||
Int64 unpackingBytesDone = 0;
|
||||
Int64 unpackingBytesTotal = 0;
|
||||
var queuedForUnpackingCount = 0;
|
||||
var queuedForDownloadingCount = 0;
|
||||
|
||||
foreach (var download in torrent.Downloads)
|
||||
{
|
||||
if (download.Completed == null)
|
||||
{
|
||||
allFinished = false;
|
||||
}
|
||||
|
||||
var (speed, bytesTotal, bytesDone) = getDownloadStats(download.DownloadId);
|
||||
|
||||
if (download.DownloadFinished != null)
|
||||
{
|
||||
downloadedCount += 1;
|
||||
}
|
||||
|
||||
if (download.DownloadStarted != null && download.DownloadFinished == null && bytesDone > 0)
|
||||
{
|
||||
downloadingCount += 1;
|
||||
downloadingBytesDone += bytesDone;
|
||||
downloadingBytesTotal += bytesTotal;
|
||||
downloadingSpeed += speed;
|
||||
}
|
||||
|
||||
if (download.UnpackingFinished != null)
|
||||
{
|
||||
unpackedCount += 1;
|
||||
}
|
||||
|
||||
if (download.UnpackingStarted != null && download.UnpackingFinished == null && bytesDone > 0)
|
||||
{
|
||||
unpackingCount += 1;
|
||||
unpackingBytesDone += bytesDone;
|
||||
unpackingBytesTotal += bytesTotal;
|
||||
}
|
||||
|
||||
if (download.UnpackingQueued != null && download.UnpackingStarted == null)
|
||||
{
|
||||
queuedForUnpackingCount += 1;
|
||||
}
|
||||
|
||||
if (download.DownloadStarted == null && download.DownloadFinished == null)
|
||||
{
|
||||
queuedForDownloadingCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (allFinished)
|
||||
{
|
||||
return "Finished";
|
||||
}
|
||||
|
||||
if (downloadingCount > 0)
|
||||
{
|
||||
var progress = downloadingBytesTotal == 0 ? 0 : (Double)downloadingBytesDone / downloadingBytesTotal * 100;
|
||||
|
||||
return $"Downloading file {downloadingCount + downloadedCount}/{torrent.Downloads.Count} ({progress:0.00}% - {FileSizeHelper.FormatSize(downloadingSpeed)}/s)";
|
||||
}
|
||||
|
||||
if (unpackingCount > 0)
|
||||
{
|
||||
var progress = unpackingBytesTotal == 0 ? 0 : (Double)unpackingBytesDone / unpackingBytesTotal * 100;
|
||||
|
||||
return $"Extracting file {unpackingCount + unpackedCount}/{torrent.Downloads.Count} ({progress:0.00}%)";
|
||||
}
|
||||
|
||||
if (queuedForUnpackingCount > 0)
|
||||
{
|
||||
return "Queued for unpacking";
|
||||
}
|
||||
|
||||
if (queuedForDownloadingCount > 0)
|
||||
{
|
||||
return "Queued for downloading";
|
||||
}
|
||||
|
||||
if (unpackedCount > 0)
|
||||
{
|
||||
return "Files unpacked";
|
||||
}
|
||||
|
||||
if (downloadedCount > 0)
|
||||
{
|
||||
return "Files downloaded to host";
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent.Completed != null)
|
||||
{
|
||||
return "Finished";
|
||||
}
|
||||
|
||||
return torrent.RdStatus switch
|
||||
{
|
||||
TorrentStatus.Queued => "Not Yet Added to Provider",
|
||||
TorrentStatus.Downloading when torrent.RdSeeders < 1 && torrent.Type != DownloadType.Nzb => "Torrent stalled",
|
||||
TorrentStatus.Downloading => $"Torrent downloading ({torrent.RdProgress}% - {FileSizeHelper.FormatSize(torrent.RdSpeed)}/s)",
|
||||
TorrentStatus.Processing => "Torrent processing",
|
||||
TorrentStatus.WaitingForFileSelection => "Torrent waiting for file selection",
|
||||
TorrentStatus.Error => $"Torrent error: {torrent.RdStatusRaw}",
|
||||
TorrentStatus.Finished => "Torrent finished, waiting for download links",
|
||||
TorrentStatus.Uploading => "Torrent uploading",
|
||||
_ => "Unknown status"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Helpers;
|
||||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
||||
|
|
@ -9,72 +10,7 @@ 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,
|
||||
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 =>
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
|
||||
|
||||
return 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 = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
var torrentDtos = allTorrents.Select(torrent => TorrentDtoMapper.ToUpdateDto(torrent, torrents.GetDownloadStats))
|
||||
.ToList();
|
||||
|
||||
await hub.Clients.All.SendCoreAsync("update",
|
||||
|
|
|
|||
|
|
@ -285,13 +285,12 @@ public class TorrentRunner(
|
|||
}
|
||||
|
||||
var allTorrents = await torrents.Get();
|
||||
var downloadsById = allTorrents.SelectMany(m => m.Downloads).ToDictionary(m => m.DownloadId, m => m);
|
||||
|
||||
// Check for deleted torrents that are stuck in the ActiveDownloads or ActiveUnpacks
|
||||
foreach (var activeDownload in ActiveDownloadClients)
|
||||
{
|
||||
var download = allTorrents.SelectMany(m => m.Downloads).FirstOrDefault(m => m.DownloadId == activeDownload.Key);
|
||||
|
||||
if (download == null)
|
||||
if (!downloadsById.ContainsKey(activeDownload.Key))
|
||||
{
|
||||
await activeDownload.Value.Cancel();
|
||||
ActiveDownloadClients.TryRemove(activeDownload.Key, out _);
|
||||
|
|
@ -302,9 +301,7 @@ public class TorrentRunner(
|
|||
|
||||
foreach (var activeUnpacks in ActiveUnpackClients)
|
||||
{
|
||||
var download = allTorrents.SelectMany(m => m.Downloads).FirstOrDefault(m => m.DownloadId == activeUnpacks.Key);
|
||||
|
||||
if (download == null)
|
||||
if (!downloadsById.ContainsKey(activeUnpacks.Key))
|
||||
{
|
||||
activeUnpacks.Value.Cancel();
|
||||
ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.IO.Abstractions;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -36,11 +35,6 @@ public class Torrents(
|
|||
{
|
||||
private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
|
||||
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
|
||||
|
||||
private IDebridClient DebridClient
|
||||
|
|
@ -656,10 +650,12 @@ public class Torrents(
|
|||
try
|
||||
{
|
||||
var rdTorrents = await DebridClient.GetDownloads();
|
||||
var torrentsByRdId = CreateTorrentLookupByRdId(torrents);
|
||||
var providerTorrentsById = CreateProviderTorrentLookupById(rdTorrents);
|
||||
|
||||
foreach (var rdTorrent in rdTorrents)
|
||||
{
|
||||
var torrent = torrents.FirstOrDefault(m => m.RdId == rdTorrent.Id);
|
||||
torrentsByRdId.TryGetValue(rdTorrent.Id, out var torrent);
|
||||
|
||||
// Auto import torrents only torrents that have their files selected
|
||||
if (torrent == null && Settings.Get.Provider.AutoImport)
|
||||
|
|
@ -690,6 +686,8 @@ public class Torrents(
|
|||
}
|
||||
|
||||
torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, DownloadType.Torrent, Settings.Get.DownloadClient.Client, newTorrent);
|
||||
torrentsByRdId[rdTorrent.Id] = torrent;
|
||||
torrents.Add(torrent);
|
||||
|
||||
await UpdateTorrentClientData(torrent, rdTorrent);
|
||||
}
|
||||
|
|
@ -701,7 +699,7 @@ public class Torrents(
|
|||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
var rdTorrent = rdTorrents.FirstOrDefault(m => m.Id == torrent.RdId);
|
||||
var rdTorrent = torrent.RdId != null && providerTorrentsById.TryGetValue(torrent.RdId, out var providerTorrent) ? providerTorrent : null;
|
||||
|
||||
if (rdTorrent == null && Settings.Get.Provider.AutoDelete && torrent.RdStatus != TorrentStatus.Queued)
|
||||
{
|
||||
|
|
@ -1069,11 +1067,11 @@ public class Torrents(
|
|||
{
|
||||
try
|
||||
{
|
||||
var originalTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
|
||||
var originalTorrent = CaptureRdState(torrent);
|
||||
|
||||
await DebridClient.UpdateData(torrent, torrentClientTorrent);
|
||||
|
||||
var newTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
|
||||
var newTorrent = CaptureRdState(torrent);
|
||||
|
||||
if (originalTorrent != newTorrent)
|
||||
{
|
||||
|
|
@ -1086,6 +1084,65 @@ public class Torrents(
|
|||
}
|
||||
}
|
||||
|
||||
private static Dictionary<String, Torrent> CreateTorrentLookupByRdId(IEnumerable<Torrent> torrents)
|
||||
{
|
||||
var lookup = new Dictionary<String, Torrent>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.RdId))
|
||||
{
|
||||
lookup[torrent.RdId] = torrent;
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private static Dictionary<String, DebridClientTorrent> CreateProviderTorrentLookupById(IEnumerable<DebridClientTorrent> torrents)
|
||||
{
|
||||
var lookup = new Dictionary<String, DebridClientTorrent>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.Id))
|
||||
{
|
||||
lookup[torrent.Id] = torrent;
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private static TorrentRdState CaptureRdState(Torrent torrent)
|
||||
{
|
||||
return new(torrent.RdName,
|
||||
torrent.RdSize,
|
||||
torrent.RdHost,
|
||||
torrent.RdSplit,
|
||||
torrent.RdProgress,
|
||||
torrent.RdStatus,
|
||||
torrent.RdStatusRaw,
|
||||
torrent.RdAdded,
|
||||
torrent.RdEnded,
|
||||
torrent.RdSpeed,
|
||||
torrent.RdSeeders,
|
||||
torrent.RdFiles);
|
||||
}
|
||||
|
||||
private readonly record struct TorrentRdState(String? RdName,
|
||||
Int64? RdSize,
|
||||
String? RdHost,
|
||||
Int64? RdSplit,
|
||||
Int64? RdProgress,
|
||||
TorrentStatus? RdStatus,
|
||||
String? RdStatusRaw,
|
||||
DateTimeOffset? RdAdded,
|
||||
DateTimeOffset? RdEnded,
|
||||
Int64? RdSpeed,
|
||||
Int64? RdSeeders,
|
||||
String? RdFiles);
|
||||
|
||||
private void Log(String message, Download? download, Torrent? torrent)
|
||||
{
|
||||
if (download != null)
|
||||
|
|
|
|||
|
|
@ -21,72 +21,7 @@ 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,
|
||||
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 =>
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
|
||||
|
||||
return 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 = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
var torrentDtos = results.Select(torrent => TorrentDtoMapper.ToListDto(torrent, torrents.GetDownloadStats))
|
||||
.ToList();
|
||||
|
||||
return Ok(torrentDtos);
|
||||
|
|
@ -108,72 +43,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
file.Torrent = null;
|
||||
}
|
||||
|
||||
var torrentDto = 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,
|
||||
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 =>
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
|
||||
|
||||
return 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 = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
var torrentDto = TorrentDtoMapper.ToDetailDto(torrent, torrents.GetDownloadStats);
|
||||
|
||||
return Ok(torrentDto);
|
||||
}
|
||||
|
|
@ -240,13 +110,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
|
||||
logger.LogDebug($"Add file");
|
||||
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
|
||||
await torrents.AddFileToDebridQueue(bytes, formData.Torrent);
|
||||
|
||||
|
|
@ -302,13 +166,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
formData.Torrent.RdName = file.FileName;
|
||||
}
|
||||
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
|
||||
await torrents.AddNzbFileToDebridQueue(bytes, file.FileName, formData.Torrent);
|
||||
|
||||
|
|
@ -350,13 +208,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
return BadRequest("Invalid torrent file");
|
||||
}
|
||||
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
|
||||
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
|
||||
|
||||
|
|
@ -461,13 +313,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
}
|
||||
else if (file != null)
|
||||
{
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
|
||||
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
|
||||
|
||||
|
|
@ -482,36 +328,32 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
|
||||
if (!String.IsNullOrWhiteSpace(request.IncludeRegex))
|
||||
{
|
||||
foreach (var availableFile in availableFiles)
|
||||
var includeRegex = BuildRegex(request.IncludeRegex, out includeError);
|
||||
|
||||
if (includeRegex != null)
|
||||
{
|
||||
try
|
||||
foreach (var availableFile in availableFiles)
|
||||
{
|
||||
if (Regex.IsMatch(availableFile.Filename, request.IncludeRegex))
|
||||
if (includeRegex.IsMatch(availableFile.Filename))
|
||||
{
|
||||
selectedFiles.Add(availableFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
includeError = ex.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrWhiteSpace(request.ExcludeRegex))
|
||||
{
|
||||
foreach (var availableFile in availableFiles)
|
||||
var excludeRegex = BuildRegex(request.ExcludeRegex, out excludeError);
|
||||
|
||||
if (excludeRegex != null)
|
||||
{
|
||||
try
|
||||
foreach (var availableFile in availableFiles)
|
||||
{
|
||||
if (!Regex.IsMatch(availableFile.Filename, request.ExcludeRegex))
|
||||
if (!excludeRegex.IsMatch(availableFile.Filename))
|
||||
{
|
||||
selectedFiles.Add(availableFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
excludeError = ex.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -526,6 +368,32 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
selectedFiles
|
||||
});
|
||||
}
|
||||
|
||||
private static Regex? BuildRegex(String pattern, out String error)
|
||||
{
|
||||
try
|
||||
{
|
||||
error = "";
|
||||
|
||||
return new(pattern, RegexOptions.Compiled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Byte[]> ReadFormFileBytes(IFormFile file)
|
||||
{
|
||||
await using var fileStream = file.OpenReadStream();
|
||||
await using var memoryStream = file.Length > 0 && file.Length <= Int32.MaxValue ? new MemoryStream((Int32)file.Length) : new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class TorrentControllerUploadFileRequest
|
||||
|
|
|
|||
Loading…
Reference in a new issue