diff --git a/public/js/app_player.js b/public/js/app_player.js new file mode 100644 index 0000000..a62517a --- /dev/null +++ b/public/js/app_player.js @@ -0,0 +1,277 @@ +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); +} + +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); + await playVodLink(movie?.vod_link); + } catch (err) { + showAlert(err.message || 'Network error.'); + } +}; + diff --git a/public/player.html b/public/player.html new file mode 100644 index 0000000..2c0dbf3 --- /dev/null +++ b/public/player.html @@ -0,0 +1,91 @@ + + + + + + Player + + + + + + + +
+
+

Loading…

+
+ + +
+
+ + + +
+ + +
+ +
+ Source + — +
+
+ + + + + + +