Some checks failed
Build and Push Docker Image / build (push) Failing after 40s
CI / Security audit (push) Failing after 27s
CI / Tests & coverage (push) Failing after 35s
Docs Check / Markdown lint (push) Successful in 32s
Docs Check / Mermaid diagram parse check (push) Successful in 1m12s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 24s
- Implement RTorrentClient extending DownloadClient abstract class - Use xmlrpc package (v1.3.2) for XML-RPC communication - Support HTTP Basic Auth when credentials are configured - Map rTorrent states (d.state, d.is_active, d.is_hash_checking) to normalized statuses - Calculate ETA from download speed and remaining bytes - Add getRtorrentInstances() to config.js - Register RTorrentClient in downloadClients.js registry - Add 8 comprehensive unit tests covering all functionality - Update .env.sample with rtorrent configuration examples - Update ARCHITECTURE.md with rtorrent client details - Update ADDING-A-DOWNLOAD-CLIENT.md with rtorrent-specific notes
255 lines
7.6 KiB
JavaScript
255 lines
7.6 KiB
JavaScript
// 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<DownloadClient>} 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<DownloadClient>} Array of client instances
|
|
*/
|
|
getClientsByType(type) {
|
|
return this.getAllClients().filter(client => client.getClientType() === type);
|
|
}
|
|
|
|
/**
|
|
* Get active downloads from all clients
|
|
* @returns {Promise<Array<NormalizedDownload>>} 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<Object>} 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<Object>>} 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<Object>>} 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()
|
|
};
|