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