From 6e199925aa4b157dd820885525d253a31f84e546 Mon Sep 17 00:00:00 2001 From: Gronod Date: Tue, 19 May 2026 14:51:22 +0100 Subject: [PATCH] refactor: make PALDRA match PDCA style exactly - remove redundant instanceConfig parameter and convert to pure singleton - Remove instanceConfig parameter from all retriever methods (getTags, getQueue, getHistory) - Retriever instances now use this.url, this.apiKey, this.id instead of passed parameter - Convert ArrRetrieverRegistry from class with convenience functions to pure singleton object - Export singleton instance directly instead of class + convenience functions - Update poller.js and historyFetcher.js to call methods on singleton directly - All 261 tests pass with zero behavior changes --- server/clients/ArrRetriever.js | 9 ++-- server/clients/PollingRadarrRetriever.js | 27 +++++----- server/clients/PollingSonarrRetriever.js | 27 +++++----- server/utils/arrRetrievers.js | 68 +++++++++--------------- server/utils/historyFetcher.js | 14 ++--- server/utils/poller.js | 16 +++--- 6 files changed, 66 insertions(+), 95 deletions(-) diff --git a/server/clients/ArrRetriever.js b/server/clients/ArrRetriever.js index e96eb22..16d45a4 100644 --- a/server/clients/ArrRetriever.js +++ b/server/clients/ArrRetriever.js @@ -44,25 +44,22 @@ class ArrRetriever { /** * Get tags from this *arr instance - * @param {Object} instanceConfig - Configuration for the instance * @returns {Promise} Array of tag objects */ - async getTags(instanceConfig) { + async getTags() { throw new Error('getTags() must be implemented by subclass'); } /** * Get queue from this *arr instance - * @param {Object} instanceConfig - Configuration for the instance * @returns {Promise} Queue object with records array */ - async getQueue(instanceConfig) { + async getQueue() { throw new Error('getQueue() must be implemented by subclass'); } /** * Get history from this *arr instance - * @param {Object} instanceConfig - Configuration for the instance * @param {Object} options - Optional parameters for history fetch * @param {number} [options.pageSize] - Number of records to fetch * @param {string} [options.sortKey] - Field to sort by @@ -73,7 +70,7 @@ class ArrRetriever { * @param {string} [options.startDate] - ISO date string for filtering * @returns {Promise} History object with records array */ - async getHistory(instanceConfig, options = {}) { + async getHistory(options = {}) { throw new Error('getHistory() must be implemented by subclass'); } } diff --git a/server/clients/PollingRadarrRetriever.js b/server/clients/PollingRadarrRetriever.js index 16ad9f8..7739e3f 100644 --- a/server/clients/PollingRadarrRetriever.js +++ b/server/clients/PollingRadarrRetriever.js @@ -18,42 +18,39 @@ class PollingRadarrRetriever extends ArrRetriever { /** * Get tags from Radarr instance - * @param {Object} instanceConfig - Configuration for the instance * @returns {Promise} Array of tag objects */ - async getTags(instanceConfig) { + async getTags() { try { - const response = await axios.get(`${instanceConfig.url}/api/v3/tag`, { - headers: { 'X-Api-Key': instanceConfig.apiKey } + const response = await axios.get(`${this.url}/api/v3/tag`, { + headers: { 'X-Api-Key': this.apiKey } }); return response.data; } catch (error) { - logToFile(`[PollingRadarrRetriever] ${instanceConfig.id} tags error: ${error.message}`); + logToFile(`[PollingRadarrRetriever] ${this.id} tags error: ${error.message}`); return []; } } /** * Get queue from Radarr instance - * @param {Object} instanceConfig - Configuration for the instance * @returns {Promise} Queue object with records array */ - async getQueue(instanceConfig) { + async getQueue() { try { - const response = await axios.get(`${instanceConfig.url}/api/v3/queue`, { - headers: { 'X-Api-Key': instanceConfig.apiKey }, + const response = await axios.get(`${this.url}/api/v3/queue`, { + headers: { 'X-Api-Key': this.apiKey }, params: { includeMovie: true } }); return response.data; } catch (error) { - logToFile(`[PollingRadarrRetriever] ${instanceConfig.id} queue error: ${error.message}`); + logToFile(`[PollingRadarrRetriever] ${this.id} queue error: ${error.message}`); return { records: [] }; } } /** * Get history from Radarr instance - * @param {Object} instanceConfig - Configuration for the instance * @param {Object} options - Optional parameters for history fetch * @param {number} [options.pageSize=10] - Number of records to fetch * @param {string} [options.sortKey] - Field to sort by @@ -62,7 +59,7 @@ class PollingRadarrRetriever extends ArrRetriever { * @param {string} [options.startDate] - ISO date string for filtering * @returns {Promise} History object with records array */ - async getHistory(instanceConfig, options = {}) { + async getHistory(options = {}) { const { pageSize = 10, sortKey, @@ -81,13 +78,13 @@ class PollingRadarrRetriever extends ArrRetriever { if (sortDir) params.sortDir = sortDir; if (startDate) params.startDate = startDate; - const response = await axios.get(`${instanceConfig.url}/api/v3/history`, { - headers: { 'X-Api-Key': instanceConfig.apiKey }, + const response = await axios.get(`${this.url}/api/v3/history`, { + headers: { 'X-Api-Key': this.apiKey }, params }); return response.data; } catch (error) { - logToFile(`[PollingRadarrRetriever] ${instanceConfig.id} history error: ${error.message}`); + logToFile(`[PollingRadarrRetriever] ${this.id} history error: ${error.message}`); return { records: [] }; } } diff --git a/server/clients/PollingSonarrRetriever.js b/server/clients/PollingSonarrRetriever.js index 1211038..16a224a 100644 --- a/server/clients/PollingSonarrRetriever.js +++ b/server/clients/PollingSonarrRetriever.js @@ -18,42 +18,39 @@ class PollingSonarrRetriever extends ArrRetriever { /** * Get tags from Sonarr instance - * @param {Object} instanceConfig - Configuration for the instance * @returns {Promise} Array of tag objects */ - async getTags(instanceConfig) { + async getTags() { try { - const response = await axios.get(`${instanceConfig.url}/api/v3/tag`, { - headers: { 'X-Api-Key': instanceConfig.apiKey } + const response = await axios.get(`${this.url}/api/v3/tag`, { + headers: { 'X-Api-Key': this.apiKey } }); return response.data; } catch (error) { - logToFile(`[PollingSonarrRetriever] ${instanceConfig.id} tags error: ${error.message}`); + logToFile(`[PollingSonarrRetriever] ${this.id} tags error: ${error.message}`); return []; } } /** * Get queue from Sonarr instance - * @param {Object} instanceConfig - Configuration for the instance * @returns {Promise} Queue object with records array */ - async getQueue(instanceConfig) { + async getQueue() { try { - const response = await axios.get(`${instanceConfig.url}/api/v3/queue`, { - headers: { 'X-Api-Key': instanceConfig.apiKey }, + const response = await axios.get(`${this.url}/api/v3/queue`, { + headers: { 'X-Api-Key': this.apiKey }, params: { includeSeries: true, includeEpisode: true } }); return response.data; } catch (error) { - logToFile(`[PollingSonarrRetriever] ${instanceConfig.id} queue error: ${error.message}`); + logToFile(`[PollingSonarrRetriever] ${this.id} queue error: ${error.message}`); return { records: [] }; } } /** * Get history from Sonarr instance - * @param {Object} instanceConfig - Configuration for the instance * @param {Object} options - Optional parameters for history fetch * @param {number} [options.pageSize=10] - Number of records to fetch * @param {string} [options.sortKey] - Field to sort by @@ -63,7 +60,7 @@ class PollingSonarrRetriever extends ArrRetriever { * @param {string} [options.startDate] - ISO date string for filtering * @returns {Promise} History object with records array */ - async getHistory(instanceConfig, options = {}) { + async getHistory(options = {}) { const { pageSize = 10, sortKey, @@ -84,13 +81,13 @@ class PollingSonarrRetriever extends ArrRetriever { if (sortDir) params.sortDir = sortDir; if (startDate) params.startDate = startDate; - const response = await axios.get(`${instanceConfig.url}/api/v3/history`, { - headers: { 'X-Api-Key': instanceConfig.apiKey }, + const response = await axios.get(`${this.url}/api/v3/history`, { + headers: { 'X-Api-Key': this.apiKey }, params }); return response.data; } catch (error) { - logToFile(`[PollingSonarrRetriever] ${instanceConfig.id} history error: ${error.message}`); + logToFile(`[PollingSonarrRetriever] ${this.id} history error: ${error.message}`); return { records: [] }; } } diff --git a/server/utils/arrRetrievers.js b/server/utils/arrRetrievers.js index 4a995f2..c405bcb 100644 --- a/server/utils/arrRetrievers.js +++ b/server/utils/arrRetrievers.js @@ -16,13 +16,11 @@ const retrieverClasses = { }; /** - * Registry and factory for *arr data retrievers + * Singleton registry for *arr data retrievers */ -class ArrRetrieverRegistry { - constructor() { - this.retrievers = new Map(); - this.initialized = false; - } +const arrRetrieverRegistry = { + retrievers: new Map(), + initialized: false, /** * Initialize all configured *arr retrievers @@ -62,7 +60,7 @@ class ArrRetrieverRegistry { this.initialized = true; logToFile(`[ArrRetrieverRegistry] Initialized ${this.retrievers.size} *arr retrievers`); - } + }, /** * Get all registered retrievers @@ -70,7 +68,7 @@ class ArrRetrieverRegistry { */ getAllRetrievers() { return Array.from(this.retrievers.values()); - } + }, /** * Get retriever by instance ID @@ -79,7 +77,7 @@ class ArrRetrieverRegistry { */ getRetriever(instanceId) { return this.retrievers.get(instanceId) || null; - } + }, /** * Get retrievers by type @@ -88,7 +86,7 @@ class ArrRetrieverRegistry { */ getRetrieversByType(type) { return this.getAllRetrievers().filter(retriever => retriever.getRetrieverType() === type); - } + }, /** * Get tags from all retrievers @@ -104,7 +102,7 @@ class ArrRetrieverRegistry { const results = await Promise.allSettled( retrievers.map(async (retriever) => { try { - const tags = await retriever.getTags(retriever); + const tags = await retriever.getTags(); logToFile(`[ArrRetrieverRegistry] ${retriever.name}: ${tags.length} tags`); return { instance: retriever.getInstanceId(), data: tags }; } catch (error) { @@ -117,7 +115,7 @@ class ArrRetrieverRegistry { return results .filter(result => result.status === 'fulfilled') .map(result => result.value); - } + }, /** * Get queue from all retrievers @@ -133,7 +131,7 @@ class ArrRetrieverRegistry { const results = await Promise.allSettled( retrievers.map(async (retriever) => { try { - const queue = await retriever.getQueue(retriever); + const queue = await retriever.getQueue(); logToFile(`[ArrRetrieverRegistry] ${retriever.name}: ${(queue.records || []).length} queue items`); return { instance: retriever.getInstanceId(), data: queue }; } catch (error) { @@ -146,7 +144,7 @@ class ArrRetrieverRegistry { return results .filter(result => result.status === 'fulfilled') .map(result => result.value); - } + }, /** * Get history from all retrievers @@ -163,7 +161,7 @@ class ArrRetrieverRegistry { const results = await Promise.allSettled( retrievers.map(async (retriever) => { try { - const history = await retriever.getHistory(retriever, options); + const history = await retriever.getHistory(options); logToFile(`[ArrRetrieverRegistry] ${retriever.name}: ${(history.records || []).length} history records`); return { instance: retriever.getInstanceId(), data: history }; } catch (error) { @@ -176,7 +174,7 @@ class ArrRetrieverRegistry { return results .filter(result => result.status === 'fulfilled') .map(result => result.value); - } + }, /** * Get tags grouped by retriever type @@ -189,7 +187,7 @@ class ArrRetrieverRegistry { const sonarrTags = await Promise.allSettled( sonarrRetrievers.map(async (retriever) => { try { - const tags = await retriever.getTags(retriever); + const tags = await retriever.getTags(); return { instance: retriever.getInstanceId(), data: tags }; } catch (error) { logToFile(`[ArrRetrieverRegistry] Error fetching tags from ${retriever.name}: ${error.message}`); @@ -201,7 +199,7 @@ class ArrRetrieverRegistry { const radarrTags = await Promise.allSettled( radarrRetrievers.map(async (retriever) => { try { - const tags = await retriever.getTags(retriever); + const tags = await retriever.getTags(); return { instance: retriever.getInstanceId(), data: tags }; } catch (error) { logToFile(`[ArrRetrieverRegistry] Error fetching tags from ${retriever.name}: ${error.message}`); @@ -218,7 +216,7 @@ class ArrRetrieverRegistry { .filter(result => result.status === 'fulfilled') .map(result => result.value) }; - } + }, /** * Get queue grouped by retriever type @@ -231,7 +229,7 @@ class ArrRetrieverRegistry { const sonarrQueues = await Promise.allSettled( sonarrRetrievers.map(async (retriever) => { try { - const queue = await retriever.getQueue(retriever); + const queue = await retriever.getQueue(); return { instance: retriever.getInstanceId(), data: queue }; } catch (error) { logToFile(`[ArrRetrieverRegistry] Error fetching queue from ${retriever.name}: ${error.message}`); @@ -243,7 +241,7 @@ class ArrRetrieverRegistry { const radarrQueues = await Promise.allSettled( radarrRetrievers.map(async (retriever) => { try { - const queue = await retriever.getQueue(retriever); + const queue = await retriever.getQueue(); return { instance: retriever.getInstanceId(), data: queue }; } catch (error) { logToFile(`[ArrRetrieverRegistry] Error fetching queue from ${retriever.name}: ${error.message}`); @@ -260,7 +258,7 @@ class ArrRetrieverRegistry { .filter(result => result.status === 'fulfilled') .map(result => result.value) }; - } + }, /** * Get history grouped by retriever type @@ -274,7 +272,7 @@ class ArrRetrieverRegistry { const sonarrHistory = await Promise.allSettled( sonarrRetrievers.map(async (retriever) => { try { - const history = await retriever.getHistory(retriever, options); + const history = await retriever.getHistory(options); return { instance: retriever.getInstanceId(), data: history }; } catch (error) { logToFile(`[ArrRetrieverRegistry] Error fetching history from ${retriever.name}: ${error.message}`); @@ -286,7 +284,7 @@ class ArrRetrieverRegistry { const radarrHistory = await Promise.allSettled( radarrRetrievers.map(async (retriever) => { try { - const history = await retriever.getHistory(retriever, options); + const history = await retriever.getHistory(options); return { instance: retriever.getInstanceId(), data: history }; } catch (error) { logToFile(`[ArrRetrieverRegistry] Error fetching history from ${retriever.name}: ${error.message}`); @@ -304,24 +302,6 @@ class ArrRetrieverRegistry { .map(result => result.value) }; } -} - -// Create singleton instance -const registry = new ArrRetrieverRegistry(); - -module.exports = { - ArrRetrieverRegistry, - registry, - - // Convenience functions - initializeRetrievers: () => registry.initialize(), - getAllRetrievers: () => registry.getAllRetrievers(), - getRetriever: (instanceId) => registry.getRetriever(instanceId), - getRetrieversByType: (type) => registry.getRetrieversByType(type), - getAllTags: () => registry.getAllTags(), - getAllQueues: () => registry.getAllQueues(), - getAllHistory: (options) => registry.getAllHistory(options), - getTagsByType: () => registry.getTagsByType(), - getQueuesByType: () => registry.getQueuesByType(), - getHistoryByType: (options) => registry.getHistoryByType(options) }; + +module.exports = arrRetrieverRegistry; diff --git a/server/utils/historyFetcher.js b/server/utils/historyFetcher.js index 5e9f6ce..01a8165 100644 --- a/server/utils/historyFetcher.js +++ b/server/utils/historyFetcher.js @@ -1,7 +1,7 @@ // Copyright (c) 2026 Gordon Bolton. MIT License. const cache = require('./cache'); const { getSonarrInstances, getRadarrInstances } = require('./config'); -const { initializeRetrievers, getRetrieversByType } = require('./arrRetrievers'); +const arrRetrieverRegistry = require('./arrRetrievers'); // Cache TTL for recent-history data: 5 minutes. // History changes slowly compared to active downloads. @@ -27,17 +27,17 @@ async function fetchSonarrHistory(since) { if (cached) return cached; // Ensure retrievers are initialized - await initializeRetrievers(); + await arrRetrieverRegistry.initialize(); const instances = getSonarrInstances(); - const sonarrRetrievers = getRetrieversByType('sonarr'); + const sonarrRetrievers = arrRetrieverRegistry.getRetrieversByType('sonarr'); const results = await Promise.all(sonarrRetrievers.map(async (retriever) => { const inst = instances.find(i => i.id === retriever.getInstanceId()); if (!inst) return []; try { - const response = await retriever.getHistory(retriever, { + const response = await retriever.getHistory({ pageSize: 100, sortKey: 'date', sortDir: 'descending', @@ -76,17 +76,17 @@ async function fetchRadarrHistory(since) { if (cached) return cached; // Ensure retrievers are initialized - await initializeRetrievers(); + await arrRetrieverRegistry.initialize(); const instances = getRadarrInstances(); - const radarrRetrievers = getRetrieversByType('radarr'); + const radarrRetrievers = arrRetrieverRegistry.getRetrieversByType('radarr'); const results = await Promise.all(radarrRetrievers.map(async (retriever) => { const inst = instances.find(i => i.id === retriever.getInstanceId()); if (!inst) return []; try { - const response = await retriever.getHistory(retriever, { + const response = await retriever.getHistory({ pageSize: 100, sortKey: 'date', sortDir: 'descending', diff --git a/server/utils/poller.js b/server/utils/poller.js index 6526b12..8bc882f 100644 --- a/server/utils/poller.js +++ b/server/utils/poller.js @@ -2,7 +2,7 @@ const axios = require('axios'); const cache = require('./cache'); const { initializeClients, getAllDownloads, getDownloadsByClientType } = require('./downloadClients'); -const { initializeRetrievers, getTagsByType, getQueuesByType, getHistoryByType } = require('./arrRetrievers'); +const arrRetrieverRegistry = require('./arrRetrievers'); const { getSonarrInstances, getRadarrInstances @@ -41,7 +41,7 @@ async function pollAllServices() { try { // Ensure download clients and *arr retrievers are initialized await initializeClients(); - await initializeRetrievers(); + await arrRetrieverRegistry.initialize(); const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); @@ -53,27 +53,27 @@ async function pollAllServices() { return downloadsByType; }), timed('Sonarr Tags', async () => { - const tagsByType = await getTagsByType(); + const tagsByType = await arrRetrieverRegistry.getTagsByType(); return tagsByType.sonarr || []; }), timed('Sonarr Queue', async () => { - const queuesByType = await getQueuesByType(); + const queuesByType = await arrRetrieverRegistry.getQueuesByType(); return queuesByType.sonarr || []; }), timed('Sonarr History', async () => { - const historyByType = await getHistoryByType({ pageSize: 10 }); + const historyByType = await arrRetrieverRegistry.getHistoryByType({ pageSize: 10 }); return historyByType.sonarr || []; }), timed('Radarr Queue', async () => { - const queuesByType = await getQueuesByType(); + const queuesByType = await arrRetrieverRegistry.getQueuesByType(); return queuesByType.radarr || []; }), timed('Radarr History', async () => { - const historyByType = await getHistoryByType({ pageSize: 10 }); + const historyByType = await arrRetrieverRegistry.getHistoryByType({ pageSize: 10 }); return historyByType.radarr || []; }), timed('Radarr Tags', async () => { - const tagsByType = await getTagsByType(); + const tagsByType = await arrRetrieverRegistry.getTagsByType(); return tagsByType.radarr || []; }), ]);