feat: implement Pluggable Abstraction Layer for Data Retrieval (PALDRA) - #19
All checks were successful
Build and Push Docker Image / build (push) Successful in 42s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 46s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m35s

- 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
This commit is contained in:
2026-05-19 14:43:28 +01:00
parent 9343486705
commit 627329df2f
6 changed files with 664 additions and 77 deletions

View File

@@ -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 { initializeRetrievers, getRetrieversByType } = 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 initializeRetrievers();
const instances = getSonarrInstances();
const results = await Promise.all(instances.map(async inst => {
const sonarrRetrievers = 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(retriever, {
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 initializeRetrievers();
const instances = getRadarrInstances();
const results = await Promise.all(instances.map(async inst => {
const radarrRetrievers = 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(retriever, {
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;