Video get-modal: lazy-load episodes for un-owned (tmdb) shows
A show you don't own ships its seasons with no episodes (loaded per-season on expand, like the full detail page), so the modal mis-read '0 episodes' as 'all owned' and seasons expanded to nothing. - Render tmdb seasons as collapsible with an 'expand to load' placeholder; fetch /api/video/tmdb/show/<id>/season/<n> on first expand, render rows, pre-select the missing-aired episodes, enable the season select-all. - Skip the 'you have every episode' hint when any season is lazy/un-owned; lazy seasons aren't tagged owned, so they stay visible under 'Missing only'. - Next-episode line falls back to the payload's next_episode stub when no episodes are loaded (the only next-up source for un-owned shows).
This commit is contained in:
parent
0071358125
commit
4b7a915d39
2 changed files with 73 additions and 7 deletions
|
|
@ -136,7 +136,14 @@
|
|||
// Collapse/expand a season (ignore clicks on its checkbox).
|
||||
var sh = e.target.closest('[data-vgm-season-toggle]');
|
||||
if (sh && !e.target.closest('.vgm-season-check')) {
|
||||
sh.parentNode.classList.toggle('vgm-season--open');
|
||||
var seasonEl = sh.parentNode;
|
||||
var opening = !seasonEl.classList.contains('vgm-season--open');
|
||||
seasonEl.classList.toggle('vgm-season--open');
|
||||
// First expand of an un-owned (tmdb) season → fetch its episodes.
|
||||
if (opening && seasonEl.getAttribute('data-vgm-lazy') === '1' &&
|
||||
!seasonEl.getAttribute('data-vgm-loaded')) {
|
||||
loadSeason(seasonEl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('[data-vgm-wishlist]')) {
|
||||
|
|
@ -256,17 +263,36 @@
|
|||
badge + '</label>';
|
||||
}
|
||||
|
||||
function renderEpisodes(d) {
|
||||
function renderEpisodes(d, o) {
|
||||
var wrap = modalEl.querySelector('[data-vgm-eps]'); if (!wrap) return;
|
||||
if (!d.seasons || !d.seasons.length) { wrap.hidden = true; return; }
|
||||
var today = isoToday();
|
||||
modalState = { kind: 'show', sel: new Set() };
|
||||
var totalMissing = 0;
|
||||
// For an un-owned (tmdb) show the episodes aren't shipped with the detail —
|
||||
// they load per-season on expand (like the full detail page). Stash the tv
|
||||
// id so the lazy loader can fetch them.
|
||||
var tvId = (o && o.source === 'tmdb') ? parseInt(o.id, 10) : null;
|
||||
modalState = { kind: 'show', sel: new Set(), tvId: tvId, today: today };
|
||||
var totalMissing = 0, anyLazy = false;
|
||||
var html = '<div class="vgm-eps-head"><span class="vgm-eps-h">Episodes</span>' +
|
||||
'<label class="vgm-eps-toggle"><input type="checkbox" data-vgm-missing-only checked>' +
|
||||
'<span>Missing only</span></label></div><div class="vgm-eps-list">';
|
||||
d.seasons.forEach(function (s) {
|
||||
var eps = s.episodes || [];
|
||||
// Lazy: a tmdb season with a known count but no episodes shipped yet.
|
||||
if (!eps.length && (s.episode_total || 0) > 0 && tvId) {
|
||||
anyLazy = true;
|
||||
totalMissing += s.episode_total; // all wanted → never "all owned"
|
||||
html += '<div class="vgm-season" data-vgm-lazy="1" data-vgm-season="' + s.season_number + '">' +
|
||||
'<div class="vgm-season-head" data-vgm-season-toggle>' +
|
||||
'<span class="vgm-season-check"><input type="checkbox" data-vgm-season-all="' + s.season_number + '" disabled></span>' +
|
||||
'<span class="vgm-season-name">' + esc(s.title || ('Season ' + s.season_number)) + '</span>' +
|
||||
'<span class="vgm-season-meta">' + s.episode_total + ' eps</span>' +
|
||||
'<span class="vgm-season-chev" aria-hidden="true">⌄</span>' +
|
||||
'</div>' +
|
||||
'<div class="vgm-season-eps"><div class="vgm-season-loading">Expand to load episodes…</div></div>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
var missing = 0;
|
||||
eps.forEach(function (e) { if (epState(e, today) === 'missing') { missing++; modalState.sel.add(s.season_number + '_' + e.episode_number); } });
|
||||
totalMissing += missing;
|
||||
|
|
@ -284,7 +310,8 @@
|
|||
});
|
||||
html += '</div>';
|
||||
// When you own everything, the missing-only view would be blank — say so.
|
||||
if (totalMissing === 0) {
|
||||
// (Skipped for lazy/un-owned shows: nothing is owned, episodes load on expand.)
|
||||
if (totalMissing === 0 && !anyLazy) {
|
||||
html += '<div class="vgm-eps-allowned">✓ You have every episode. Turn off “Missing only” to re-download.</div>';
|
||||
}
|
||||
wrap.innerHTML = html;
|
||||
|
|
@ -294,6 +321,38 @@
|
|||
d.seasons.forEach(function (s) { syncSeasonCheck(modalEl, s.season_number); });
|
||||
}
|
||||
|
||||
// Fetch a tmdb season's episodes the first time it's expanded, render them,
|
||||
// and pre-select the missing-aired ones (everything un-owned that has aired).
|
||||
function loadSeason(seasonEl) {
|
||||
if (!modalState || !modalState.tvId || !seasonEl) return;
|
||||
var sn = parseInt(seasonEl.getAttribute('data-vgm-season'), 10);
|
||||
seasonEl.setAttribute('data-vgm-loaded', '1'); // guard double-fetch
|
||||
var body = seasonEl.querySelector('.vgm-season-eps');
|
||||
if (body) body.innerHTML = '<div class="vgm-season-loading">Loading episodes…</div>';
|
||||
fetch('/api/video/tmdb/show/' + modalState.tvId + '/season/' + sn, { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!modalEl || !seasonEl.parentNode) return;
|
||||
var eps = (data && data.episodes) || [];
|
||||
var b = seasonEl.querySelector('.vgm-season-eps');
|
||||
if (!eps.length) { if (b) b.innerHTML = '<div class="vgm-season-loading">No episode info available.</div>'; return; }
|
||||
var today = modalState.today;
|
||||
if (b) b.innerHTML = eps.map(function (e) { return epRow(sn, e, today); }).join('');
|
||||
eps.forEach(function (e) {
|
||||
if (epState(e, today) === 'missing') modalState.sel.add(sn + '_' + e.episode_number);
|
||||
});
|
||||
var all = seasonEl.querySelector('[data-vgm-season-all]');
|
||||
if (all) all.disabled = false;
|
||||
syncSeasonCheck(modalEl, sn);
|
||||
updateFooter();
|
||||
})
|
||||
.catch(function () {
|
||||
var b = seasonEl.querySelector('.vgm-season-eps');
|
||||
if (b) b.innerHTML = '<div class="vgm-season-loading">Couldn’t load episodes — tap to retry.</div>';
|
||||
seasonEl.removeAttribute('data-vgm-loaded'); // allow a retry on next expand
|
||||
});
|
||||
}
|
||||
|
||||
function fill(d, o) {
|
||||
var q = function (s) { return modalEl.querySelector(s); };
|
||||
var id = o.id;
|
||||
|
|
@ -341,7 +400,7 @@
|
|||
renderRatings(d); // IMDb / Rotten Tomatoes / Metacritic chips (both kinds)
|
||||
if (o.kind === 'show') {
|
||||
modalState = modalState || { kind: 'show', sel: new Set() };
|
||||
renderEpisodes(d); renderNext(d); maybeFollow(d); // selector + next-up + follow offer
|
||||
renderEpisodes(d, o); renderNext(d); maybeFollow(d); // selector + next-up + follow offer
|
||||
} else {
|
||||
modalState = { kind: 'movie', owned: !!d.owned }; // owned movies → re-download, not wishlist
|
||||
renderOwned(d);
|
||||
|
|
@ -391,11 +450,17 @@
|
|||
best = { s: s.season_number, e: e.episode_number, air_date: e.air_date, title: e.title };
|
||||
});
|
||||
});
|
||||
// Un-owned (tmdb) shows ship no episodes — fall back to the payload's
|
||||
// next_episode stub from the TMDB extras.
|
||||
if (!best && d.next_episode && d.next_episode.episode_number != null) {
|
||||
var ne = d.next_episode;
|
||||
best = { s: ne.season_number, e: ne.episode_number, air_date: ne.air_date, title: ne.name };
|
||||
}
|
||||
if (!best) { box.hidden = true; return; }
|
||||
box.innerHTML = '<span class="vgm-next-ic">▶</span>' +
|
||||
'<span class="vgm-next-txt"><strong>Next episode</strong> · S' + best.s + ' · E' + best.e +
|
||||
(best.title ? ' — ' + esc(best.title) : '') + '</span>' +
|
||||
'<span class="vgm-next-date">' + esc(fmtDate(best.air_date)) + '</span>';
|
||||
(best.air_date ? '<span class="vgm-next-date">' + esc(fmtDate(best.air_date)) + '</span>' : '');
|
||||
box.hidden = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2044,6 +2044,7 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
.vgm-season--open .vgm-season-chev { transform: rotate(180deg); }
|
||||
.vgm-season-eps { display: none; padding: 2px 6px 8px; }
|
||||
.vgm-season--open .vgm-season-eps { display: block; }
|
||||
.vgm-season-loading { padding: 14px 12px; font-size: 12.5px; font-weight: 600; color: rgba(255, 255, 255, 0.4); text-align: center; }
|
||||
|
||||
.vgm-ep { display: flex; align-items: center; gap: 11px; padding: 8px 10px; border-radius: 9px; cursor: pointer; transition: background 0.12s ease; }
|
||||
.vgm-ep:hover { background: rgba(255, 255, 255, 0.04); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue