9cffb96f29
- Create server/services/DownloadAssembler.js with 7 pure functions: - getCoverArt, getImportIssues, getSonarrLink, getRadarrLink - canBlocklist, extractEpisode, gatherEpisodes - Update server/routes/dashboard.js to use DownloadAssembler - Add comprehensive unit tests (73 tests covering edge cases) - Fix null check in extractEpisode function - All tests passing: DownloadAssembler (73/73), TagMatcher (26/26)
108 lines
3.8 KiB
JavaScript
108 lines
3.8 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
// Helper function to extract poster/cover art URL from a movie or series object
|
|
function getCoverArt(item) {
|
|
if (!item || !item.images) return null;
|
|
const poster = item.images.find(img => img.coverType === 'poster');
|
|
if (poster) return poster.remoteUrl || poster.url || null;
|
|
// Fallback to fanart if no poster
|
|
const fanart = item.images.find(img => img.coverType === 'fanart');
|
|
return fanart ? (fanart.remoteUrl || fanart.url || null) : null;
|
|
}
|
|
|
|
// Extract import issues from a Sonarr/Radarr queue record
|
|
function getImportIssues(queueRecord) {
|
|
if (!queueRecord) return null;
|
|
const state = queueRecord.trackedDownloadState;
|
|
const status = queueRecord.trackedDownloadStatus;
|
|
if (state !== 'importPending' && status !== 'warning' && status !== 'error') return null;
|
|
const messages = [];
|
|
if (queueRecord.statusMessages && queueRecord.statusMessages.length > 0) {
|
|
for (const sm of queueRecord.statusMessages) {
|
|
if (sm.messages && sm.messages.length > 0) {
|
|
messages.push(...sm.messages);
|
|
} else if (sm.title) {
|
|
messages.push(sm.title);
|
|
}
|
|
}
|
|
}
|
|
if (queueRecord.errorMessage) {
|
|
messages.push(queueRecord.errorMessage);
|
|
}
|
|
if (messages.length === 0) return null;
|
|
return messages;
|
|
}
|
|
|
|
// Helper to build Sonarr web UI link for a series
|
|
function getSonarrLink(series) {
|
|
if (!series || !series._instanceUrl || !series.titleSlug) return null;
|
|
return `${series._instanceUrl}/series/${series.titleSlug}`;
|
|
}
|
|
|
|
// Helper to build Radarr web UI link for a movie
|
|
function getRadarrLink(movie) {
|
|
if (!movie || !movie._instanceUrl || !movie.titleSlug) return null;
|
|
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
|
}
|
|
|
|
// Determine if a download can be blocklisted by the current user
|
|
// Admins: always true (they have arrQueueId)
|
|
// Non-admins: true if importIssues OR (torrent >1h old AND availability<100%)
|
|
function canBlocklist(download, isAdmin) {
|
|
if (isAdmin) return true;
|
|
if (download.importIssues && download.importIssues.length > 0) return true;
|
|
if (download.qbittorrent && download.addedOn && download.availability) {
|
|
const oneHourAgo = Date.now() - 3600000; // 1 hour in ms
|
|
const addedOn = new Date(download.addedOn).getTime();
|
|
const isOldEnough = addedOn < oneHourAgo;
|
|
const availability = parseFloat(download.availability);
|
|
const isLowAvailability = availability < 100;
|
|
return isOldEnough && isLowAvailability;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Extract episode info from a Sonarr queue/history record.
|
|
// Returns { season, episode, title } or null if data is missing.
|
|
function extractEpisode(record) {
|
|
if (!record) return null;
|
|
const ep = record.episode || {};
|
|
const s = ep.seasonNumber != null ? ep.seasonNumber : record.seasonNumber;
|
|
const e = ep.episodeNumber != null ? ep.episodeNumber : record.episodeNumber;
|
|
if (s == null || e == null) return null;
|
|
const title = ep.title || null;
|
|
return { season: s, episode: e, title };
|
|
}
|
|
|
|
// Find all episodes associated with a download by matching all queue/history records
|
|
// that share the same title string. Returns sorted array of { season, episode, title }.
|
|
function gatherEpisodes(titleLower, sonarrRecords) {
|
|
const episodes = [];
|
|
const seen = new Set();
|
|
for (const r of sonarrRecords) {
|
|
const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
|
|
if (rTitle && (rTitle.includes(titleLower) || titleLower.includes(rTitle))) {
|
|
const ep = extractEpisode(r);
|
|
if (ep) {
|
|
const key = `${ep.season}x${ep.episode}`;
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
episodes.push(ep);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
episodes.sort((a, b) => a.season - b.season || a.episode - b.episode);
|
|
return episodes;
|
|
}
|
|
|
|
module.exports = {
|
|
getCoverArt,
|
|
getImportIssues,
|
|
getSonarrLink,
|
|
getRadarrLink,
|
|
canBlocklist,
|
|
extractEpisode,
|
|
gatherEpisodes
|
|
};
|