feat: show episode info on download and history cards
All checks were successful
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

View File

@@ -290,6 +290,30 @@ function hideLoginError() {
errorDiv.style.display = 'none';
}
// Build an episode-info element for series downloads/history.
// Single episode: "S01E05 — Episode Title"
// Multiple episodes: "Multiple episodes" with tooltip listing them all.
// Returns null if no episode data.
function formatEpisodeInfo(episodes) {
if (!episodes || episodes.length === 0) return null;
const el = document.createElement('p');
el.className = 'episode-info';
if (episodes.length === 1) {
const ep = episodes[0];
const code = 'S' + String(ep.season).padStart(2, '0') + 'E' + String(ep.episode).padStart(2, '0');
el.textContent = ep.title ? code + ' \u2014 ' + ep.title : code;
} else {
el.textContent = 'Multiple episodes';
el.classList.add('multi-episode');
const lines = episodes.map(ep => {
const code = 'S' + String(ep.season).padStart(2, '0') + 'E' + String(ep.episode).padStart(2, '0');
return ep.title ? code + ' \u2014 ' + ep.title : code;
});
el.setAttribute('data-tooltip', lines.join('\n'));
}
return el;
}
// fetchUserDownloads is kept for the showAll toggle re-connection case
// but the primary data path is now via SSE (startSSE / EventSource).
@@ -478,6 +502,8 @@ function createDownloadCard(download) {
series.textContent = `Series: ${download.seriesName}`;
}
infoDiv.appendChild(series);
const epEl = formatEpisodeInfo(download.episodes);
if (epEl) infoDiv.appendChild(epEl);
}
if (download.movieName) {
@@ -982,6 +1008,8 @@ function createHistoryCard(item) {
p.textContent = 'Series: ' + item.seriesName;
}
info.appendChild(p);
const epEl = formatEpisodeInfo(item.episodes);
if (epEl) info.appendChild(epEl);
}
if (item.movieName) {
const p = document.createElement('p');

View File

@@ -485,6 +485,36 @@ body {
font-size: 0.8rem;
}
.episode-info {
color: var(--text-secondary);
font-size: 0.78rem;
margin: -2px 0 6px;
}
.episode-info.multi-episode {
cursor: help;
text-decoration: underline dotted;
position: relative;
}
.episode-info.multi-episode:hover::after {
content: attr(data-tooltip);
position: absolute;
left: 0;
top: 100%;
z-index: 20;
background: var(--card-bg);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 10px;
font-size: 0.75rem;
white-space: pre-line;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-width: 320px;
pointer-events: none;
}
/* ===== Detail Row (Inline) ===== */
.download-details {
display: flex;