Extract DownloadAssembler service from dashboard routes
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

- 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)
This commit is contained in:
2026-05-20 22:32:09 +01:00
parent 4d61dd566f
commit 9cffb96f29
3 changed files with 915 additions and 149 deletions
+53 -149
View File
@@ -11,103 +11,7 @@ const { getSonarrInstances, getRadarrInstances } = require('../utils/config');
const downloadClientRegistry = require('../utils/downloadClients');
const sanitizeError = require('../utils/sanitizeError');
const TagMatcher = require('../services/TagMatcher');
// 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) {
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;
}
const DownloadAssembler = require('../services/DownloadAssembler');
// Track active dashboard clients.
@@ -249,7 +153,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
const dlObj = {
type: 'series',
title: nzbName,
coverArt: getCoverArt(series),
coverArt: DownloadAssembler.getCoverArt(series),
status: slotState.status,
progress: slot.percentage,
mb: slot.mb,
@@ -258,17 +162,17 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
speed: slotState.speed,
eta: slot.timeleft,
seriesName: series.title,
episodes: gatherEpisodes(nzbNameLower, sonarrQueue.data.records),
episodes: DownloadAssembler.gatherEpisodes(nzbNameLower, sonarrQueue.data.records),
allTags,
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined
};
const issues = getImportIssues(sonarrMatch);
const issues = DownloadAssembler.getImportIssues(sonarrMatch);
if (issues) dlObj.importIssues = issues;
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = series.path || null;
dlObj.arrLink = getSonarrLink(series);
dlObj.arrLink = DownloadAssembler.getSonarrLink(series);
dlObj.arrQueueId = sonarrMatch.id;
dlObj.arrType = 'sonarr';
dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null;
@@ -276,7 +180,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
dlObj.arrContentId = sonarrMatch.episodeId || null;
dlObj.arrContentType = 'episode';
}
dlObj.canBlocklist = canBlocklist(dlObj, isAdmin);
dlObj.canBlocklist = DownloadAssembler.canBlocklist(dlObj, isAdmin);
userDownloads.push(dlObj);
}
}
@@ -298,7 +202,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
const dlObj = {
type: 'movie',
title: nzbName,
coverArt: getCoverArt(movie),
coverArt: DownloadAssembler.getCoverArt(movie),
status: slotState.status,
progress: slot.percentage,
mb: slot.mb,
@@ -312,12 +216,12 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined
};
const issues = getImportIssues(radarrMatch);
const issues = DownloadAssembler.getImportIssues(radarrMatch);
if (issues) dlObj.importIssues = issues;
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = movie.path || null;
dlObj.arrLink = getRadarrLink(movie);
dlObj.arrLink = DownloadAssembler.getRadarrLink(movie);
dlObj.arrQueueId = radarrMatch.id;
dlObj.arrType = 'radarr';
dlObj.arrInstanceUrl = radarrMatch._instanceUrl || null;
@@ -325,7 +229,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
dlObj.arrContentId = radarrMatch.movieId || null;
dlObj.arrContentType = 'movie';
}
dlObj.canBlocklist = canBlocklist(dlObj, isAdmin);
dlObj.canBlocklist = DownloadAssembler.canBlocklist(dlObj, isAdmin);
userDownloads.push(dlObj);
}
}
@@ -364,12 +268,12 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
const dlObj = {
type: 'series',
title: nzbName,
coverArt: getCoverArt(series),
coverArt: DownloadAssembler.getCoverArt(series),
status: slot.status,
size: slot.size,
completedAt: slot.completed_time,
seriesName: series.title,
episodes: gatherEpisodes(nzbNameLower, sonarrHistory.data.records),
episodes: DownloadAssembler.gatherEpisodes(nzbNameLower, sonarrHistory.data.records),
allTags,
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined
@@ -377,7 +281,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = series.path || null;
dlObj.arrLink = getSonarrLink(series);
dlObj.arrLink = DownloadAssembler.getSonarrLink(series);
}
userDownloads.push(dlObj);
}
@@ -400,7 +304,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
const dlObj = {
type: 'movie',
title: nzbName,
coverArt: getCoverArt(movie),
coverArt: DownloadAssembler.getCoverArt(movie),
status: slot.status,
size: slot.size,
completedAt: slot.completed_time,
@@ -413,7 +317,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = movie.path || null;
dlObj.arrLink = getRadarrLink(movie);
dlObj.arrLink = DownloadAssembler.getRadarrLink(movie);
}
userDownloads.push(dlObj);
}
@@ -470,18 +374,18 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'series';
download.coverArt = getCoverArt(series);
download.coverArt = DownloadAssembler.getCoverArt(series);
download.seriesName = series.title;
download.episodes = gatherEpisodes(torrentNameLower, sonarrQueue.data.records);
download.episodes = DownloadAssembler.gatherEpisodes(torrentNameLower, sonarrQueue.data.records);
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined;
const sonarrIssues = getImportIssues(sonarrMatch);
const sonarrIssues = DownloadAssembler.getImportIssues(sonarrMatch);
if (sonarrIssues) download.importIssues = sonarrIssues;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = series.path || null;
download.arrLink = getSonarrLink(series);
download.arrLink = DownloadAssembler.getSonarrLink(series);
download.arrQueueId = sonarrMatch.id;
download.arrType = 'sonarr';
download.arrInstanceUrl = sonarrMatch._instanceUrl || null;
@@ -489,7 +393,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
download.arrContentId = sonarrMatch.episodeId || null;
download.arrContentType = 'episode';
}
download.canBlocklist = canBlocklist(download, isAdmin);
download.canBlocklist = DownloadAssembler.canBlocklist(download, isAdmin);
userDownloads.push(download);
continue; // Skip to next torrent
}
@@ -512,18 +416,18 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'movie';
download.coverArt = getCoverArt(movie);
download.coverArt = DownloadAssembler.getCoverArt(movie);
download.movieName = movie.title;
download.movieInfo = radarrMatch;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined;
const radarrIssues = getImportIssues(radarrMatch);
const radarrIssues = DownloadAssembler.getImportIssues(radarrMatch);
if (radarrIssues) download.importIssues = radarrIssues;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = movie.path || null;
download.arrLink = getRadarrLink(movie);
download.arrLink = DownloadAssembler.getRadarrLink(movie);
download.arrQueueId = radarrMatch.id;
download.arrType = 'radarr';
download.arrInstanceUrl = radarrMatch._instanceUrl || null;
@@ -531,7 +435,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
download.arrContentId = radarrMatch.movieId || null;
download.arrContentType = 'movie';
}
download.canBlocklist = canBlocklist(download, isAdmin);
download.canBlocklist = DownloadAssembler.canBlocklist(download, isAdmin);
userDownloads.push(download);
continue; // Skip to next torrent
}
@@ -554,16 +458,16 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'series';
download.coverArt = getCoverArt(series);
download.coverArt = DownloadAssembler.getCoverArt(series);
download.seriesName = series.title;
download.episodes = gatherEpisodes(torrentNameLower, sonarrHistory.data.records);
download.episodes = DownloadAssembler.gatherEpisodes(torrentNameLower, sonarrHistory.data.records);
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = series.path || null;
download.arrLink = getSonarrLink(series);
download.arrLink = DownloadAssembler.getSonarrLink(series);
}
userDownloads.push(download);
continue;
@@ -587,7 +491,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'movie';
download.coverArt = getCoverArt(movie);
download.coverArt = DownloadAssembler.getCoverArt(movie);
download.movieName = movie.title;
download.movieInfo = radarrHistoryMatch;
download.allTags = allTags;
@@ -596,7 +500,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = movie.path || null;
download.arrLink = getRadarrLink(movie);
download.arrLink = DownloadAssembler.getRadarrLink(movie);
}
userDownloads.push(download);
continue;
@@ -1039,11 +943,11 @@ router.get('/stream', requireAuth, async (req, res) => {
const allTags = TagMatcher.extractAllTags(series.tags, sonarrTagMap);
const matchedUserTag = TagMatcher.extractUserTag(series.tags, sonarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slotState.status, progress: Math.round(slot.progress * 100), mb: slot.mb, mbmissing: slot.mbleft, size: Math.round(slot.mb * 1024 * 1024), speed: Math.round((slot.kbpersec || 0) * 1024), eta: slot.timeleft, seriesName: series.title, episodes: gatherEpisodes(nzbNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
const issues = getImportIssues(sonarrMatch);
const dlObj = { type: 'series', title: nzbName, coverArt: DownloadAssembler.getCoverArt(series), status: slotState.status, progress: Math.round(slot.progress * 100), mb: slot.mb, mbmissing: slot.mbleft, size: Math.round(slot.mb * 1024 * 1024), speed: Math.round((slot.kbpersec || 0) * 1024), eta: slot.timeleft, seriesName: series.title, episodes: DownloadAssembler.gatherEpisodes(nzbNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
const issues = DownloadAssembler.getImportIssues(sonarrMatch);
if (issues) dlObj.importIssues = issues;
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); dlObj.arrQueueId = sonarrMatch.id; dlObj.arrType = 'sonarr'; dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null; dlObj.arrInstanceKey = sonarrMatch._instanceKey || null; dlObj.arrContentId = sonarrMatch.episodeId || null; dlObj.arrContentType = 'episode'; }
dlObj.canBlocklist = canBlocklist(dlObj, isAdmin);
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = DownloadAssembler.getSonarrLink(series); dlObj.arrQueueId = sonarrMatch.id; dlObj.arrType = 'sonarr'; dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null; dlObj.arrInstanceKey = sonarrMatch._instanceKey || null; dlObj.arrContentId = sonarrMatch.episodeId || null; dlObj.arrContentType = 'episode'; }
dlObj.canBlocklist = DownloadAssembler.canBlocklist(dlObj, isAdmin);
userDownloads.push(dlObj);
}
}
@@ -1057,11 +961,11 @@ router.get('/stream', requireAuth, async (req, res) => {
const allTags = TagMatcher.extractAllTags(movie.tags, radarrTagMap);
const matchedUserTag = TagMatcher.extractUserTag(movie.tags, radarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slotState.status, progress: Math.round(slot.progress * 100), mb: slot.mb, mbmissing: slot.mbleft, size: Math.round(slot.mb * 1024 * 1024), speed: Math.round((slot.kbpersec || 0) * 1024), eta: slot.timeleft, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
const issues = getImportIssues(radarrMatch);
const dlObj = { type: 'movie', title: nzbName, coverArt: DownloadAssembler.getCoverArt(movie), status: slotState.status, progress: Math.round(slot.progress * 100), mb: slot.mb, mbmissing: slot.mbleft, size: Math.round(slot.mb * 1024 * 1024), speed: Math.round((slot.kbpersec || 0) * 1024), eta: slot.timeleft, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
const issues = DownloadAssembler.getImportIssues(radarrMatch);
if (issues) dlObj.importIssues = issues;
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); dlObj.arrQueueId = radarrMatch.id; dlObj.arrType = 'radarr'; dlObj.arrInstanceUrl = radarrMatch._instanceUrl || null; dlObj.arrInstanceKey = radarrMatch._instanceKey || null; dlObj.arrContentId = radarrMatch.movieId || null; dlObj.arrContentType = 'movie'; }
dlObj.canBlocklist = canBlocklist(dlObj, isAdmin);
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = DownloadAssembler.getRadarrLink(movie); dlObj.arrQueueId = radarrMatch.id; dlObj.arrType = 'radarr'; dlObj.arrInstanceUrl = radarrMatch._instanceUrl || null; dlObj.arrInstanceKey = radarrMatch._instanceKey || null; dlObj.arrContentId = radarrMatch.movieId || null; dlObj.arrContentType = 'movie'; }
dlObj.canBlocklist = DownloadAssembler.canBlocklist(dlObj, isAdmin);
userDownloads.push(dlObj);
}
}
@@ -1086,8 +990,8 @@ router.get('/stream', requireAuth, async (req, res) => {
const allTags = TagMatcher.extractAllTags(series.tags, sonarrTagMap);
const matchedUserTag = TagMatcher.extractUserTag(series.tags, sonarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slot.status, mb: slot.mb, size: Math.round((slot.mb || 0) * 1024 * 1024), completedAt: slot.completed_time, seriesName: series.title, episodes: gatherEpisodes(nzbNameLower, sonarrHistory.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); }
const dlObj = { type: 'series', title: nzbName, coverArt: DownloadAssembler.getCoverArt(series), status: slot.status, mb: slot.mb, size: Math.round((slot.mb || 0) * 1024 * 1024), completedAt: slot.completed_time, seriesName: series.title, episodes: DownloadAssembler.gatherEpisodes(nzbNameLower, sonarrHistory.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = DownloadAssembler.getSonarrLink(series); }
userDownloads.push(dlObj);
}
}
@@ -1103,8 +1007,8 @@ router.get('/stream', requireAuth, async (req, res) => {
const allTags = TagMatcher.extractAllTags(movie.tags, radarrTagMap);
const matchedUserTag = TagMatcher.extractUserTag(movie.tags, radarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slot.status, mb: slot.mb, size: Math.round((slot.mb || 0) * 1024 * 1024), completedAt: slot.completed_time, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); }
const dlObj = { type: 'movie', title: nzbName, coverArt: DownloadAssembler.getCoverArt(movie), status: slot.status, mb: slot.mb, size: Math.round((slot.mb || 0) * 1024 * 1024), completedAt: slot.completed_time, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined, client: 'sabnzbd', instanceId: slot.instanceId || 'sabnzbd-default', instanceName: slot.instanceName || 'SABnzbd' };
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = DownloadAssembler.getRadarrLink(movie); }
userDownloads.push(dlObj);
}
}
@@ -1126,10 +1030,10 @@ router.get('/stream', requireAuth, async (req, res) => {
const matchedUserTag = TagMatcher.extractUserTag(series.tags, sonarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const download = mapTorrentToDownload(torrent);
Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodes: gatherEpisodes(torrentNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
const issues = getImportIssues(sonarrMatch); if (issues) download.importIssues = issues;
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); download.arrQueueId = sonarrMatch.id; download.arrType = 'sonarr'; download.arrInstanceUrl = sonarrMatch._instanceUrl || null; download.arrInstanceKey = sonarrMatch._instanceKey || null; download.arrContentId = sonarrMatch.episodeId || null; download.arrContentType = 'episode'; }
download.canBlocklist = canBlocklist(download, isAdmin);
Object.assign(download, { type: 'series', coverArt: DownloadAssembler.getCoverArt(series), seriesName: series.title, episodes: DownloadAssembler.gatherEpisodes(torrentNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
const issues = DownloadAssembler.getImportIssues(sonarrMatch); if (issues) download.importIssues = issues;
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = DownloadAssembler.getSonarrLink(series); download.arrQueueId = sonarrMatch.id; download.arrType = 'sonarr'; download.arrInstanceUrl = sonarrMatch._instanceUrl || null; download.arrInstanceKey = sonarrMatch._instanceKey || null; download.arrContentId = sonarrMatch.episodeId || null; download.arrContentType = 'episode'; }
download.canBlocklist = DownloadAssembler.canBlocklist(download, isAdmin);
userDownloads.push(download); continue;
}
}
@@ -1143,10 +1047,10 @@ router.get('/stream', requireAuth, async (req, res) => {
const matchedUserTag = TagMatcher.extractUserTag(movie.tags, radarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const download = mapTorrentToDownload(torrent);
Object.assign(download, { type: 'movie', coverArt: getCoverArt(movie), movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
const issues = getImportIssues(radarrMatch); if (issues) download.importIssues = issues;
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); download.arrQueueId = radarrMatch.id; download.arrType = 'radarr'; download.arrInstanceUrl = radarrMatch._instanceUrl || null; download.arrInstanceKey = radarrMatch._instanceKey || null; download.arrContentId = radarrMatch.movieId || null; download.arrContentType = 'movie'; }
download.canBlocklist = canBlocklist(download, isAdmin);
Object.assign(download, { type: 'movie', coverArt: DownloadAssembler.getCoverArt(movie), movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
const issues = DownloadAssembler.getImportIssues(radarrMatch); if (issues) download.importIssues = issues;
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = DownloadAssembler.getRadarrLink(movie); download.arrQueueId = radarrMatch.id; download.arrType = 'radarr'; download.arrInstanceUrl = radarrMatch._instanceUrl || null; download.arrInstanceKey = radarrMatch._instanceKey || null; download.arrContentId = radarrMatch.movieId || null; download.arrContentType = 'movie'; }
download.canBlocklist = DownloadAssembler.canBlocklist(download, isAdmin);
userDownloads.push(download); continue;
}
}
@@ -1160,8 +1064,8 @@ router.get('/stream', requireAuth, async (req, res) => {
const matchedUserTag = TagMatcher.extractUserTag(series.tags, sonarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const download = mapTorrentToDownload(torrent);
Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodes: gatherEpisodes(torrentNameLower, sonarrHistory.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); }
Object.assign(download, { type: 'series', coverArt: DownloadAssembler.getCoverArt(series), seriesName: series.title, episodes: DownloadAssembler.gatherEpisodes(torrentNameLower, sonarrHistory.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = DownloadAssembler.getSonarrLink(series); }
userDownloads.push(download); continue;
}
}
@@ -1175,8 +1079,8 @@ router.get('/stream', requireAuth, async (req, res) => {
const matchedUserTag = TagMatcher.extractUserTag(movie.tags, radarrTagMap, username);
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
const download = mapTorrentToDownload(torrent);
Object.assign(download, { type: 'movie', coverArt: getCoverArt(movie), movieName: movie.title, movieInfo: radarrHistoryMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); }
Object.assign(download, { type: 'movie', coverArt: DownloadAssembler.getCoverArt(movie), movieName: movie.title, movieInfo: radarrHistoryMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? TagMatcher.buildTagBadges(allTags, embyUserMap) : undefined });
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = DownloadAssembler.getRadarrLink(movie); }
userDownloads.push(download);
}
}