Files
sofarr/server/utils/historyFetcher.js
T
gronod b9b5d7d393
Build and Push Docker Image / build (push) Successful in 39s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 55s
CI / Security audit (push) Successful in 1m6s
CI / Tests & coverage (push) Successful in 1m15s
Increase history pageSize from 100 to 500 to fetch more records
Fixes issue where series beyond position 100 in history were not appearing
in recently completed section.
2026-05-21 01:10:36 +01:00

154 lines
5.0 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
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.
const HISTORY_CACHE_TTL = 5 * 60 * 1000;
// Sonarr event types that represent a successful import
const SONARR_IMPORTED_EVENTS = new Set(['downloadFolderImported', 'downloadImported']);
// Sonarr event types that represent a failed import
const SONARR_FAILED_EVENTS = new Set(['downloadFailed', 'importFailed']);
// Radarr equivalents
const RADARR_IMPORTED_EVENTS = new Set(['downloadFolderImported', 'downloadImported']);
const RADARR_FAILED_EVENTS = new Set(['downloadFailed', 'importFailed']);
/**
* Fetch recent history records from all Sonarr instances for the given date window.
* Results are cached under 'history:sonarr' for HISTORY_CACHE_TTL.
* @param {Date} since - Only include records on or after this date
* @returns {Promise<Array>} Flat array of Sonarr history records (with _instanceUrl and _instanceName)
*/
async function fetchSonarrHistory(since) {
const cacheKey = 'history:sonarr';
const cached = cache.get(cacheKey);
if (cached) return cached;
// Ensure retrievers are initialized
await arrRetrieverRegistry.initialize();
const instances = getSonarrInstances();
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 retriever.getHistory({
pageSize: 500,
sortKey: 'date',
sortDir: 'descending',
includeSeries: true,
includeEpisode: true,
startDate: since.toISOString()
});
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;
r._instanceUrl = inst.url;
r._instanceName = inst.name || inst.id;
return r;
});
} catch (err) {
console.error(`[HistoryFetcher] Sonarr ${inst.id} error:`, err.message);
return [];
}
}));
const flat = results.flat();
cache.set(cacheKey, flat, HISTORY_CACHE_TTL);
return flat;
}
/**
* Fetch recent history records from all Radarr instances for the given date window.
* Results are cached under 'history:radarr' for HISTORY_CACHE_TTL.
* @param {Date} since - Only include records on or after this date
* @returns {Promise<Array>} Flat array of Radarr history records (with _instanceUrl and _instanceName)
*/
async function fetchRadarrHistory(since) {
const cacheKey = 'history:radarr';
const cached = cache.get(cacheKey);
if (cached) return cached;
// Ensure retrievers are initialized
await arrRetrieverRegistry.initialize();
const instances = getRadarrInstances();
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 retriever.getHistory({
pageSize: 500,
sortKey: 'date',
sortDir: 'descending',
includeMovie: true,
startDate: since.toISOString()
});
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;
r._instanceUrl = inst.url;
r._instanceName = inst.name || inst.id;
return r;
});
} catch (err) {
console.error(`[HistoryFetcher] Radarr ${inst.id} error:`, err.message);
return [];
}
}));
const flat = results.flat();
cache.set(cacheKey, flat, HISTORY_CACHE_TTL);
return flat;
}
/**
* Classify a Sonarr history record's event type.
* @param {string} eventType
* @returns {'imported'|'failed'|'other'}
*/
function classifySonarrEvent(eventType) {
if (SONARR_IMPORTED_EVENTS.has(eventType)) return 'imported';
if (SONARR_FAILED_EVENTS.has(eventType)) return 'failed';
return 'other';
}
/**
* Classify a Radarr history record's event type.
* @param {string} eventType
* @returns {'imported'|'failed'|'other'}
*/
function classifyRadarrEvent(eventType) {
if (RADARR_IMPORTED_EVENTS.has(eventType)) return 'imported';
if (RADARR_FAILED_EVENTS.has(eventType)) return 'failed';
return 'other';
}
/**
* Invalidate cached history so the next request fetches fresh data.
* Called externally if needed (e.g. after a forced refresh).
*/
function invalidateHistoryCache() {
cache.invalidate('history:sonarr');
cache.invalidate('history:radarr');
}
module.exports = {
fetchSonarrHistory,
fetchRadarrHistory,
classifySonarrEvent,
classifyRadarrEvent,
invalidateHistoryCache,
HISTORY_CACHE_TTL
};