';
- }
var searchTimer = null;
function $(s, r) { return (r || document).querySelector(s); }
@@ -91,8 +71,15 @@
function orbSize(n) { return n >= 10 ? 'orb-lg' : n >= 4 ? 'orb-md' : 'orb-sm'; }
function nebulaOrb(sh, idx) {
+ // Source-aware: a TMDB show opens its show page; a YouTube channel (source
+ // 'youtube') opens the in-app channel page. YEAR is the "season", video the
+ // "episode" — the data is already shaped that way, so the same render runs.
+ var yt = sh.source === 'youtube';
var src = sh.library_id != null ? 'library' : 'tmdb';
var openId = sh.library_id != null ? sh.library_id : sh.tmdb_id;
+ var openAttrs = yt
+ ? 'data-vwsh-open-channel data-yt="' + esc(sh.youtube_id) + '"'
+ : 'data-vwsh-open-show data-vwsh-src="' + src + '" data-vwsh-id="' + esc(openId) + '"';
var hue = hueOf(sh.title);
var total = sh.wanted || 0;
var img = sh.poster_url
@@ -100,27 +87,29 @@
'onerror="this.outerHTML=\'
' + esc(initials(sh.title)) + '
\'">'
: '
' + esc(initials(sh.title)) + '
';
// Episodes are shown grouped under a clickable season header (header →
- // show page); each episode card SELECTS the episode (drives the info bar).
- // Season = a poster panel on the LEFT (→ show page) with the episode grid
- // flowing to its RIGHT, so the horizontal space is actually used.
+ // show/channel page); each episode card SELECTS it (drives the info bar).
+ // Season = a poster panel on the LEFT with the episode grid to its RIGHT.
var seasons = (sh.seasons || []).map(function (se) {
var n = se.episodes.length;
var posterUrl = se.poster_url || sh.poster_url || null;
var thumb = posterUrl ? '' : '📺';
var cards = (se.episodes || []).map(function (e) { return epCard(sh, se, e); }).join('');
+ var sName = yt ? (se.season_number ? se.season_number : 'Undated') : ('Season ' + se.season_number);
+ var sRm = yt
+ ? 'data-vwsh-rm="yt-season" data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '"'
+ : 'data-vwsh-rm="season" data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '"';
return '
';
}).join('');
- var eps = total + ' episode' + (total === 1 ? '' : 's');
+ var eps = total + (yt ? ' video' : ' episode') + (total === 1 ? '' : 's');
// --orb-hue on the GROUP so the music orb styles + my cinematic-expand
// backdrop (--vwsh-poster) both resolve; poster bleeds in only when expanded.
var gstyle = 'animation-delay:' + Math.min(idx * 45, 700) + 'ms;--orb-hue:' + hue +
@@ -129,8 +118,12 @@
// Header is a 3-column row that FLANKS the poster: synopsis (left) · poster
// (middle) · cast (right). When collapsed (or no data) the side columns are
// empty → hidden → just the centered bubble, so the nebula grid is unchanged.
- return '
' +
'' +
@@ -175,6 +168,17 @@
var synEl = group && group.querySelector('[data-vwsh-syn]');
var castEl = group && group.querySelector('[data-vwsh-cast]');
if (!synEl || !castEl) return;
+ // YouTube: a video carries its own description; there's no cast and no
+ // tmdb episode endpoint, so just paint the selected video's synopsis.
+ if (group.getAttribute('data-vwsh-source') === 'youtube') {
+ if (sel) {
+ var yd = fmtDate(sel.air_date);
+ synEl.innerHTML = (yd ? '' + esc(yd) + '' : '') +
+ esc(sel.overview || 'No description for this video.');
+ } else { synEl.innerHTML = ''; }
+ castEl.innerHTML = '';
+ return;
+ }
var si = state.showInfo[tmdb] || {};
var eyebrow, overview, castArr;
if (sel) {
@@ -212,6 +216,7 @@
if (!group) return;
var tmdb = parseInt(group.getAttribute('data-vwsh-tmdb'), 10);
renderInfoBar(group, tmdb, null); // paint the View-show button immediately
+ if (group.getAttribute('data-vwsh-source') === 'youtube') return; // no tmdb detail for channels
if (group.getAttribute('data-vwsh-info-loaded')) return;
group.setAttribute('data-vwsh-info-loaded', '1');
var sh = state.showData[tmdb]; if (!sh) return;
@@ -229,38 +234,41 @@
// A single episode card. Clicking it SELECTS the episode (drives the info bar);
// the "View show" button in the info bar is what navigates.
function epCard(sh, se, e) {
- var t = e.title || ('Episode ' + e.episode_number);
+ var yt = sh.source === 'youtube';
+ var t = e.title || (yt ? 'Untitled' : ('Episode ' + e.episode_number));
var st = STATUS[e.status] ? e.status : 'wanted';
var date = fmtDate(e.air_date);
+ // TMDB shows the SxEx label; a YouTube video shows just its upload date.
+ var metaTxt = yt ? (date || 'Video') : ('S' + se.season_number + '·E' + e.episode_number + (date ? ' · ' + esc(date) : ''));
var thumb = e.still_url
? ''
: '';
- return '