function decodeJwt(token) { return JSON.parse(atob(token.split('.')[1])); } function getQueryParam(name) { const url = new URL(window.location.href); return url.searchParams.get(name); } function showAlert(message) { const el = document.getElementById('alert'); if (!el) return; el.textContent = message; el.classList.remove('d-none'); } function setSourceLabel(text) { const el = document.getElementById('source-label'); if (!el) return; el.textContent = text || '—'; el.title = text || ''; } function setHeader(movie) { const titleEl = document.getElementById('movie-title'); const categoryEl = document.getElementById('movie-category'); const ratingEl = document.getElementById('movie-rating'); if (titleEl) titleEl.textContent = movie?.title || 'Player'; if (categoryEl) { const cat = movie?.category ? String(movie.category) : ''; categoryEl.textContent = cat ? `${cat} · ` : ''; } if (ratingEl) { const r = movie?.rating ? String(movie.rating) : ''; ratingEl.textContent = r ? `⭐ ${r}` : ''; } document.title = movie?.title ? `Player - ${movie.title}` : 'Player'; } function normalizeVodLink(raw) { const v = String(raw || '').trim(); if (!v) return null; // Support pasted iframe embed code (extract src) if (v.toLowerCase().includes(' { // If the browser can't play it (or CORS blocks), fall back to iframe. setSourceLabel('Embed (fallback)'); showEmbedPlayer(raw); }, { once: true }); try { videoEl.load(); } catch (_) {} return; } // HLS (.m3u8) if (looksLikeHls(raw)) { setSourceLabel('HLS stream (.m3u8)'); const videoEl = showVideoPlayer(); if (!videoEl) return; // Native HLS (Safari) first if (videoEl.canPlayType('application/vnd.apple.mpegurl')) { videoEl.src = raw; try { videoEl.load(); } catch (_) {} return; } // hls.js fallback (if supported) if (window.Hls && window.Hls.isSupported()) { const hls = new window.Hls({ enableWorker: true, }); hls.loadSource(raw); hls.attachMedia(videoEl); hls.on(window.Hls.Events.ERROR, function (_evt, data) { if (data?.fatal) { try { hls.destroy(); } catch (_) {} setSourceLabel('Embed (fallback)'); showEmbedPlayer(raw); } }); return; } // Last resort setSourceLabel('Embed (fallback)'); showEmbedPlayer(raw); return; } // Default: treat as embeddable URL setSourceLabel('Embed'); showEmbedPlayer(raw); } function resolveMovieSource(movie) { const mode = String(movie?.player_mode || 'auto').toLowerCase(); const legacy = movie?.vod_link; const video = movie?.video_src; const embed = movie?.embed_src; if (mode === 'video') return video || legacy || embed || null; if (mode === 'embed') return embed || legacy || video || null; // auto return video || embed || legacy || null; } window.onload = async function () { // Basic auth UX: require a valid local token (like the rest of the app). const token = localStorage.getItem('auth_token'); if (!token) { window.location.href = '/'; return; } try { const decoded = decodeJwt(token); const currentTime = Math.floor(Date.now() / 1000); if (decoded.exp && decoded.exp < currentTime) { localStorage.removeItem('auth_token'); window.location.href = '/'; return; } if (decoded.mustChangePassword) { window.location.href = '/change-password.html'; return; } const nameEl = document.getElementById('player-user-name'); if (nameEl) nameEl.textContent = decoded.name || ''; } catch (_) { localStorage.removeItem('auth_token'); window.location.href = '/'; return; } const id = getQueryParam('id'); if (!id) { showAlert('Missing movie id.'); return; } try { const res = await fetch(`/api/movies/${encodeURIComponent(id)}`, { method: 'GET' }); const movie = await res.json().catch(() => ({})); if (!res.ok) { showAlert(movie?.message || movie?.error || `Failed to load movie (${res.status}).`); return; } setHeader(movie); const src = resolveMovieSource(movie); await playVodLink(src); } catch (err) { showAlert(err.message || 'Network error.'); } };