- Add includeEpisode:true to Sonarr queue and history API requests in both the poller and historyFetcher - Add extractEpisode() / gatherEpisodes() helpers in dashboard.js and history.js to build a sorted, deduplicated episodes array covering all records matching a download title (handles multi- episode packs and series packs) - Replace episodeInfo: sonarrMatch with episodes: gatherEpisodes() across all 8 assignment sites in dashboard.js - Add episodes field to /api/history/recent response items - Frontend: formatEpisodeInfo() renders S01E05 for single episodes or 'Multiple episodes' with hover tooltip listing all for packs - CSS: .episode-info and .multi-episode tooltip styles - ARCHITECTURE.md: update polling table and download/history schemas
143 lines
4.7 KiB
JavaScript
143 lines
4.7 KiB
JavaScript
const axios = require('axios');
|
|
const cache = require('./cache');
|
|
const { getSonarrInstances, getRadarrInstances } = require('./config');
|
|
|
|
// 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;
|
|
|
|
const instances = getSonarrInstances();
|
|
const results = await Promise.all(instances.map(async inst => {
|
|
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 records = (response.data && response.data.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;
|
|
|
|
const instances = getRadarrInstances();
|
|
const results = await Promise.all(instances.map(async inst => {
|
|
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 records = (response.data && response.data.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
|
|
};
|