fix(queue): extract shared arr cache helper, annotate season packs, null-guard flatMap (closes #61)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
//
|
||||
// Shared helpers for assembling the cached *arr queue payload.
|
||||
//
|
||||
// Both the background poller (`server/utils/poller.js`) and the webhook
|
||||
// processor (`server/routes/webhook.js`) build the `poll:sonarr-queue` and
|
||||
// `poll:radarr-queue` cache entries from an array of per-instance queue
|
||||
// responses. Historically the same `flatMap` block was duplicated across all
|
||||
// four call sites (Sonarr + Radarr × poller + webhook) and had begun to drift.
|
||||
//
|
||||
// This module centralises that logic, adds defensive null-guards, and — for
|
||||
// Sonarr only — annotates season-pack records (queue entries sharing a
|
||||
// `downloadId`) with `isSeasonPack` and `episodeCount`. See Issue #61.
|
||||
//
|
||||
const { logToFile } = require('./logger');
|
||||
|
||||
/**
|
||||
* Build the flattened, instance-tagged `records` array for the
|
||||
* `poll:sonarr-queue` / `poll:radarr-queue` cache entry.
|
||||
*
|
||||
* @param {Array<{ instance: string, data: { records?: Array<object> } }>} queues
|
||||
* Per-instance queue responses as returned by
|
||||
* `arrRetrieverRegistry.getQueuesByType()` (or the equivalent batched
|
||||
* retrieval in the poller).
|
||||
* @param {Array<{ id: string, url: string, apiKey: string, name?: string }>} instances
|
||||
* Configured instances; used to resolve `_instanceUrl` / `_instanceKey`.
|
||||
* @param {'series'|'movie'} mediaKey
|
||||
* Sonarr records embed a `series` object; Radarr records embed a `movie`
|
||||
* object. The embedded object is annotated with `_instanceUrl` so that
|
||||
* downstream link builders work.
|
||||
* @returns {Array<object>} The flattened, annotated records array.
|
||||
*/
|
||||
function buildArrQueueCache(queues, instances, mediaKey) {
|
||||
if (!Array.isArray(queues) || queues.length === 0) return [];
|
||||
if (mediaKey !== 'series' && mediaKey !== 'movie') {
|
||||
logToFile(`[arrQueueHelpers] Invalid mediaKey "${mediaKey}"; expected 'series' or 'movie'`);
|
||||
return [];
|
||||
}
|
||||
const safeInstances = Array.isArray(instances) ? instances : [];
|
||||
|
||||
const out = [];
|
||||
for (const q of queues) {
|
||||
try {
|
||||
if (!q || !q.data) continue;
|
||||
const inst = safeInstances.find(i => i.id === q.instance);
|
||||
const url = inst ? inst.url : null;
|
||||
const key = inst ? inst.apiKey : null;
|
||||
const records = Array.isArray(q.data.records) ? q.data.records : [];
|
||||
for (const r of records) {
|
||||
try {
|
||||
if (!r) continue;
|
||||
if (r[mediaKey]) {
|
||||
r[mediaKey]._instanceUrl = url;
|
||||
}
|
||||
r._instanceUrl = url;
|
||||
r._instanceKey = key;
|
||||
out.push(r);
|
||||
} catch (perRecordErr) {
|
||||
logToFile(`[arrQueueHelpers] Skipping malformed ${mediaKey} record: ${perRecordErr.message}`);
|
||||
}
|
||||
}
|
||||
} catch (perInstanceErr) {
|
||||
logToFile(`[arrQueueHelpers] Skipping malformed ${mediaKey} queue payload: ${perInstanceErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Sonarr-only: season pack annotation. Group by downloadId; entries that
|
||||
// share a downloadId are episodes belonging to the same release (a season
|
||||
// pack). Movies (mediaKey === 'movie') are single-record by nature.
|
||||
if (mediaKey === 'series') {
|
||||
try {
|
||||
const groups = new Map();
|
||||
for (const r of out) {
|
||||
const dlId = r && r.downloadId;
|
||||
if (!dlId) continue;
|
||||
if (!groups.has(dlId)) groups.set(dlId, []);
|
||||
groups.get(dlId).push(r);
|
||||
}
|
||||
for (const group of groups.values()) {
|
||||
if (group.length > 1) {
|
||||
for (const r of group) {
|
||||
r.isSeasonPack = true;
|
||||
r.episodeCount = group.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (annotateErr) {
|
||||
logToFile(`[arrQueueHelpers] Season-pack annotation failed: ${annotateErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildArrQueueCache
|
||||
};
|
||||
+3
-22
@@ -3,6 +3,7 @@ const axios = require('axios');
|
||||
const cache = require('./cache');
|
||||
const { initializeClients, getAllDownloads, getDownloadsByClientType } = require('./downloadClients');
|
||||
const arrRetrieverRegistry = require('./arrRetrievers');
|
||||
const { buildArrQueueCache } = require('./arrQueueHelpers');
|
||||
const {
|
||||
getSonarrInstances,
|
||||
getRadarrInstances,
|
||||
@@ -237,17 +238,7 @@ async function pollAllServices() {
|
||||
cache.set('poll:sonarr-tags', sonarrTagsResults, cacheTTL);
|
||||
// Tag queue/history records with _instanceUrl so embedded series/movie objects can build links
|
||||
cache.set('poll:sonarr-queue', {
|
||||
records: sonarrQueues.flatMap(q => {
|
||||
const inst = sonarrInstances.find(i => i.id === q.instance);
|
||||
const url = inst ? inst.url : null;
|
||||
const key = inst ? inst.apiKey : null;
|
||||
return (q.data.records || []).map(r => {
|
||||
if (r.series) r.series._instanceUrl = url;
|
||||
r._instanceUrl = url;
|
||||
r._instanceKey = key;
|
||||
return r;
|
||||
});
|
||||
})
|
||||
records: buildArrQueueCache(sonarrQueues, sonarrInstances, 'series')
|
||||
}, cacheTTL);
|
||||
cache.set('poll:sonarr-history', {
|
||||
records: sonarrHistories.flatMap(h => h.data.records || [])
|
||||
@@ -265,17 +256,7 @@ async function pollAllServices() {
|
||||
// Radarr
|
||||
if (shouldPollRadarr) {
|
||||
cache.set('poll:radarr-queue', {
|
||||
records: radarrQueues.flatMap(q => {
|
||||
const inst = radarrInstances.find(i => i.id === q.instance);
|
||||
const url = inst ? inst.url : null;
|
||||
const key = inst ? inst.apiKey : null;
|
||||
return (q.data.records || []).map(r => {
|
||||
if (r.movie) r.movie._instanceUrl = url;
|
||||
r._instanceUrl = url;
|
||||
r._instanceKey = key;
|
||||
return r;
|
||||
});
|
||||
})
|
||||
records: buildArrQueueCache(radarrQueues, radarrInstances, 'movie')
|
||||
}, cacheTTL);
|
||||
cache.set('poll:radarr-history', {
|
||||
records: radarrHistories.flatMap(h => h.data.records || [])
|
||||
|
||||
Reference in New Issue
Block a user