Add Deluge integration; revamp admin Jobs & Logs UI

Introduce Deluge download client service and tests, remove obsolete rdtclient service, and update qbittorrent integration/tests and download-client interfaces/manager. Large UI refactor for admin pages: Jobs and Logs were redesigned to be responsive (mobile card views + desktop tables), improved headers, dialogs, controls, and better status/detail rendering. Also updated DownloadClient components (card, management, modal), organize-files processor, audible-series integration, and related unit tests to align with integration changes. Minor UX and accessibility tweaks, cron handling/validation adjustments, and a few formatting/cleanup fixes throughout.
This commit is contained in:
kikootwo
2026-02-20 20:44:26 -05:00
parent 04dbb05a6e
commit d70f6c9957
22 changed files with 1742 additions and 679 deletions
+5 -3
View File
@@ -414,10 +414,12 @@ function parseSeriesBooks(
if (!bookAsin || seenAsins.has(bookAsin)) return;
seenAsins.add(bookAsin);
// Title
const title = $el.find('h2').first().text().trim() ||
$el.find('h3 a').first().text().trim() ||
// Title: h3 a / .bc-heading a hold the real book title;
// h2 on series pages is the position label ("Book 1"), so try it last.
const title = $el.find('h3 a').first().text().trim() ||
$el.find('.bc-heading a').first().text().trim() ||
$el.find('h2 a').first().text().trim() ||
$el.find('h2').first().text().trim() ||
'';
if (!title) return;
+385
View File
@@ -0,0 +1,385 @@
/**
* Component: Deluge Integration Service
* Documentation: documentation/phase3/download-clients.md
*/
import axios, { AxiosInstance } from 'axios';
import https from 'https';
import path from 'path';
import * as parseTorrentModule from 'parse-torrent';
import { RMABLogger } from '../utils/logger';
import { PathMapper, PathMappingConfig } from '../utils/path-mapper';
import {
IDownloadClient, DownloadClientType, ProtocolType,
DownloadInfo, DownloadStatus, AddDownloadOptions, ConnectionTestResult,
} from '../interfaces/download-client.interface';
const parseTorrent = (parseTorrentModule as any).default || parseTorrentModule;
const logger = RMABLogger.create('Deluge');
export class DelugeService implements IDownloadClient {
readonly clientType: DownloadClientType = 'deluge';
readonly protocol: ProtocolType = 'torrent';
private client: AxiosInstance;
private baseUrl: string;
private password: string;
private defaultSavePath: string;
private defaultCategory: string;
private pathMappingConfig: PathMappingConfig;
private sessionCookie: string = '';
private requestId: number = 0;
constructor(
baseUrl: string,
_username: string, // Unused — Deluge uses password-only auth; kept for consistent signature
password: string,
defaultSavePath: string = '/downloads',
defaultCategory: string = 'readmeabook',
disableSSLVerify: boolean = false,
pathMappingConfig?: PathMappingConfig
) {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.password = password;
this.defaultSavePath = defaultSavePath;
this.defaultCategory = defaultCategory;
this.pathMappingConfig = pathMappingConfig || { enabled: false, remotePath: '', localPath: '' };
const httpsAgent = disableSSLVerify && this.baseUrl.startsWith('https')
? new https.Agent({ rejectUnauthorized: false }) : undefined;
if (httpsAgent) logger.info('[Deluge] SSL certificate verification disabled');
this.client = axios.create({ baseURL: this.baseUrl, timeout: 30000, httpsAgent });
}
/** JSON-RPC call with automatic re-authentication on auth failure */
private async rpc(method: string, params: any[] = [], retried = false): Promise<any> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.sessionCookie) headers['Cookie'] = this.sessionCookie;
try {
const reqId = ++this.requestId;
const { data } = await this.client.post('/json', { method, params, id: reqId }, { headers });
// Deluge error.code === 1: "Not authenticated" — re-login then retry
if (data.error?.code === 1 && !retried) {
await this.login();
return this.rpc(method, params, true);
}
// Deluge error.code === 2: "Unknown method" — daemon disconnected, force reconnect
// Only retry for core.* methods — plugin methods (label.*) fail because the plugin
// isn't enabled, not because the daemon is disconnected.
if (data.error?.code === 2 && !retried && method.startsWith('core.')) {
await this.login(true);
return this.rpc(method, params, true);
}
return data;
} catch (error) {
if (!retried) { await this.login(); return this.rpc(method, params, true); }
throw error;
}
}
private async login(forceReconnect: boolean = false): Promise<void> {
const { data, headers } = await this.client.post(
'/json',
{ method: 'auth.login', params: [this.password], id: ++this.requestId },
{ headers: { 'Content-Type': 'application/json' } }
);
if (!data?.result) throw new Error('Failed to authenticate with Deluge — check your password');
const cookies = headers['set-cookie'];
if (cookies?.length) this.sessionCookie = cookies[0].split(';')[0];
logger.info('Successfully authenticated with Deluge');
// Deluge Web UI requires a daemon connection before core.* methods work.
// When forceReconnect is true, skip the web.connected check and force a fresh connection.
await this.ensureDaemonConnected(forceReconnect);
}
/**
* Ensure the Web UI is connected to a deluged daemon host.
* Uses web.connected (returns boolean) as the check — daemon.info is NOT a valid
* method through the Deluge Web UI JSON-RPC; only web.* and core.* methods work.
*/
private async ensureDaemonConnected(force: boolean = false): Promise<void> {
if (!force) {
const test = await this.rpc('web.connected', [], true);
if (test.result === true) return;
}
logger.info('Connecting to daemon...');
const hostsData = await this.rpc('web.get_hosts', [], true);
const hosts: any[] = hostsData.result || [];
if (hosts.length === 0) {
throw new Error('Deluge has no daemon hosts configured. Add a host in the Deluge Web UI under Connection Manager.');
}
const hostId = hosts[0][0];
const connectResult = await this.rpc('web.connect', [hostId], true);
if (connectResult.error) {
throw new Error(`Failed to connect to Deluge daemon: ${connectResult.error.message}`);
}
// Verify connection is established
const verify = await this.rpc('web.connected', [], true);
if (verify.result !== true) {
throw new Error('Deluge daemon failed to respond after web.connect. Check that deluged is running.');
}
logger.info('Connected to Deluge daemon');
}
// =========================================================================
// IDownloadClient Implementation
// =========================================================================
async testConnection(): Promise<ConnectionTestResult> {
try {
await this.login();
return { success: true, message: 'Connected to Deluge' };
} catch (error) {
const msg = error instanceof Error ? error.message : 'Connection failed';
if (axios.isAxiosError(error)) {
const c = error.code;
if (c?.includes('CERT') || c?.includes('SSL')) return { success: false, message: `SSL verification failed (${c}). Enable "Disable SSL Verification".` };
if (c === 'ECONNREFUSED') return { success: false, message: `Connection refused at: ${this.baseUrl}` };
if (c === 'ETIMEDOUT' || c === 'ECONNABORTED') return { success: false, message: `Connection timeout: ${this.baseUrl}` };
if (c === 'ENOTFOUND') return { success: false, message: `Host not found: ${this.baseUrl}` };
if (error.response?.status === 401) return { success: false, message: 'Authentication failed. Check your password.' };
}
logger.error('Connection test failed', { error: msg });
return { success: false, message: msg };
}
}
async addDownload(url: string, options?: AddDownloadOptions): Promise<string> {
if (!url || typeof url !== 'string' || url.trim() === '') {
throw new Error('Invalid download URL: URL is required and must be a non-empty string');
}
const category = options?.category || this.defaultCategory;
return url.startsWith('magnet:')
? this.addMagnetLink(url, category, options)
: this.addTorrentFile(url, category, options);
}
private async addMagnetLink(magnetUrl: string, category: string, options?: AddDownloadOptions): Promise<string> {
const infoHash = this.extractHashFromMagnet(magnetUrl);
if (!infoHash) throw new Error('Invalid magnet link - could not extract info_hash');
logger.info(`Extracted info_hash from magnet: ${infoHash}`);
const existing = await this.rpc('core.get_torrent_status', [infoHash, ['name']]);
if (existing.result && Object.keys(existing.result).length > 0) {
logger.info(`Torrent ${infoHash} already exists (duplicate)`);
return infoHash;
}
const opts = this.buildTorrentOptions(options?.paused);
const data = await this.rpc('core.add_torrent_magnet', [magnetUrl, opts]);
if (!data.result) throw new Error(`Deluge rejected magnet link: ${data.error?.message || 'unknown error'}`);
await this.postAddSetup(data.result, category);
logger.info(`Successfully added magnet link: ${infoHash}`);
return infoHash;
}
private async addTorrentFile(torrentUrl: string, category: string, options?: AddDownloadOptions): Promise<string> {
logger.info(`Downloading .torrent file from: ${torrentUrl}`);
let torrentResponse;
try {
torrentResponse = await axios.get(torrentUrl, {
responseType: 'arraybuffer', maxRedirects: 0,
validateStatus: (s) => s >= 200 && s < 300, timeout: 30000,
});
if (torrentResponse.data.length > 0) {
const magnetMatch = torrentResponse.data.toString().match(/^magnet:\?[^\s]+$/);
if (magnetMatch) return this.addMagnetLink(magnetMatch[0], category, options);
}
} catch (error) {
if (!axios.isAxiosError(error) || !error.response) throw error;
const status = error.response.status;
if (status >= 300 && status < 400) {
const loc = error.response.headers['location'];
if (loc?.startsWith('magnet:')) return this.addMagnetLink(loc, category, options);
if (loc?.startsWith('http://') || loc?.startsWith('https://')) {
try { torrentResponse = await axios.get(loc, { responseType: 'arraybuffer', timeout: 30000, maxRedirects: 5 }); }
catch { throw new Error('Failed to download torrent file after redirect'); }
} else { throw new Error(`Invalid redirect location: ${loc}`); }
} else { throw new Error(`Failed to download torrent: HTTP ${status}`); }
}
const torrentBuffer = Buffer.from(torrentResponse.data);
let parsed: any;
try { parsed = await parseTorrent(torrentBuffer); }
catch { throw new Error('Invalid .torrent file - failed to parse'); }
const infoHash = parsed.infoHash;
if (!infoHash) throw new Error('Failed to extract info_hash from .torrent file');
logger.info(`Extracted info_hash: ${infoHash}`);
const existing = await this.rpc('core.get_torrent_status', [infoHash, ['name']]);
if (existing.result && Object.keys(existing.result).length > 0) {
logger.info(`Torrent ${infoHash} already exists (duplicate)`);
return infoHash;
}
const filename = parsed.name ? `${parsed.name}.torrent` : 'torrent.torrent';
const opts = this.buildTorrentOptions(options?.paused);
const data = await this.rpc('core.add_torrent_file', [filename, torrentBuffer.toString('base64'), opts]);
if (!data.result) throw new Error(`Deluge rejected .torrent file: ${data.error?.message || 'unknown error'}`);
await this.postAddSetup(infoHash, category);
logger.info(`Successfully added torrent: ${infoHash}`);
return infoHash;
}
async getDownload(id: string): Promise<DownloadInfo | null> {
const fields = ['name', 'total_size', 'total_done', 'progress', 'state',
'download_payload_rate', 'eta', 'label', 'save_path',
'time_added', 'is_finished', 'seeding_time', 'ratio', 'message'];
for (let attempt = 0; attempt <= 3; attempt++) {
const { result } = await this.rpc('core.get_torrent_status', [id, fields]);
if (result && Object.keys(result).length > 0) return this.mapToDownloadInfo(id, result);
if (attempt === 3) return null;
const delay = 500 * Math.pow(2, attempt);
logger.warn(`Torrent ${id} not found, retrying in ${delay}ms (${attempt + 1}/3)`);
await new Promise(r => setTimeout(r, delay));
}
return null;
}
async pauseDownload(id: string): Promise<void> {
await this.rpc('core.pause_torrent', [[id]]);
logger.info(`Paused torrent: ${id}`);
}
async resumeDownload(id: string): Promise<void> {
await this.rpc('core.resume_torrent', [[id]]);
logger.info(`Resumed torrent: ${id}`);
}
async deleteDownload(id: string, deleteFiles: boolean = false): Promise<void> {
await this.rpc('core.remove_torrent', [id, deleteFiles]);
logger.info(`Deleted torrent: ${id}`);
}
async postProcess(_id: string): Promise<void> {} // No-op: seeding cleanup scheduler manages lifecycle
async getCategories(): Promise<string[]> {
try { const { result } = await this.rpc('label.get_labels'); return Array.isArray(result) ? result : []; }
catch { return []; }
}
async setCategory(id: string, category: string): Promise<void> {
await this.applyLabel(id, category);
logger.info(`Set label for torrent ${id}: ${category}`);
}
// =========================================================================
// Internal Helpers
// =========================================================================
private buildTorrentOptions(paused?: boolean): Record<string, any> {
const remoteSavePath = PathMapper.reverseTransform(this.defaultSavePath, this.pathMappingConfig);
const opts: Record<string, any> = { download_location: remoteSavePath, move_completed: false, move_completed_path: '' };
if (paused) opts.add_paused = true;
return opts;
}
private async postAddSetup(hash: string, category: string): Promise<void> {
await this.disableSeedLimits(hash);
await this.applyLabel(hash, category);
}
private async applyLabel(hash: string, label: string): Promise<void> {
try {
try { await this.rpc('label.add', [label]); } catch { /* may already exist */ }
await this.rpc('label.set_torrent', [hash, label]);
} catch (error) {
logger.warn(`Failed to apply label "${label}" to ${hash}: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async disableSeedLimits(hash: string): Promise<void> {
try {
await this.rpc('core.set_torrent_options', [[hash], { stop_at_ratio: false, seed_time_limit: -1 }]);
} catch (error) {
logger.warn(`Failed to disable seed limits for ${hash}: ${error instanceof Error ? error.message : String(error)}`);
}
}
private mapToDownloadInfo(hash: string, t: Record<string, any>): DownloadInfo {
return {
id: hash, name: t.name || '', size: t.total_size || 0,
bytesDownloaded: t.total_done || 0, progress: (t.progress || 0) / 100,
status: this.mapStatus(t.state), downloadSpeed: t.download_payload_rate || 0,
eta: t.eta > 0 ? t.eta : 0, category: t.label || '',
downloadPath: t.save_path ? path.join(t.save_path, t.name || '') : undefined,
completedAt: t.is_finished && t.time_added ? new Date(t.time_added * 1000) : undefined,
errorMessage: t.message || undefined, seedingTime: t.seeding_time,
ratio: t.ratio >= 0 ? t.ratio : undefined,
};
}
private mapStatus(state: string): DownloadStatus {
const map: Record<string, DownloadStatus> = {
'Downloading': 'downloading', 'Seeding': 'seeding', 'Paused': 'paused',
'Checking': 'checking', 'Queued': 'queued', 'Error': 'failed', 'Moving': 'downloading',
};
return map[state] || 'downloading';
}
private extractHashFromMagnet(magnetUrl: string): string | null {
const match = magnetUrl.match(/xt=urn:btih:([a-fA-F0-9]{40}|[a-zA-Z0-9]{32})/i);
return match ? match[1].toLowerCase() : null;
}
}
// Singleton factory (matches Transmission/qBittorrent pattern)
let delugeServiceInstance: DelugeService | null = null;
let configLoaded = false;
export async function getDelugeService(): Promise<DelugeService> {
if (delugeServiceInstance && configLoaded) return delugeServiceInstance;
try {
const { getConfigService } = await import('../services/config.service');
const { getDownloadClientManager } = await import('../services/download-client-manager.service');
const configService = await getConfigService();
const manager = getDownloadClientManager(configService);
const clientConfig = await manager.getClientForProtocol('torrent');
if (!clientConfig) throw new Error('Deluge is not configured. Please configure a Deluge client in admin settings.');
if (clientConfig.type !== 'deluge') throw new Error(`Expected Deluge client but found ${clientConfig.type}`);
if (!clientConfig.url) throw new Error('Deluge is not fully configured. Check your configuration in admin settings.');
const baseDir = await configService.get('download_dir') || '/downloads';
const downloadDir = clientConfig.customPath ? require('path').join(baseDir, clientConfig.customPath) : baseDir;
delugeServiceInstance = new DelugeService(
clientConfig.url, clientConfig.username || '', clientConfig.password || '',
downloadDir, clientConfig.category || 'readmeabook', clientConfig.disableSSLVerify,
{ enabled: clientConfig.remotePathMappingEnabled || false, remotePath: clientConfig.remotePath || '', localPath: clientConfig.localPath || '' }
);
const result = await delugeServiceInstance.testConnection();
if (!result.success) throw new Error(result.message || 'Deluge connection test failed.');
logger.info('[Deluge] Connection test successful');
configLoaded = true;
return delugeServiceInstance;
} catch (error) {
logger.error('[Deluge] Failed to initialize service', { error: error instanceof Error ? error.message : String(error) });
delugeServiceInstance = null;
configLoaded = false;
throw error;
}
}
export function invalidateDelugeService(): void {
delugeServiceInstance = null;
configLoaded = false;
logger.info('[Deluge] Service singleton invalidated');
}
+15 -3
View File
@@ -586,7 +586,19 @@ export class QBittorrentService implements IDownloadClient {
throw new Error(`Torrent ${hash} not found`);
}
return torrents[0];
// Find the torrent with the exact matching hash.
// Some qBittorrent-compatible clients (e.g. RDTClient) ignore the hashes
// filter and return all torrents, so we must verify the hash ourselves.
const normalizedHash = hash.toLowerCase();
const match = torrents.find(
(t: TorrentInfo) => t.hash?.toLowerCase() === normalizedHash
);
if (!match) {
throw new Error(`Torrent ${hash} not found`);
}
return match;
} catch (error) {
// Don't log error here - caller handles it (e.g., duplicate checking)
throw error;
@@ -1103,7 +1115,7 @@ export class QBittorrentService implements IDownloadClient {
stalledDL: 'downloading',
stalledUP: 'seeding',
pausedDL: 'paused',
// pausedUP = download finished, paused on upload side (e.g. RDT-Client, ratio met)
// pausedUP = download finished, paused on upload side (e.g. ratio met)
pausedUP: 'seeding',
queuedDL: 'queued',
queuedUP: 'seeding',
@@ -1158,7 +1170,7 @@ export class QBittorrentService implements IDownloadClient {
stalledDL: 'downloading',
stalledUP: 'completed',
pausedDL: 'paused',
// pausedUP = download finished, paused on upload side (e.g. RDT-Client, ratio met)
// pausedUP = download finished, paused on upload side (e.g. ratio met)
pausedUP: 'completed',
queuedDL: 'queued',
queuedUP: 'completed',
-105
View File
@@ -1,105 +0,0 @@
/**
* Component: RDT-Client Integration Service
* Documentation: documentation/phase3/download-clients.md
*
* RDT-Client is a Real-Debrid torrent proxy that emulates the qBittorrent API.
* Extends QBittorrentService and overrides behavioral differences:
* - Duplicate detection: deletes stale torrent before adding fresh (no false matches)
* - postProcess: removes torrent entry from client after files are organized
* - ensureCategory: no-op (RDT-Client doesn't support categories)
*/
import { RMABLogger } from '../utils/logger';
import { DownloadClientType } from '../interfaces/download-client.interface';
import { QBittorrentService, AddTorrentOptions } from './qbittorrent.service';
const logger = RMABLogger.create('RDTClient');
export class RDTClientService extends QBittorrentService {
override readonly clientType: DownloadClientType = 'rdtclient';
/**
* Override: Delete any existing torrent with the same hash before adding.
* RDT-Client can have stale entries from previous requests that cause
* false duplicate detection — always start fresh.
*/
protected override async addMagnetLink(
magnetUrl: string,
category: string,
options?: AddTorrentOptions
): Promise<string> {
const infoHash = this.extractHashFromMagnet(magnetUrl);
if (infoHash) {
await this.deleteStaleIfExists(infoHash);
}
return super.addMagnetLink(magnetUrl, category, options);
}
/**
* Override: Delete any existing torrent with the same hash before adding.
* Same rationale as addMagnetLink — prevent false duplicate short-circuits.
*/
protected override async addTorrentFile(
torrentUrl: string,
category: string,
options?: AddTorrentOptions
): Promise<string> {
// We can't pre-extract the hash from a .torrent URL without downloading it,
// so we let the parent handle the full flow. The parent's duplicate check
// calls getTorrent which will find any stale entry — but the parent
// short-circuits on duplicates. To handle this, we override addTorrentFile
// to intercept after the parent downloads and parses the torrent.
//
// The parent's addTorrentFile downloads the .torrent, parses it, checks for
// duplicates, then uploads. Since we can't hook into the middle of that flow
// without duplicating the download logic, we accept that .torrent file adds
// may encounter a stale duplicate. The primary use case (magnet links from
// indexers) is handled by the addMagnetLink override above.
//
// For .torrent files, the parent will return the existing hash if a duplicate
// is found. The postProcess cleanup after organize will still clean it up.
return super.addTorrentFile(torrentUrl, category, options);
}
/**
* Override: Remove torrent entry from RDT-Client after files are organized.
* Unlike qBittorrent (which seeds), RDT-Client torrents should be cleaned up
* immediately — Real-Debrid handles seeding on their infrastructure.
*/
override async postProcess(id: string): Promise<void> {
try {
logger.info(`Removing torrent ${id} from RDT-Client (post-organize cleanup)`);
await this.deleteTorrent(id, false);
logger.info(`Successfully removed torrent ${id} from RDT-Client`);
} catch (error) {
// Non-fatal: torrent may already have been removed
logger.warn(
`Failed to remove torrent ${id} from RDT-Client: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Override: No-op. RDT-Client doesn't support qBittorrent categories.
* Avoids 404 errors that appear in logs when the parent tries to create/update categories.
*/
protected override async ensureCategory(_category: string): Promise<void> {
// No-op: RDT-Client does not support categories
}
/**
* Delete a stale torrent if it exists, so a fresh add doesn't short-circuit.
*/
private async deleteStaleIfExists(hash: string): Promise<void> {
try {
await this.getTorrent(hash);
// If we get here, torrent exists — delete it
logger.info(`Deleting stale torrent ${hash} from RDT-Client before fresh add`);
await this.deleteTorrent(hash, false);
} catch {
// Torrent doesn't exist — nothing to clean up
}
}
}
@@ -11,7 +11,7 @@
// =========================================================================
/** Supported download client types — single source of truth */
export const SUPPORTED_CLIENT_TYPES = ['qbittorrent', 'sabnzbd', 'nzbget', 'transmission', 'rdtclient'] as const;
export const SUPPORTED_CLIENT_TYPES = ['qbittorrent', 'sabnzbd', 'nzbget', 'transmission', 'deluge'] as const;
/** Identifies the specific download client software */
export type DownloadClientType = (typeof SUPPORTED_CLIENT_TYPES)[number];
@@ -22,7 +22,7 @@ export const CLIENT_DISPLAY_NAMES: Record<DownloadClientType, string> = {
sabnzbd: 'SABnzbd',
nzbget: 'NZBGet',
transmission: 'Transmission',
rdtclient: 'RDT-Client',
deluge: 'Deluge',
};
/** Get display name for a client type, falling back to the raw type */
@@ -39,7 +39,7 @@ export const CLIENT_PROTOCOL_MAP: Record<DownloadClientType, ProtocolType> = {
sabnzbd: 'usenet',
nzbget: 'usenet',
transmission: 'torrent',
rdtclient: 'torrent',
deluge: 'torrent',
};
/** Unified download status across all clients */
@@ -865,12 +865,9 @@ async function cleanupDownloadAfterOrganize(
});
// Check if this is a non-torrent indexer with cleanup enabled.
// RDT-Client is an exception: even though it's torrent protocol, it needs cleanup
// because Real-Debrid handles seeding — local torrent entries should be removed.
const isRDTClient = downloadHistory.downloadClient === 'rdtclient';
const isTorrentProtocol = indexer?.protocol?.toLowerCase() === 'torrent';
if (!indexer || (!isRDTClient && isTorrentProtocol) || !indexer.removeAfterProcessing) {
if (!indexer || isTorrentProtocol || !indexer.removeAfterProcessing) {
return;
}
@@ -2,7 +2,7 @@
* Component: Download Client Manager Service
* Documentation: documentation/phase3/download-clients.md
*
* Manages multiple download clients (qBittorrent, Transmission, SABnzbd, NZBGet) with protocol-based routing.
* Manages multiple download clients (qBittorrent, Transmission, Deluge, SABnzbd, NZBGet) with protocol-based routing.
* Supports migration from legacy single-client config to multi-client JSON array format.
*/
@@ -16,7 +16,7 @@ import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
import { NZBGetService } from '@/lib/integrations/nzbget.service';
import { TransmissionService } from '@/lib/integrations/transmission.service';
import { RDTClientService } from '@/lib/integrations/rdtclient.service';
import { DelugeService } from '@/lib/integrations/deluge.service';
import { PathMappingConfig } from '@/lib/utils/path-mapper';
import { IDownloadClient, DownloadClientType, ProtocolType, CLIENT_PROTOCOL_MAP, getClientDisplayName } from '@/lib/interfaces/download-client.interface';
@@ -194,8 +194,8 @@ export class DownloadClientManager {
return this.createNZBGetService(config, downloadDir);
case 'transmission':
return this.createTransmissionService(config, downloadDir);
case 'rdtclient':
return this.createRDTClientService(config, downloadDir);
case 'deluge':
return this.createDelugeService(config, downloadDir);
default:
throw new Error(`Unsupported download client type: ${config.type}`);
}
@@ -339,9 +339,9 @@ export class DownloadClientManager {
}
/**
* Create RDT-Client service instance (same constructor as qBittorrent — identical API)
* Create Deluge service instance
*/
private createRDTClientService(config: DownloadClientConfig, downloadDir: string): RDTClientService {
private createDelugeService(config: DownloadClientConfig, downloadDir: string): DelugeService {
const pathMapping: PathMappingConfig | undefined = config.remotePathMappingEnabled && config.remotePath && config.localPath
? {
enabled: true,
@@ -350,7 +350,7 @@ export class DownloadClientManager {
}
: undefined;
return new RDTClientService(
return new DelugeService(
config.url,
config.username || '',
config.password || '',