// Copyright (c) 2026 Gordon Bolton. MIT License. const { logToFile } = require('./logger'); const { getSABnzbdInstances, getQbittorrentInstances, getTransmissionInstances, getRtorrentInstances } = require('./config'); // Import client classes const SABnzbdClient = require('../clients/SABnzbdClient'); const QBittorrentClient = require('../clients/QBittorrentClient'); const TransmissionClient = require('../clients/TransmissionClient'); const RTorrentClient = require('../clients/RTorrentClient'); // Client type mapping const clientClasses = { sabnzbd: SABnzbdClient, qbittorrent: QBittorrentClient, transmission: TransmissionClient, rtorrent: RTorrentClient }; /** * Registry and factory for download clients */ class DownloadClientRegistry { constructor() { this.clients = new Map(); this.initialized = false; } /** * Initialize all configured download clients */ async initialize() { if (this.initialized) { return; } logToFile('[DownloadClientRegistry] Initializing download clients...'); // Get all instance configurations const sabnzbdInstances = getSABnzbdInstances(); const qbittorrentInstances = getQbittorrentInstances(); const transmissionInstances = getTransmissionInstances(); const rtorrentInstances = getRtorrentInstances(); // Create client instances const instanceConfigs = [ ...sabnzbdInstances.map(inst => ({ ...inst, type: 'sabnzbd' })), ...qbittorrentInstances.map(inst => ({ ...inst, type: 'qbittorrent' })), ...transmissionInstances.map(inst => ({ ...inst, type: 'transmission' })), ...rtorrentInstances.map(inst => ({ ...inst, type: 'rtorrent' })) ]; for (const config of instanceConfigs) { try { const ClientClass = clientClasses[config.type]; if (!ClientClass) { logToFile(`[DownloadClientRegistry] Unknown client type: ${config.type}`); continue; } const client = new ClientClass(config); this.clients.set(config.id, client); logToFile(`[DownloadClientRegistry] Created ${config.type} client: ${config.name} (${config.id})`); } catch (error) { logToFile(`[DownloadClientRegistry] Failed to create client ${config.id}: ${error.message}`); } } this.initialized = true; logToFile(`[DownloadClientRegistry] Initialized ${this.clients.size} download clients`); } /** * Get all registered clients * @returns {Array} Array of client instances */ getAllClients() { return Array.from(this.clients.values()); } /** * Get client by instance ID * @param {string} instanceId - The instance ID * @returns {DownloadClient|null} Client instance or null if not found */ getClient(instanceId) { return this.clients.get(instanceId) || null; } /** * Get clients by type * @param {string} type - Client type ('sabnzbd', 'qbittorrent', 'transmission') * @returns {Array} Array of client instances */ getClientsByType(type) { return this.getAllClients().filter(client => client.getClientType() === type); } /** * Get active downloads from all clients * @returns {Promise>} Array of all downloads */ async getAllDownloads() { const clients = this.getAllClients(); if (clients.length === 0) { return []; } // Reset fallback flags for qBittorrent clients for (const client of clients) { if (client.resetFallbackFlag) { client.resetFallbackFlag(); } } // Fetch downloads from all clients in parallel const results = await Promise.allSettled( clients.map(async (client) => { try { const downloads = await client.getActiveDownloads(); logToFile(`[DownloadClientRegistry] ${client.name}: ${downloads.length} downloads`); return downloads; } catch (error) { logToFile(`[DownloadClientRegistry] Error fetching from ${client.name}: ${error.message}`); return []; } }) ); // Flatten and return all downloads const allDownloads = results .filter(result => result.status === 'fulfilled') .flatMap(result => result.value); logToFile(`[DownloadClientRegistry] Total downloads from all clients: ${allDownloads.length}`); return allDownloads; } /** * Get downloads grouped by client type (for backward compatibility) * @returns {Promise} Downloads grouped by client type */ async getDownloadsByClientType() { const clients = this.getAllClients(); const result = {}; // Group by client type for (const client of clients) { const type = client.getClientType(); if (!result[type]) { result[type] = []; } try { const downloads = await client.getActiveDownloads(); result[type].push(...downloads); } catch (error) { logToFile(`[DownloadClientRegistry] Error fetching from ${client.name}: ${error.message}`); } } return result; } /** * Test connection to all clients * @returns {Promise>} Array of connection test results */ async testAllConnections() { const clients = this.getAllClients(); const results = await Promise.allSettled( clients.map(async (client) => { try { const success = await client.testConnection(); return { instanceId: client.getInstanceId(), instanceName: client.name, clientType: client.getClientType(), success, error: null }; } catch (error) { return { instanceId: client.getInstanceId(), instanceName: client.name, clientType: client.getClientType(), success: false, error: error.message }; } }) ); return results .filter(result => result.status === 'fulfilled') .map(result => result.value); } /** * Get client status information from all clients * @returns {Promise>} Array of client status objects */ async getAllClientStatuses() { const clients = this.getAllClients(); const results = await Promise.allSettled( clients.map(async (client) => { try { const status = await client.getClientStatus(); return { instanceId: client.getInstanceId(), instanceName: client.name, clientType: client.getClientType(), status }; } catch (error) { logToFile(`[DownloadClientRegistry] Error getting status from ${client.name}: ${error.message}`); return { instanceId: client.getInstanceId(), instanceName: client.name, clientType: client.getClientType(), status: null, error: error.message }; } }) ); return results .filter(result => result.status === 'fulfilled') .map(result => result.value); } } // Create singleton instance const registry = new DownloadClientRegistry(); module.exports = { DownloadClientRegistry, registry, // Convenience functions initializeClients: () => registry.initialize(), getAllClients: () => registry.getAllClients(), getClient: (instanceId) => registry.getClient(instanceId), getClientsByType: (type) => registry.getClientsByType(type), getAllDownloads: () => registry.getAllDownloads(), getDownloadsByClientType: () => registry.getDownloadsByClientType(), testAllConnections: () => registry.testAllConnections(), getAllClientStatuses: () => registry.getAllClientStatuses() };