// 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 } }>} 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} 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 };