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
127 lines
3.4 KiB
JavaScript
127 lines
3.4 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
const { logToFile } = require('./logger');
|
|
|
|
// Validate that a configured service URL is well-formed and uses http(s).
|
|
// Emits a warning (never throws) so a misconfigured instance degrades
|
|
// gracefully rather than crashing the whole server.
|
|
function validateInstanceUrl(url, instanceId) {
|
|
if (!url || typeof url !== 'string') {
|
|
logToFile(`[Config] WARNING: instance "${instanceId}" has no URL configured`);
|
|
return false;
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(url);
|
|
} catch {
|
|
logToFile(`[Config] WARNING: instance "${instanceId}" has an invalid URL: "${url}"`);
|
|
return false;
|
|
}
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
logToFile(`[Config] WARNING: instance "${instanceId}" URL must use http or https, got "${parsed.protocol}"`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function parseInstances(envVar, legacyUrl, legacyKey, legacyUsername, legacyPassword) {
|
|
// Try to parse JSON array format first
|
|
if (envVar) {
|
|
try {
|
|
// Handle multi-line JSON by removing newlines and extra spaces
|
|
const cleaned = envVar.replace(/\s+/g, ' ').trim();
|
|
const instances = JSON.parse(cleaned);
|
|
if (Array.isArray(instances) && instances.length > 0) {
|
|
logToFile(`[Config] Parsed ${instances.length} instances from JSON array`);
|
|
return instances.map((inst, idx) => {
|
|
const id = inst.name || `instance-${idx + 1}`;
|
|
validateInstanceUrl(inst.url, id);
|
|
return { ...inst, id };
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logToFile(`[Config] Failed to parse JSON array: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Fall back to legacy single-instance format
|
|
if (legacyUrl && legacyKey) {
|
|
logToFile(`[Config] Using legacy single-instance format`);
|
|
validateInstanceUrl(legacyUrl, 'default');
|
|
return [{
|
|
id: 'default',
|
|
name: 'Default',
|
|
url: legacyUrl,
|
|
apiKey: legacyKey,
|
|
username: legacyUsername,
|
|
password: legacyPassword
|
|
}];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function getSABnzbdInstances() {
|
|
return parseInstances(
|
|
process.env.SABNZBD_INSTANCES,
|
|
process.env.SABNZBD_URL,
|
|
process.env.SABNZBD_API_KEY
|
|
);
|
|
}
|
|
|
|
function getSonarrInstances() {
|
|
return parseInstances(
|
|
process.env.SONARR_INSTANCES,
|
|
process.env.SONARR_URL,
|
|
process.env.SONARR_API_KEY
|
|
);
|
|
}
|
|
|
|
function getRadarrInstances() {
|
|
return parseInstances(
|
|
process.env.RADARR_INSTANCES,
|
|
process.env.RADARR_URL,
|
|
process.env.RADARR_API_KEY
|
|
);
|
|
}
|
|
|
|
function getQbittorrentInstances() {
|
|
return parseInstances(
|
|
process.env.QBITTORRENT_INSTANCES,
|
|
process.env.QBITTORRENT_URL,
|
|
null, // no apiKey for qBittorrent
|
|
process.env.QBITTORRENT_USERNAME,
|
|
process.env.QBITTORRENT_PASSWORD
|
|
);
|
|
}
|
|
|
|
function getTransmissionInstances() {
|
|
return parseInstances(
|
|
process.env.TRANSMISSION_INSTANCES,
|
|
process.env.TRANSMISSION_URL,
|
|
null, // no apiKey for Transmission
|
|
process.env.TRANSMISSION_USERNAME,
|
|
process.env.TRANSMISSION_PASSWORD
|
|
);
|
|
}
|
|
|
|
function getRtorrentInstances() {
|
|
return parseInstances(
|
|
process.env.RTORRENT_INSTANCES,
|
|
process.env.RTORRENT_URL,
|
|
null, // no apiKey for rtorrent
|
|
process.env.RTORRENT_USERNAME,
|
|
process.env.RTORRENT_PASSWORD
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
getSABnzbdInstances,
|
|
getSonarrInstances,
|
|
getRadarrInstances,
|
|
getQbittorrentInstances,
|
|
getTransmissionInstances,
|
|
getRtorrentInstances,
|
|
parseInstances,
|
|
validateInstanceUrl
|
|
};
|