6fa9c79a7d
Build and Push Docker Image / build (push) Successful in 59s
Docs Check / Markdown lint (push) Failing after 1m45s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m7s
CI / Security audit (push) Successful in 2m33s
Docs Check / Mermaid diagram parse check (push) Successful in 2m55s
CI / Swagger Validation & Coverage (push) Successful in 3m19s
CI / Tests & coverage (push) Successful in 3m29s
- RTorrentClient: guard d.multicall2 returning non-array, per-row try/catch, explicit Number()/String() coercions, _extractArrInfo null-safe - RTorrentClient.getClientStatus: coerce rates through Number.isFinite - SABnzbdClient: history limit now reads SAB_HISTORY_LIMIT env var (default 10) - DownloadClient: added _recordLastError, _clearLastError, getLastError on base - All four clients call _recordLastError on failure, _clearLastError on success - DownloadClientRegistry.getAllClientStatuses: includes lastError in result - GET /api/status/status: exposes downloadClients[] array with per-client lastError - Tests: RTorrentClient null-safety + lastError, SABnzbd history limit + lastError, downloadClients.test expectation updated for new lastError field
139 lines
5.0 KiB
JavaScript
139 lines
5.0 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
/**
|
|
* Abstract base class for all download clients.
|
|
* Defines the common interface that all download clients must implement.
|
|
*/
|
|
class DownloadClient {
|
|
/**
|
|
* @param {Object} instanceConfig - Configuration for this client instance
|
|
* @param {string} instanceConfig.id - Unique identifier for this instance
|
|
* @param {string} instanceConfig.name - Display name for this instance
|
|
* @param {string} instanceConfig.url - Base URL for the client API
|
|
* @param {string} [instanceConfig.apiKey] - API key for authentication (if applicable)
|
|
* @param {string} [instanceConfig.username] - Username for authentication (if applicable)
|
|
* @param {string} [instanceConfig.password] - Password for authentication (if applicable)
|
|
*/
|
|
constructor(instanceConfig) {
|
|
if (this.constructor === DownloadClient) {
|
|
throw new Error('DownloadClient is an abstract class and cannot be instantiated directly');
|
|
}
|
|
|
|
this.id = instanceConfig.id;
|
|
this.name = instanceConfig.name;
|
|
this.url = instanceConfig.url;
|
|
this.apiKey = instanceConfig.apiKey;
|
|
this.username = instanceConfig.username;
|
|
this.password = instanceConfig.password;
|
|
|
|
// Last error encountered while talking to this client.
|
|
// Cleared on successful calls via _clearLastError(); set via _recordLastError().
|
|
// Surfaced through getAllClientStatuses() so the admin status panel can show
|
|
// a per-client failure indicator without needing to scrape logs.
|
|
this.lastError = null;
|
|
}
|
|
|
|
/**
|
|
* Record an error encountered while talking to this client.
|
|
* @param {string} operation - Short description of the operation (e.g. 'getActiveDownloads')
|
|
* @param {Error|string} error - Error object or message
|
|
*/
|
|
_recordLastError(operation, error) {
|
|
const message = (error && error.message) ? error.message : String(error || 'unknown error');
|
|
this.lastError = {
|
|
operation,
|
|
message,
|
|
at: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear the last error (called when an operation succeeds).
|
|
*/
|
|
_clearLastError() {
|
|
this.lastError = null;
|
|
}
|
|
|
|
/**
|
|
* Public accessor for the last recorded error, or null if none.
|
|
* @returns {{operation:string, message:string, at:string}|null}
|
|
*/
|
|
getLastError() {
|
|
return this.lastError;
|
|
}
|
|
|
|
/**
|
|
* Get the client type identifier (e.g., 'qbittorrent', 'sabnzbd', 'transmission')
|
|
* @returns {string} The client type
|
|
*/
|
|
getClientType() {
|
|
throw new Error('getClientType() must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Get the unique instance ID
|
|
* @returns {string} The instance ID
|
|
*/
|
|
getInstanceId() {
|
|
return this.id;
|
|
}
|
|
|
|
/**
|
|
* Test connection to the download client
|
|
* @returns {Promise<boolean>} True if connection is successful
|
|
*/
|
|
async testConnection() {
|
|
throw new Error('testConnection() must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Get active downloads from this client
|
|
* @returns {Promise<Array<NormalizedDownload>>} Array of normalized download objects
|
|
*/
|
|
async getActiveDownloads() {
|
|
throw new Error('getActiveDownloads() must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Optional: Get client status information
|
|
* @returns {Promise<Object|null>} Client status object or null if not supported
|
|
*/
|
|
async getClientStatus() {
|
|
return null; // Default implementation - optional method
|
|
}
|
|
|
|
/**
|
|
* Normalize a download object to the standard schema
|
|
* @param {Object} download - Raw download object from client
|
|
* @returns {NormalizedDownload} Normalized download object
|
|
*/
|
|
normalizeDownload(download) {
|
|
throw new Error('normalizeDownload() must be implemented by subclass');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} NormalizedDownload
|
|
* @property {string} id - Client-specific unique ID
|
|
* @property {string} title - Download title/name
|
|
* @property {'usenet'|'torrent'} type - Download type
|
|
* @property {string} client - Client identifier ('sabnzbd', 'qbittorrent', 'transmission', etc.)
|
|
* @property {string} instanceId - Instance identifier
|
|
* @property {string} instanceName - Instance display name
|
|
* @property {string} status - Normalized status (Downloading, Seeding, Paused, etc.)
|
|
* @property {number} progress - Progress percentage (0-100)
|
|
* @property {number} size - Total size in bytes
|
|
* @property {number} downloaded - Downloaded bytes
|
|
* @property {number} speed - Current speed in bytes/sec
|
|
* @property {number|null} eta - Estimated time remaining in seconds, null if unknown
|
|
* @property {string|undefined} category - Download category (optional)
|
|
* @property {string[]|undefined} tags - Download tags (optional)
|
|
* @property {string|undefined} savePath - Save path (optional)
|
|
* @property {string|undefined} addedOn - Added timestamp (optional)
|
|
* @property {number|undefined} arrQueueId - Sonarr/Radarr queue ID (optional)
|
|
* @property {'series'|'movie'|undefined} arrType - Sonarr/Radarr type (optional)
|
|
* @property {any|undefined} raw - Original client response (escape hatch)
|
|
*/
|
|
|
|
module.exports = DownloadClient;
|