feat: show episode info on download and history cards
Build and Push Docker Image / build (push) Successful in 37s
CI / Security audit (push) Successful in 59s
CI / Tests & coverage (push) Successful in 54s

- 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
This commit is contained in:
2026-05-17 17:03:23 +01:00
parent c1fb55c5b8
commit d1496a76e2
7 changed files with 142 additions and 14 deletions
+34 -1
View File
@@ -82,6 +82,37 @@ function buildTagBadges(allTags, embyUserMap) {
});
}
// Extract episode info from a Sonarr history record.
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 history records
// that share the same source title. Returns sorted, deduplicated array.
function gatherEpisodes(titleLower, records) {
const episodes = [];
const seen = new Set();
for (const r of records) {
const rTitle = (r.sourceTitle || r.title || '').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;
}
function getSonarrLink(series) {
if (!series || !series._instanceUrl || !series.titleSlug) return null;
return `${series._instanceUrl}/series/${series.titleSlug}`;
@@ -177,11 +208,13 @@ router.get('/recent', requireAuth, async (req, res) => {
? record.quality.quality.name
: null;
const sourceTitle = record.sourceTitle || record.title || series.title;
const item = {
type: 'series',
outcome,
title: record.sourceTitle || record.title || series.title,
title: sourceTitle,
seriesName: series.title,
episodes: gatherEpisodes(sourceTitle.toLowerCase(), sonarrHistory),
coverArt: getCoverArt(series),
completedAt: record.date,
quality,