/* WATCH — hero, player, source list (switch mid-watch), episodes. URL params: ?id=&type= TV adds &s=&e= as playback progresses. */ (function () { 'use strict'; const params = new URLSearchParams(location.search); const id = (params.get('id') || '').trim(); const type = params.get('type') === 'tv' ? 'tv' : 'movie'; SR.app.initChrome(); const content = document.getElementById('content'); if (!id || !/^\d+$/.test(id)) { content.replaceChildren(SR.cards.emptyState('Missing ?id=', 'This page needs a TMDB id, e.g. watch.html?id=1396&type=tv')); return; } const heroEl = document.getElementById('hero'); const heroBackdrop = document.getElementById('heroBackdrop'); const heroTitleCard = document.getElementById('heroTitleCard'); const heroTitle = document.getElementById('heroTitle'); const heroMeta = document.getElementById('heroMeta'); const heroGenres = document.getElementById('heroGenres'); const heroOverview = document.getElementById('heroOverview'); const playButton = document.getElementById('playButton'); const playLabel = document.getElementById('playLabel'); const episodeSection = document.getElementById('episodeSection'); const seasonTabs = document.getElementById('seasonTabs'); const episodeShelf = document.getElementById('episodeShelf'); const playerOverlay = document.getElementById('playerOverlay'); const playerClose = document.getElementById('playerClose'); // custom player controls const video = document.getElementById('video'); const statusEl = document.getElementById('playerStatus'); const sourceLabel = document.getElementById('sourceLabel'); const resumeNote = document.getElementById('resumeNote'); const sourcesPanel = document.getElementById('sourcesPanel'); const sourcesClose = document.getElementById('sourcesClose'); const playerWrap = document.getElementById('playerWrap'); const bigPlay = document.getElementById('bigPlay'); const pcSeek = document.getElementById('pcSeek'); const pcPlay = document.getElementById('pcPlay'); const pcMute = document.getElementById('pcMute'); const pcVol = document.getElementById('pcVol'); const pcTime = document.getElementById('pcTime'); const pcCc = document.getElementById('pcCc'); const pcQuality = document.getElementById('pcQuality'); const pcGear = document.getElementById('pcGear'); const pcPip = document.getElementById('pcPip'); const pcFull = document.getElementById('pcFull'); const state = { details: null, seasons: [], episodes: [], streams: [], unsupported: [], providerErrors: {}, loadErrors: {}, sourceIndex: -1, season: null, episode: null, loadedKey: null, loadToken: 0, overlayOpen: false, lastFocusedElement: null, pendingClose: false, }; /* ---- status overlay ------------------------------------------------------ */ function closeSources() { sourcesPanel.hidden = true; pcGear.classList.remove('is-on'); } function showStatus(msg, isError) { closeSources(); statusEl.hidden = false; playerWrap.classList.remove('player-wrap--controls', 'player-wrap--paused'); statusEl.innerHTML = isError ? `

Playback problem

${SR.util.escapeHtml(msg)}

` : `

${SR.util.escapeHtml(msg)}

`; } function hideStatus() { statusEl.hidden = true; syncPaused(); } /* ---- player --------------------------------------------------------------- */ const player = SR.player.create(video, { onStatus: (msg) => showStatus(msg), onReady: (resumeAt) => { hideStatus(); if (resumeAt > 10) resumeNote.textContent = `Resuming at ${SR.util.formatSeconds(resumeAt)}`; }, onError: (msg) => { showStatus(msg, true); SR.app.toast(msg, true); }, }); /* ---- custom player controls -------------------------------------------------- */ const ICON_PLAY = ''; const ICON_PAUSE = ''; const ICON_VOL = ''; const ICON_VOL_X = ''; let hideTimer = null; let scrubbing = false; function paintProgress(el, ratio) { const pct = Math.max(0, Math.min(1, ratio)) * 100; el.style.setProperty('--pc-progress', pct + '%'); } function timeLabel() { const d = video.duration; const ds = d && isFinite(d) ? SR.util.formatSeconds(d) : '0:00'; return `${SR.util.formatSeconds(video.currentTime)} / ${ds}`; } function syncPlayIcon() { const playing = !video.paused && !video.ended; pcPlay.innerHTML = playing ? ICON_PAUSE : ICON_PLAY; pcPlay.setAttribute('aria-label', playing ? 'Pause' : 'Play'); } function syncVolIcon() { const muted = video.muted || video.volume === 0; pcMute.innerHTML = muted ? ICON_VOL_X : ICON_VOL; pcMute.setAttribute('aria-label', muted ? 'Unmute' : 'Mute'); pcMute.classList.toggle('is-on', muted); pcVol.value = String(video.muted ? 0 : video.volume); paintProgress(pcVol, video.muted ? 0 : video.volume); } function syncTimeUI() { if (scrubbing) return; const d = video.duration; if (d && isFinite(d) && d > 0) { pcSeek.value = String(Math.round((video.currentTime / d) * 1000)); paintProgress(pcSeek, video.currentTime / d); } pcTime.textContent = timeLabel(); } function syncPaused() { playerWrap.classList.toggle('player-wrap--paused', video.paused); } function togglePlay() { if (!video.src && !video.currentSrc) return; if (video.paused || video.ended) { video.play().catch(() => {}); } else { video.pause(); } } function showControls() { playerWrap.classList.add('player-wrap--controls'); clearTimeout(hideTimer); hideTimer = setTimeout(() => { if (!video.paused && !video.ended && !scrubbing && sourcesPanel.hidden) { playerWrap.classList.remove('player-wrap--controls'); } }, 3000); } function toggleFullscreen() { const el = playerWrap; const doc = document; if (doc.fullscreenElement || doc.webkitFullscreenElement) { (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc); } else { (el.requestFullscreen || el.webkitRequestFullscreen).call(el); } } function togglePip() { if (!document.pictureInPictureEnabled) { SR.app.toast('Picture-in-picture is not supported in this browser', true); return; } if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); } else { video.requestPictureInPicture().catch(() => {}); } } function toggleCc() { const tracks = video.textTracks; let target = null; for (let i = 0; i < tracks.length; i++) { if (tracks[i].kind === 'subtitles') { target = tracks[i]; break; } } if (!target) { SR.app.toast('This source has no subtitles', true); return; } target.mode = target.mode === 'showing' ? 'hidden' : 'showing'; pcCc.classList.toggle('is-on', target.mode === 'showing'); pcCc.setAttribute('aria-pressed', String(target.mode === 'showing')); } // video events video.addEventListener('click', () => { if (!sourcesPanel.hidden) { closeSources(); return; } togglePlay(); }); video.addEventListener('dblclick', toggleFullscreen); video.addEventListener('play', () => { syncPlayIcon(); syncPaused(); showControls(); }); video.addEventListener('pause', () => { syncPlayIcon(); syncPaused(); showControls(); }); video.addEventListener('loadedmetadata', () => { syncPlayIcon(); syncPaused(); syncTimeUI(); }); video.addEventListener('timeupdate', syncTimeUI); video.addEventListener('volumechange', syncVolIcon); // buttons bigPlay.addEventListener('click', (e) => { e.stopPropagation(); togglePlay(); }); pcPlay.addEventListener('click', togglePlay); pcMute.addEventListener('click', () => { video.muted = !video.muted; }); pcCc.addEventListener('click', toggleCc); pcGear.addEventListener('click', () => { if (sourcesPanel.hidden) { sourcesPanel.hidden = false; pcGear.classList.add('is-on'); showControls(); } else { closeSources(); } }); sourcesClose.addEventListener('click', closeSources); pcPip.addEventListener('click', togglePip); pcFull.addEventListener('click', toggleFullscreen); // seek + volume scrubbing pcSeek.addEventListener('pointerdown', () => { scrubbing = true; showControls(); }); pcSeek.addEventListener('input', () => { const d = video.duration; if (d && isFinite(d) && d > 0) { paintProgress(pcSeek, pcSeek.value / 1000); pcTime.textContent = `${SR.util.formatSeconds((pcSeek.value / 1000) * d)} / ${SR.util.formatSeconds(d)}`; } }); pcSeek.addEventListener('change', () => { const d = video.duration; if (d && isFinite(d) && d > 0) { video.currentTime = (pcSeek.value / 1000) * d; } scrubbing = false; syncTimeUI(); }); pcVol.addEventListener('input', () => { video.volume = Number(pcVol.value); video.muted = video.volume === 0; }); // reveal controls on hover/interaction playerWrap.addEventListener('pointermove', showControls); playerWrap.addEventListener('pointerdown', showControls); // keyboard shortcuts document.addEventListener('keydown', (e) => { const tag = (e.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea' || tag === 'select') return; const d = video.duration; switch (e.key) { case ' ': case 'k': e.preventDefault(); togglePlay(); break; case 'ArrowRight': if (d && isFinite(d)) video.currentTime = Math.min(d, video.currentTime + 10); break; case 'ArrowLeft': if (d && isFinite(d)) video.currentTime = Math.max(0, video.currentTime - 10); break; case 'ArrowUp': e.preventDefault(); video.volume = Math.min(1, video.volume + 0.05); video.muted = false; break; case 'ArrowDown': e.preventDefault(); video.volume = Math.max(0, video.volume - 0.05); break; case 'm': video.muted = !video.muted; break; case 'f': toggleFullscreen(); break; case 'c': toggleCc(); break; case 'Escape': if (!sourcesPanel.hidden) closeSources(); else if (state.overlayOpen) { if (document.fullscreenElement) { state.pendingClose = true; document.exitFullscreen().catch(() => {}); } else { closePlayer(); } } break; } if (video.src || video.currentSrc) showControls(); }); // initial icon state syncPlayIcon(); syncVolIcon(); /* ---- hero ------------------------------------------------------------------ */ function openPlayer() { if (state.overlayOpen) return; state.overlayOpen = true; state.lastFocusedElement = document.activeElement; playerOverlay.hidden = false; document.body.classList.add('player-open'); playerClose.focus(); } function closePlayer() { if (!state.overlayOpen) return; state.overlayOpen = false; state.pendingClose = false; video.pause(); playerOverlay.hidden = true; document.body.classList.remove('player-open'); if (state.lastFocusedElement && typeof state.lastFocusedElement.focus === 'function') { state.lastFocusedElement.focus(); } } document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement && state.pendingClose && state.overlayOpen) closePlayer(); }); function syncHeader() { document.body.classList.toggle('is-scrolled', window.scrollY > 8); } window.addEventListener('scroll', syncHeader, { passive: true }); syncHeader(); function setPlayLabel() { if (type === 'tv' && state.season != null && state.episode != null) { playLabel.textContent = `Play S${state.season} E${state.episode}`; } else { playLabel.textContent = 'Play'; } } function renderHero(d) { document.title = `${d.name || d.title} — bug.tv`; const isTv = type === 'tv'; const year = String((isTv ? d.first_air_date : d.release_date) || '').slice(0, 4); const facts = []; if (isTv) { if (d.number_of_seasons) { facts.push(`${d.number_of_seasons} season${d.number_of_seasons === 1 ? '' : 's'}`); } } else { const runtime = SR.util.runtimeLabel(d.runtime); if (runtime) facts.push(runtime); } if (year) facts.push(year); const vote = Number(d.vote_average || 0); const filled = Math.max(0, Math.min(5, Math.round(vote / 2))); const rating = vote ? `${'★'.repeat(filled)}${'☆'.repeat(5 - filled)} ${vote.toFixed(1)}` : ''; const title = d.name || d.title || ''; heroTitle.textContent = title; heroMeta.innerHTML = [rating, ...facts.map((fact) => `${SR.util.escapeHtml(fact)}`)] .filter(Boolean) .join(''); heroGenres.replaceChildren(); for (const genre of d.genres || []) { const tag = document.createElement('span'); tag.className = 'tag'; tag.textContent = genre.name; heroGenres.appendChild(tag); } heroOverview.textContent = d.overview || ''; heroOverview.hidden = !d.overview; const images = d.images || {}; const backdrop = d.backdrop_url || (images.backdrops && images.backdrops[0] && images.backdrops[0].url); heroBackdrop.style.backgroundImage = backdrop ? `url("${SR.util.escapeHtml(SR.util.tmdbResize(backdrop, 'w1280'))}")` : ''; const logos = images.logos || []; const logo = (logos.find((l) => l && l.url && String(l.iso_639_1 || '').toLowerCase() === 'en') || {}).url; if (logo) { heroTitleCard.alt = title; heroTitleCard.src = SR.util.tmdbResize(logo, 'w500'); heroTitleCard.hidden = false; heroTitle.hidden = true; heroTitleCard.onerror = () => { heroTitleCard.hidden = true; heroTitle.hidden = false; }; } else { heroTitleCard.hidden = true; heroTitle.hidden = false; } playButton.hidden = false; setPlayLabel(); } function renderHeroError(message) { heroTitle.textContent = 'Unavailable'; heroTitle.hidden = false; heroMeta.textContent = message; heroOverview.textContent = ''; heroBackdrop.style.backgroundImage = ''; heroTitleCard.hidden = true; playButton.hidden = true; episodeSection.hidden = true; } /* ---- url -------------------------------------------------------------------- */ function syncUrl() { const p = new URLSearchParams(); p.set('id', id); p.set('type', type); if (type === 'tv') { if (state.season != null) p.set('s', state.season); if (state.episode != null) p.set('e', state.episode); } history.replaceState(null, '', `?${p.toString()}`); } /* ---- streams ------------------------------------------------------------------ */ function keyFor() { return SR.storage.keyFor({ type, tmdbId: Number(id), season: state.season, episode: state.episode }); } let providerCatalog = null; // cached per page load; enabled names are re-read from localStorage every lookup async function getProviderCatalog() { if (providerCatalog) return providerCatalog; try { const data = await SR.api.providers(); providerCatalog = { all: data.all || [], enabled: data.enabled || [] }; } catch (e) { return { all: [], enabled: [] }; } return providerCatalog; } async function getProviderInfo() { if (SR.config.getProviders()) return SR.config.resolveProviders(null); return SR.config.resolveProviders(await getProviderCatalog()); } async function loadStreams() { const token = ++state.loadToken; const providerInfo = await getProviderInfo(); if (token !== state.loadToken) return; if (!providerInfo.useServerDefaults && providerInfo.names.length === 0) { closeSources(); pcQuality.textContent = ''; sourceLabel.textContent = ''; resumeNote.textContent = ''; showStatus('No sources are enabled. Open Settings → Sources and enable at least one provider.', true); return; } const query = { tmdb: id, type, providers: providerInfo.useServerDefaults ? null : providerInfo.names, }; if (type === 'tv') { if (state.season == null || state.episode == null) return; query.season = state.season; query.episode = state.episode; } closeSources(); pcQuality.textContent = ''; showStatus('Finding streams…'); sourceLabel.textContent = ''; resumeNote.textContent = ''; try { const data = await SR.api.streams(query); if (token !== state.loadToken) return; const allStreams = data.streams || []; state.streams = allStreams.filter(SR.util.webPlayable); state.unsupported = allStreams.filter((s) => !SR.util.webPlayable(s)); state.providerErrors = data.providerErrors || {}; state.loadErrors = data.loadErrors || {}; renderSources(); pcGear.hidden = state.streams.length === 0; if (state.streams.length) playSource(0); else { const failed = Object.keys({ ...state.loadErrors, ...state.providerErrors }); const hidden = state.unsupported.length; if (failed.length || hidden) { const parts = ['No web-playable streams found.']; if (failed.length) parts.push(`Provider failures: ${failed.join(', ')}.`); if (hidden) parts.push(`Hidden ${hidden} unsupported source(s).`); showStatus(parts.join(' '), true); } else { showStatus('No streams found for this title.', true); } } } catch (e) { if (token !== state.loadToken) return; SR.app.showError(e, 'stream lookup failed'); showStatus('Stream lookup failed.', true); } } // Sort the popup by provider: provider name is the primary key (A→Z), // original rank is the tie-breaker so order inside a provider is stable. function sortedStreams() { return state.streams .map((s, i) => ({ s, i })) .sort((a, b) => { const pa = String(a.s.provider || '').toLowerCase(); const pb = String(b.s.provider || '').toLowerCase(); if (pa !== pb) return pa < pb ? -1 : 1; return a.i - b.i; }); } function renderSources() { const list = document.getElementById('sourceList'); list.replaceChildren(); let lastProvider = null; for (const { s, i } of sortedStreams()) { if (s.provider !== lastProvider) { lastProvider = s.provider; const h = document.createElement('div'); h.className = 'player-sources__group'; h.textContent = s.provider || 'Other'; list.appendChild(h); } const b = document.createElement('button'); b.type = 'button'; b.className = i === state.sourceIndex ? 'source-item source-item--active' : 'source-item'; const meta = [s.quality, s.audio].filter(Boolean).join(' · '); b.innerHTML = ` ${i + 1} ${SR.util.escapeHtml(s.name)} ${meta ? `${SR.util.escapeHtml(meta)}` : ''}`; b.addEventListener('click', () => playSource(i)); list.appendChild(b); } const failures = { ...state.loadErrors, ...state.providerErrors }; const failed = Object.keys(failures); if (failed.length) { const note = document.createElement('div'); note.className = 'player-sources__group player-sources__group--error'; note.textContent = `Failed: ${failed.join(', ')}`; note.title = failed.map((name) => `${name}: ${failures[name] || 'no streams'}`).join('\n'); list.appendChild(note); } if (state.unsupported.length) { const types = [ ...new Set(state.unsupported.map((s) => String(s.type || 'unknown').toLowerCase())), ]; const note = document.createElement('div'); note.className = 'player-sources__group player-sources__group--error'; note.textContent = `Hidden unsupported: ${state.unsupported.length} (${types.join(', ')})`; note.title = state.unsupported .map((s) => `${s.provider || 'Other'}: ${s.name || 'stream'} (${s.type || 'unknown'})`) .join('\n'); list.appendChild(note); } const count = `${state.streams.length} source${state.streams.length === 1 ? '' : 's'}`; document.getElementById('sourcesCount').textContent = state.unsupported.length ? `${count} · ${state.unsupported.length} hidden` : count; } function playSource(i) { const s = state.streams[i]; if (!s) return; const key = keyFor(); // Same episode as the one currently loaded → source switch: carry the // playhead over (resumeAt: null). A different episode → fresh resume: // pick up its own saved record, or start from the top. const resumeAt = state.loadedKey === key ? null : (() => { const rec = SR.storage.get(key); return rec && rec.t > 10 ? rec.t : 0; })(); state.sourceIndex = i; state.loadedKey = key; closeSources(); renderSources(); sourceLabel.textContent = `${s.name} — ${[s.quality, s.provider].filter(Boolean).join(', ')}`; pcQuality.textContent = s.quality || ''; player.load(s, { resumeAt }); } /* ---- resume persistence ---------------------------------------------------------- */ let lastPersist = 0; function persistProgress(force) { const d = video.duration; if (!d || !isFinite(d)) return; const t = video.currentTime; if (t < 10) return; const key = keyFor(); if (t >= d - 15 || t / d >= 0.95) { SR.storage.remove(key); return; } const now = Date.now(); if (!force && now - lastPersist < 3000) return; lastPersist = now; const ep = state.episodes.find((e) => e.episode_number === state.episode); SR.storage.save({ key, type, tmdbId: Number(id), season: state.season, episode: state.episode, show: state.details ? state.details.name || state.details.title : '', title: ep ? ep.name || '' : '', poster: state.details ? SR.util.tmdbResize(state.details.poster_url, 'w342') : '', t, d, at: now, provider: state.streams[state.sourceIndex] ? state.streams[state.sourceIndex].provider : '', }); } video.addEventListener('timeupdate', () => persistProgress(false)); video.addEventListener('pause', () => persistProgress(true)); video.addEventListener('seeked', () => persistProgress(true)); video.addEventListener('ended', () => SR.storage.remove(keyFor())); window.addEventListener('pagehide', () => persistProgress(true)); document.addEventListener('visibilitychange', () => { if (document.hidden) persistProgress(true); }); /* ---- episodes ------------------------------------------------------------------------ */ async function loadSeasons() { const data = await SR.api.seasons(id); state.seasons = (data.seasons || []).slice().sort((a, b) => a.season_number - b.season_number); renderSeasonTabs(); } function renderSeasonTabs() { seasonTabs.replaceChildren(); for (const s of state.seasons) { const b = document.createElement('button'); b.type = 'button'; b.role = 'tab'; b.className = s.season_number === state.season ? 'chip chip--active' : 'chip'; b.textContent = `S${s.season_number}`; b.title = `${s.name || `Season ${s.season_number}`} (${s.episode_count || 0})`; b.addEventListener('click', () => { if (s.season_number !== state.season) loadEpisodes(s.season_number, null); }); seasonTabs.appendChild(b); } } function showShelfSpinner() { episodeShelf.replaceChildren(); const cell = document.createElement('div'); cell.className = 'episode-shelf__loading'; cell.innerHTML = '
'; episodeShelf.appendChild(cell); } function progressFor(season, episode) { const key = SR.storage.keyFor({ type, tmdbId: Number(id), season, episode }); const rec = SR.storage.get(key); if (!rec || !rec.t || !rec.d) return 0; return Math.max(0, Math.min(1, rec.t / rec.d)); } function renderEpisodes() { episodeShelf.replaceChildren(); if (!state.episodes.length) { const empty = SR.cards.emptyState('No episodes listed'); empty.className = `${empty.className} episode-shelf__empty`; episodeShelf.appendChild(empty); return; } for (const ep of state.episodes) { const current = ep.episode_number === state.episode; const b = document.createElement('button'); b.type = 'button'; b.className = current ? 'episode-card episode-card--current' : 'episode-card'; const date = ep.air_date ? ` · ${SR.util.escapeHtml(ep.air_date)}` : ''; const still = ep.still || (ep.still_path ? SR.util.tmdbResize(ep.still_path, 'w300') : ''); const progress = progressFor(state.season, ep.episode_number); b.innerHTML = ` ${still ? `` : ``} ${progress > 0 ? `` : ''} S${state.season} · E${ep.episode_number}${date} ${SR.util.escapeHtml(ep.name || `Episode ${ep.episode_number}`)} ${SR.util.escapeHtml(SR.util.runtimeLabel(ep.runtime) || '')} `; const img = b.querySelector('.episode-card__thumb img'); if (img) { img.addEventListener('error', () => { const thumb = img.closest('.episode-card__thumb'); img.remove(); thumb.classList.add('episode-card__thumb--empty'); const num = document.createElement('span'); num.setAttribute('aria-hidden', 'true'); num.textContent = ep.episode_number; thumb.prepend(num); }); } b.addEventListener('click', () => playEpisode(ep.episode_number)); episodeShelf.appendChild(b); } } async function loadEpisodes(season, autoplayEpisode) { const token = ++state.loadToken; state.season = season; state.episode = autoplayEpisode != null ? autoplayEpisode : null; renderSeasonTabs(); showShelfSpinner(); const data = await SR.api.episodes(id, season); if (token !== state.loadToken) return; state.episodes = data.episodes || []; if (autoplayEpisode != null && state.episodes.some((e) => e.episode_number === autoplayEpisode)) { playEpisode(autoplayEpisode); return; } state.episode = state.episodes.length ? state.episodes[0].episode_number : null; renderEpisodes(); syncUrl(); setPlayLabel(); } function playEpisode(n) { state.episode = n; syncUrl(); renderEpisodes(); setPlayLabel(); openPlayer(); loadStreams(); } playButton.addEventListener('click', () => { if (type === 'tv' && state.episode == null) { if (state.episodes.length) playEpisode(state.episodes[0].episode_number); return; } if (state.loadedKey === keyFor() && state.streams.length) { openPlayer(); video.play().catch(() => {}); return; } openPlayer(); loadStreams(); }); playerClose.addEventListener('click', closePlayer); (async function init() { let details; try { details = await SR.api.details(type, id); } catch (e) { SR.app.showError(e, 'failed to load this title'); renderHeroError('Could not load this title.'); return; } state.details = details; renderHero(details); if (type !== 'tv') { setPlayLabel(); return; } episodeSection.hidden = false; showStatus('Loading episodes…'); try { await loadSeasons(); } catch (e) { SR.app.showError(e, 'failed to load seasons'); renderHeroError('Could not load seasons.'); return; } if (!state.seasons.length) { renderHeroError('No seasons found for this show.'); return; } const sParam = parseInt(params.get('s'), 10); const eParam = parseInt(params.get('e'), 10); const hasSeason = (n) => state.seasons.some((s) => s.season_number === n); const season = hasSeason(sParam) ? sParam : hasSeason(1) ? 1 : state.seasons[0].season_number; try { await loadEpisodes(season, null); } catch (e) { SR.app.showError(e, 'failed to load episodes'); renderHeroError('Could not load episodes.'); return; } if (!state.episodes.length) { renderHeroError('No episodes found for this show.'); return; } if (season === sParam && state.episodes.some((e) => e.episode_number === eParam)) { playEpisode(eParam); } })(); })();