Files
sofarr/server/services/DownloadAssembler.js
T
gronod 9cffb96f29
Build and Push Docker Image / build (push) Successful in 45s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m9s
CI / Security audit (push) Successful in 1m24s
CI / Tests & coverage (push) Failing after 1m37s
Extract DownloadAssembler service from dashboard routes
- 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)
2026-05-20 22:32:09 +01:00

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