All checks were successful
- Create ArrRetriever abstract base class defining pluggable interface - Implement PollingSonarrRetriever and PollingRadarrRetriever with HTTP polling - Add ArrRetrieverRegistry for managing retriever instances - Refactor poller.js to use retriever registry instead of direct Axios calls - Update historyFetcher.js to use retriever registry - Preserve all cache keys, TTLs, timing logs, SSE broadcasts, error handling - Enable future webhook listeners without touching poller logic
328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
// 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
|
|
};
|
|
|
|
/**
|
|
* Registry and factory for *arr data retrievers
|
|
*/
|
|
class ArrRetrieverRegistry {
|
|
constructor() {
|
|
this.retrievers = new Map();
|
|
this.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<ArrRetriever>} 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<ArrRetriever>} Array of retriever instances
|
|
*/
|
|
getRetrieversByType(type) {
|
|
return this.getAllRetrievers().filter(retriever => retriever.getRetrieverType() === type);
|
|
}
|
|
|
|
/**
|
|
* Get tags from all retrievers
|
|
* @returns {Promise<Array<Object>>} 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(retriever);
|
|
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<Object>>} 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(retriever);
|
|
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<Object>>} 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(retriever, 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<Object>} 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(retriever);
|
|
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(retriever);
|
|
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<Object>} 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(retriever);
|
|
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(retriever);
|
|
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<Object>} 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(retriever, 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(retriever, 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)
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
};
|