diff --git a/server/clients/ArrRetriever.js b/server/clients/ArrRetriever.js new file mode 100644 index 0000000..16d45a4 --- /dev/null +++ b/server/clients/ArrRetriever.js @@ -0,0 +1,78 @@ +// Copyright (c) 2026 Gordon Bolton. MIT License. + +/** + * Abstract base class for all *arr data retrievers. + * Defines the common interface that all retrievers must implement. + * This pluggable layer enables future retrieval strategies (e.g., webhook listeners) + * to push normalized data directly into the existing cache and SSE system + * without touching the poller logic. + */ +class ArrRetriever { + /** + * @param {Object} instanceConfig - Configuration for this retriever 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 *arr API + * @param {string} instanceConfig.apiKey - API key for authentication + */ + constructor(instanceConfig) { + if (this.constructor === ArrRetriever) { + throw new Error('ArrRetriever 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; + } + + /** + * Get the retriever type identifier (e.g., 'sonarr', 'radarr') + * @returns {string} The retriever type + */ + getRetrieverType() { + throw new Error('getRetrieverType() must be implemented by subclass'); + } + + /** + * Get the unique instance ID + * @returns {string} The instance ID + */ + getInstanceId() { + return this.id; + } + + /** + * Get tags from this *arr instance + * @returns {Promise} Array of tag objects + */ + async getTags() { + throw new Error('getTags() must be implemented by subclass'); + } + + /** + * Get queue from this *arr instance + * @returns {Promise} Queue object with records array + */ + async getQueue() { + throw new Error('getQueue() must be implemented by subclass'); + } + + /** + * Get history from this *arr 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 + * @param {string} [options.sortDir] - Sort direction ('ascending' or 'descending') + * @param {boolean} [options.includeSeries] - Include series data (Sonarr) + * @param {boolean} [options.includeEpisode] - Include episode data (Sonarr) + * @param {boolean} [options.includeMovie] - Include movie data (Radarr) + * @param {string} [options.startDate] - ISO date string for filtering + * @returns {Promise} History object with records array + */ + async getHistory(options = {}) { + throw new Error('getHistory() must be implemented by subclass'); + } +} + +module.exports = ArrRetriever; diff --git a/server/clients/PollingRadarrRetriever.js b/server/clients/PollingRadarrRetriever.js new file mode 100644 index 0000000..7739e3f --- /dev/null +++ b/server/clients/PollingRadarrRetriever.js @@ -0,0 +1,93 @@ +// Copyright (c) 2026 Gordon Bolton. MIT License. +const axios = require('axios'); +const ArrRetriever = require('./ArrRetriever'); +const { logToFile } = require('../utils/logger'); + +/** + * Polling-based Radarr data retriever. + * Implements the ArrRetriever interface using direct HTTP polling. + */ +class PollingRadarrRetriever extends ArrRetriever { + constructor(instanceConfig) { + super(instanceConfig); + } + + getRetrieverType() { + return 'radarr'; + } + + /** + * Get tags from Radarr instance + * @returns {Promise} Array of tag objects + */ + async getTags() { + try { + const response = await axios.get(`${this.url}/api/v3/tag`, { + headers: { 'X-Api-Key': this.apiKey } + }); + return response.data; + } catch (error) { + logToFile(`[PollingRadarrRetriever] ${this.id} tags error: ${error.message}`); + return []; + } + } + + /** + * Get queue from Radarr instance + * @returns {Promise} Queue object with records array + */ + async getQueue() { + try { + 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] ${this.id} queue error: ${error.message}`); + return { records: [] }; + } + } + + /** + * Get history from Radarr 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 + * @param {string} [options.sortDir] - Sort direction ('ascending' or 'descending') + * @param {boolean} [options.includeMovie=true] - Include movie data + * @param {string} [options.startDate] - ISO date string for filtering + * @returns {Promise} History object with records array + */ + async getHistory(options = {}) { + const { + pageSize = 10, + sortKey, + sortDir, + includeMovie = true, + startDate + } = options; + + try { + const params = { + pageSize, + includeMovie + }; + + if (sortKey) params.sortKey = sortKey; + if (sortDir) params.sortDir = sortDir; + if (startDate) params.startDate = startDate; + + const response = await axios.get(`${this.url}/api/v3/history`, { + headers: { 'X-Api-Key': this.apiKey }, + params + }); + return response.data; + } catch (error) { + logToFile(`[PollingRadarrRetriever] ${this.id} history error: ${error.message}`); + return { records: [] }; + } + } +} + +module.exports = PollingRadarrRetriever; diff --git a/server/clients/PollingSonarrRetriever.js b/server/clients/PollingSonarrRetriever.js new file mode 100644 index 0000000..16a224a --- /dev/null +++ b/server/clients/PollingSonarrRetriever.js @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Gordon Bolton. MIT License. +const axios = require('axios'); +const ArrRetriever = require('./ArrRetriever'); +const { logToFile } = require('../utils/logger'); + +/** + * Polling-based Sonarr data retriever. + * Implements the ArrRetriever interface using direct HTTP polling. + */ +class PollingSonarrRetriever extends ArrRetriever { + constructor(instanceConfig) { + super(instanceConfig); + } + + getRetrieverType() { + return 'sonarr'; + } + + /** + * Get tags from Sonarr instance + * @returns {Promise} Array of tag objects + */ + async getTags() { + try { + const response = await axios.get(`${this.url}/api/v3/tag`, { + headers: { 'X-Api-Key': this.apiKey } + }); + return response.data; + } catch (error) { + logToFile(`[PollingSonarrRetriever] ${this.id} tags error: ${error.message}`); + return []; + } + } + + /** + * Get queue from Sonarr instance + * @returns {Promise} Queue object with records array + */ + async getQueue() { + try { + 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] ${this.id} queue error: ${error.message}`); + return { records: [] }; + } + } + + /** + * Get history from Sonarr 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 + * @param {string} [options.sortDir] - Sort direction ('ascending' or 'descending') + * @param {boolean} [options.includeSeries=true] - Include series data + * @param {boolean} [options.includeEpisode=true] - Include episode data + * @param {string} [options.startDate] - ISO date string for filtering + * @returns {Promise} History object with records array + */ + async getHistory(options = {}) { + const { + pageSize = 10, + sortKey, + sortDir, + includeSeries = true, + includeEpisode = true, + startDate + } = options; + + try { + const params = { + pageSize, + includeSeries, + includeEpisode + }; + + if (sortKey) params.sortKey = sortKey; + if (sortDir) params.sortDir = sortDir; + if (startDate) params.startDate = startDate; + + const response = await axios.get(`${this.url}/api/v3/history`, { + headers: { 'X-Api-Key': this.apiKey }, + params + }); + return response.data; + } catch (error) { + logToFile(`[PollingSonarrRetriever] ${this.id} history error: ${error.message}`); + return { records: [] }; + } + } +} + +module.exports = PollingSonarrRetriever; diff --git a/server/utils/arrRetrievers.js b/server/utils/arrRetrievers.js new file mode 100644 index 0000000..c405bcb --- /dev/null +++ b/server/utils/arrRetrievers.js @@ -0,0 +1,307 @@ +// Copyright (c) 2026 Gordon Bolton. MIT License. +const { logToFile } = require('./logger'); +const { + getSonarrInstances, + getRadarrInstances +} = require('./config'); + +// Import retriever classes +const PollingSonarrRetriever = require('../clients/PollingSonarrRetriever'); +const PollingRadarrRetriever = require('../clients/PollingRadarrRetriever'); + +// Retriever type mapping +const retrieverClasses = { + sonarr: PollingSonarrRetriever, + radarr: PollingRadarrRetriever +}; + +/** + * Singleton registry for *arr data retrievers + */ +const arrRetrieverRegistry = { + retrievers: new Map(), + initialized: false, + + /** + * Initialize all configured *arr retrievers + */ + async initialize() { + if (this.initialized) { + return; + } + + logToFile('[ArrRetrieverRegistry] Initializing *arr retrievers...'); + + // Get all instance configurations + const sonarrInstances = getSonarrInstances(); + const radarrInstances = getRadarrInstances(); + + // Create retriever instances + const instanceConfigs = [ + ...sonarrInstances.map(inst => ({ ...inst, type: 'sonarr' })), + ...radarrInstances.map(inst => ({ ...inst, type: 'radarr' })) + ]; + + for (const config of instanceConfigs) { + try { + const RetrieverClass = retrieverClasses[config.type]; + if (!RetrieverClass) { + logToFile(`[ArrRetrieverRegistry] Unknown retriever type: ${config.type}`); + continue; + } + + const retriever = new RetrieverClass(config); + this.retrievers.set(config.id, retriever); + logToFile(`[ArrRetrieverRegistry] Created ${config.type} retriever: ${config.name} (${config.id})`); + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Failed to create retriever ${config.id}: ${error.message}`); + } + } + + this.initialized = true; + logToFile(`[ArrRetrieverRegistry] Initialized ${this.retrievers.size} *arr retrievers`); + }, + + /** + * Get all registered retrievers + * @returns {Array} Array of retriever instances + */ + getAllRetrievers() { + return Array.from(this.retrievers.values()); + }, + + /** + * Get retriever by instance ID + * @param {string} instanceId - The instance ID + * @returns {ArrRetriever|null} Retriever instance or null if not found + */ + getRetriever(instanceId) { + return this.retrievers.get(instanceId) || null; + }, + + /** + * Get retrievers by type + * @param {string} type - Retriever type ('sonarr', 'radarr') + * @returns {Array} Array of retriever instances + */ + getRetrieversByType(type) { + return this.getAllRetrievers().filter(retriever => retriever.getRetrieverType() === type); + }, + + /** + * Get tags from all retrievers + * @returns {Promise>} Array of tag results with instance info + */ + async getAllTags() { + const retrievers = this.getAllRetrievers(); + if (retrievers.length === 0) { + return []; + } + + // Fetch tags from all retrievers in parallel + const results = await Promise.allSettled( + retrievers.map(async (retriever) => { + try { + const tags = await retriever.getTags(); + logToFile(`[ArrRetrieverRegistry] ${retriever.name}: ${tags.length} tags`); + return { instance: retriever.getInstanceId(), data: tags }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching tags from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: [] }; + } + }) + ); + + return results + .filter(result => result.status === 'fulfilled') + .map(result => result.value); + }, + + /** + * Get queue from all retrievers + * @returns {Promise>} Array of queue results with instance info + */ + async getAllQueues() { + const retrievers = this.getAllRetrievers(); + if (retrievers.length === 0) { + return []; + } + + // Fetch queues from all retrievers in parallel + const results = await Promise.allSettled( + retrievers.map(async (retriever) => { + try { + const queue = await retriever.getQueue(); + logToFile(`[ArrRetrieverRegistry] ${retriever.name}: ${(queue.records || []).length} queue items`); + return { instance: retriever.getInstanceId(), data: queue }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching queue from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: { records: [] } }; + } + }) + ); + + return results + .filter(result => result.status === 'fulfilled') + .map(result => result.value); + }, + + /** + * Get history from all retrievers + * @param {Object} options - Optional parameters for history fetch + * @returns {Promise>} Array of history results with instance info + */ + async getAllHistory(options = {}) { + const retrievers = this.getAllRetrievers(); + if (retrievers.length === 0) { + return []; + } + + // Fetch history from all retrievers in parallel + const results = await Promise.allSettled( + retrievers.map(async (retriever) => { + try { + const history = await retriever.getHistory(options); + logToFile(`[ArrRetrieverRegistry] ${retriever.name}: ${(history.records || []).length} history records`); + return { instance: retriever.getInstanceId(), data: history }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching history from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: { records: [] } }; + } + }) + ); + + return results + .filter(result => result.status === 'fulfilled') + .map(result => result.value); + }, + + /** + * Get tags grouped by retriever type + * @returns {Promise} Tags grouped by retriever type (array of { instance, data } objects) + */ + async getTagsByType() { + const sonarrRetrievers = this.getRetrieversByType('sonarr'); + const radarrRetrievers = this.getRetrieversByType('radarr'); + + const sonarrTags = await Promise.allSettled( + sonarrRetrievers.map(async (retriever) => { + try { + const tags = await retriever.getTags(); + return { instance: retriever.getInstanceId(), data: tags }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching tags from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: [] }; + } + }) + ); + + const radarrTags = await Promise.allSettled( + radarrRetrievers.map(async (retriever) => { + try { + const tags = await retriever.getTags(); + return { instance: retriever.getInstanceId(), data: tags }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching tags from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: [] }; + } + }) + ); + + return { + sonarr: sonarrTags + .filter(result => result.status === 'fulfilled') + .map(result => result.value), + radarr: radarrTags + .filter(result => result.status === 'fulfilled') + .map(result => result.value) + }; + }, + + /** + * Get queue grouped by retriever type + * @returns {Promise} Queue grouped by retriever type + */ + async getQueuesByType() { + const sonarrRetrievers = this.getRetrieversByType('sonarr'); + const radarrRetrievers = this.getRetrieversByType('radarr'); + + const sonarrQueues = await Promise.allSettled( + sonarrRetrievers.map(async (retriever) => { + try { + const queue = await retriever.getQueue(); + return { instance: retriever.getInstanceId(), data: queue }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching queue from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: { records: [] } }; + } + }) + ); + + const radarrQueues = await Promise.allSettled( + radarrRetrievers.map(async (retriever) => { + try { + const queue = await retriever.getQueue(); + return { instance: retriever.getInstanceId(), data: queue }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching queue from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: { records: [] } }; + } + }) + ); + + return { + sonarr: sonarrQueues + .filter(result => result.status === 'fulfilled') + .map(result => result.value), + radarr: radarrQueues + .filter(result => result.status === 'fulfilled') + .map(result => result.value) + }; + }, + + /** + * Get history grouped by retriever type + * @param {Object} options - Optional parameters for history fetch + * @returns {Promise} History grouped by retriever type + */ + async getHistoryByType(options = {}) { + const sonarrRetrievers = this.getRetrieversByType('sonarr'); + const radarrRetrievers = this.getRetrieversByType('radarr'); + + const sonarrHistory = await Promise.allSettled( + sonarrRetrievers.map(async (retriever) => { + try { + const history = await retriever.getHistory(options); + return { instance: retriever.getInstanceId(), data: history }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching history from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: { records: [] } }; + } + }) + ); + + const radarrHistory = await Promise.allSettled( + radarrRetrievers.map(async (retriever) => { + try { + const history = await retriever.getHistory(options); + return { instance: retriever.getInstanceId(), data: history }; + } catch (error) { + logToFile(`[ArrRetrieverRegistry] Error fetching history from ${retriever.name}: ${error.message}`); + return { instance: retriever.getInstanceId(), data: { records: [] } }; + } + }) + ); + + return { + sonarr: sonarrHistory + .filter(result => result.status === 'fulfilled') + .map(result => result.value), + radarr: radarrHistory + .filter(result => result.status === 'fulfilled') + .map(result => result.value) + }; + } +}; + +module.exports = arrRetrieverRegistry; diff --git a/server/utils/historyFetcher.js b/server/utils/historyFetcher.js index 8f5066d..01a8165 100644 --- a/server/utils/historyFetcher.js +++ b/server/utils/historyFetcher.js @@ -1,7 +1,7 @@ // Copyright (c) 2026 Gordon Bolton. MIT License. -const axios = require('axios'); const cache = require('./cache'); const { getSonarrInstances, getRadarrInstances } = require('./config'); +const arrRetrieverRegistry = require('./arrRetrievers'); // Cache TTL for recent-history data: 5 minutes. // History changes slowly compared to active downloads. @@ -26,21 +26,26 @@ async function fetchSonarrHistory(since) { const cached = cache.get(cacheKey); if (cached) return cached; + // Ensure retrievers are initialized + await arrRetrieverRegistry.initialize(); + const instances = getSonarrInstances(); - const results = await Promise.all(instances.map(async inst => { + 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 axios.get(`${inst.url}/api/v3/history`, { - headers: { 'X-Api-Key': inst.apiKey }, - params: { - pageSize: 100, - sortKey: 'date', - sortDir: 'descending', - includeSeries: true, - includeEpisode: true, - startDate: since.toISOString() - } + const response = await retriever.getHistory({ + pageSize: 100, + sortKey: 'date', + sortDir: 'descending', + includeSeries: true, + includeEpisode: true, + startDate: since.toISOString() }); - const records = (response.data && response.data.records) || []; + const records = (response && response.records) || []; return records.map(r => { if (r.series) r.series._instanceUrl = inst.url; if (r.series) r.series._instanceName = inst.name || inst.id; @@ -70,20 +75,25 @@ async function fetchRadarrHistory(since) { const cached = cache.get(cacheKey); if (cached) return cached; + // Ensure retrievers are initialized + await arrRetrieverRegistry.initialize(); + const instances = getRadarrInstances(); - const results = await Promise.all(instances.map(async inst => { + 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 axios.get(`${inst.url}/api/v3/history`, { - headers: { 'X-Api-Key': inst.apiKey }, - params: { - pageSize: 100, - sortKey: 'date', - sortDir: 'descending', - includeMovie: true, - startDate: since.toISOString() - } + const response = await retriever.getHistory({ + pageSize: 100, + sortKey: 'date', + sortDir: 'descending', + includeMovie: true, + startDate: since.toISOString() }); - const records = (response.data && response.data.records) || []; + const records = (response && response.records) || []; return records.map(r => { if (r.movie) r.movie._instanceUrl = inst.url; if (r.movie) r.movie._instanceName = inst.name || inst.id; diff --git a/server/utils/poller.js b/server/utils/poller.js index f10c1dd..8bc882f 100644 --- a/server/utils/poller.js +++ b/server/utils/poller.js @@ -2,6 +2,7 @@ const axios = require('axios'); const cache = require('./cache'); const { initializeClients, getAllDownloads, getDownloadsByClientType } = require('./downloadClients'); +const arrRetrieverRegistry = require('./arrRetrievers'); const { getSonarrInstances, getRadarrInstances @@ -38,8 +39,9 @@ async function pollAllServices() { const start = Date.now(); try { - // Ensure download clients are initialized + // Ensure download clients and *arr retrievers are initialized await initializeClients(); + await arrRetrieverRegistry.initialize(); const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); @@ -50,58 +52,30 @@ async function pollAllServices() { const downloadsByType = await getDownloadsByClientType(); return downloadsByType; }), - timed('Sonarr Tags', () => Promise.all(sonarrInstances.map(inst => - axios.get(`${inst.url}/api/v3/tag`, { - headers: { 'X-Api-Key': inst.apiKey } - }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { - console.error(`[Poller] Sonarr ${inst.id} tags error:`, err.message); - return { instance: inst.id, data: [] }; - }) - ))), - timed('Sonarr Queue', () => Promise.all(sonarrInstances.map(inst => - axios.get(`${inst.url}/api/v3/queue`, { - headers: { 'X-Api-Key': inst.apiKey }, - params: { includeSeries: true, includeEpisode: true } - }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { - console.error(`[Poller] Sonarr ${inst.id} queue error:`, err.message); - return { instance: inst.id, data: { records: [] } }; - }) - ))), - timed('Sonarr History', () => Promise.all(sonarrInstances.map(inst => - axios.get(`${inst.url}/api/v3/history`, { - headers: { 'X-Api-Key': inst.apiKey }, - params: { pageSize: 10, includeEpisode: true } - }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { - console.error(`[Poller] Sonarr ${inst.id} history error:`, err.message); - return { instance: inst.id, data: { records: [] } }; - }) - ))), - timed('Radarr Queue', () => Promise.all(radarrInstances.map(inst => - axios.get(`${inst.url}/api/v3/queue`, { - headers: { 'X-Api-Key': inst.apiKey }, - params: { includeMovie: true } - }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { - console.error(`[Poller] Radarr ${inst.id} queue error:`, err.message); - return { instance: inst.id, data: { records: [] } }; - }) - ))), - timed('Radarr History', () => Promise.all(radarrInstances.map(inst => - axios.get(`${inst.url}/api/v3/history`, { - headers: { 'X-Api-Key': inst.apiKey }, - params: { pageSize: 10 } - }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { - console.error(`[Poller] Radarr ${inst.id} history error:`, err.message); - return { instance: inst.id, data: { records: [] } }; - }) - ))), - timed('Radarr Tags', () => Promise.all(radarrInstances.map(inst => - axios.get(`${inst.url}/api/v3/tag`, { - headers: { 'X-Api-Key': inst.apiKey } - }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { - console.error(`[Poller] Radarr ${inst.id} tags error:`, err.message); - return { instance: inst.id, data: [] }; - }) - ))), + timed('Sonarr Tags', async () => { + const tagsByType = await arrRetrieverRegistry.getTagsByType(); + return tagsByType.sonarr || []; + }), + timed('Sonarr Queue', async () => { + const queuesByType = await arrRetrieverRegistry.getQueuesByType(); + return queuesByType.sonarr || []; + }), + timed('Sonarr History', async () => { + const historyByType = await arrRetrieverRegistry.getHistoryByType({ pageSize: 10 }); + return historyByType.sonarr || []; + }), + timed('Radarr Queue', async () => { + const queuesByType = await arrRetrieverRegistry.getQueuesByType(); + return queuesByType.radarr || []; + }), + timed('Radarr History', async () => { + const historyByType = await arrRetrieverRegistry.getHistoryByType({ pageSize: 10 }); + return historyByType.radarr || []; + }), + timed('Radarr Tags', async () => { + const tagsByType = await arrRetrieverRegistry.getTagsByType(); + return tagsByType.radarr || []; + }), ]); const [