|
|
|
|
@@ -94,6 +94,38 @@ function getRadarrLink(movie) {
|
|
|
|
|
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract episode info from a Sonarr queue/history record.
|
|
|
|
|
// Returns { season, episode, title } or null if data is missing.
|
|
|
|
|
function extractEpisode(record) {
|
|
|
|
|
const s = record.seasonNumber;
|
|
|
|
|
const e = record.episodeNumber;
|
|
|
|
|
if (s == null || e == null) return null;
|
|
|
|
|
const title = record.episode && record.episode.title ? record.episode.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch all Emby users and return a Map<lowerName -> displayName> (and sanitized variants).
|
|
|
|
|
// Result is cached for 60s to avoid hammering Emby on every dashboard poll.
|
|
|
|
|
async function getEmbyUsers() {
|
|
|
|
|
@@ -277,7 +309,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|
|
|
|
speed: slotState.speed,
|
|
|
|
|
eta: slot.timeleft,
|
|
|
|
|
seriesName: series.title,
|
|
|
|
|
episodeInfo: sonarrMatch,
|
|
|
|
|
episodes: gatherEpisodes(nzbNameLower, sonarrQueue.data.records),
|
|
|
|
|
allTags,
|
|
|
|
|
matchedUserTag: matchedUserTag || null,
|
|
|
|
|
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
|
|
|
|
@@ -374,7 +406,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|
|
|
|
size: slot.size,
|
|
|
|
|
completedAt: slot.completed_time,
|
|
|
|
|
seriesName: series.title,
|
|
|
|
|
episodeInfo: sonarrMatch,
|
|
|
|
|
episodes: gatherEpisodes(nzbNameLower, sonarrHistory.data.records),
|
|
|
|
|
allTags,
|
|
|
|
|
matchedUserTag: matchedUserTag || null,
|
|
|
|
|
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
|
|
|
|
@@ -477,7 +509,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|
|
|
|
download.type = 'series';
|
|
|
|
|
download.coverArt = getCoverArt(series);
|
|
|
|
|
download.seriesName = series.title;
|
|
|
|
|
download.episodeInfo = sonarrMatch;
|
|
|
|
|
download.episodes = gatherEpisodes(torrentNameLower, sonarrQueue.data.records);
|
|
|
|
|
download.allTags = allTags;
|
|
|
|
|
download.matchedUserTag = matchedUserTag || null;
|
|
|
|
|
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
|
|
|
|
|
@@ -547,7 +579,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|
|
|
|
download.type = 'series';
|
|
|
|
|
download.coverArt = getCoverArt(series);
|
|
|
|
|
download.seriesName = series.title;
|
|
|
|
|
download.episodeInfo = sonarrHistoryMatch;
|
|
|
|
|
download.episodes = gatherEpisodes(torrentNameLower, sonarrHistory.data.records);
|
|
|
|
|
download.allTags = allTags;
|
|
|
|
|
download.matchedUserTag = matchedUserTag || null;
|
|
|
|
|
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
|
|
|
|
|
@@ -857,7 +889,7 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|
|
|
|
const allTags = extractAllTags(series.tags, sonarrTagMap);
|
|
|
|
|
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
|
|
|
|
|
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
|
|
|
|
|
const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
|
|
|
|
|
const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, seriesName: series.title, episodes: gatherEpisodes(nzbNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
|
|
|
|
|
const issues = getImportIssues(sonarrMatch);
|
|
|
|
|
if (issues) dlObj.importIssues = issues;
|
|
|
|
|
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); }
|
|
|
|
|
@@ -904,7 +936,7 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|
|
|
|
const allTags = extractAllTags(series.tags, sonarrTagMap);
|
|
|
|
|
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
|
|
|
|
|
if (showAll ? allTags.length > 0 : !!matchedUserTag) {
|
|
|
|
|
const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slot.status, size: slot.size, completedAt: slot.completed_time, seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
|
|
|
|
|
const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slot.status, size: slot.size, completedAt: slot.completed_time, seriesName: series.title, episodes: gatherEpisodes(nzbNameLower, sonarrHistory.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
|
|
|
|
|
if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); }
|
|
|
|
|
userDownloads.push(dlObj);
|
|
|
|
|
}
|
|
|
|
|
@@ -944,7 +976,7 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|
|
|
|
const matchedUserTag = 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, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
|
|
|
|
|
Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodes: gatherEpisodes(torrentNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? 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); }
|
|
|
|
|
userDownloads.push(download); continue;
|
|
|
|
|
@@ -976,7 +1008,7 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|
|
|
|
const matchedUserTag = 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, episodeInfo: sonarrHistoryMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
|
|
|
|
|
Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodes: gatherEpisodes(torrentNameLower, sonarrHistory.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
|
|
|
|
|
if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); }
|
|
|
|
|
userDownloads.push(download); continue;
|
|
|
|
|
}
|
|
|
|
|
|