diff --git a/frontend/js/favorites.js b/frontend/js/favorites.js index 9ee11b5..1426d9a 100644 --- a/frontend/js/favorites.js +++ b/frontend/js/favorites.js @@ -120,7 +120,7 @@ App.favorites = App.favorites || {}; if (App.videos && typeof App.videos.attachNoReferrerRetry === 'function') { App.videos.attachNoReferrerRetry(thumb); } - card.onclick = () => App.player.open(item.url); + card.onclick = () => App.player.open(item.meta || item); const favoriteBtn = card.querySelector('.favorite-btn'); if (favoriteBtn) { favoriteBtn.onclick = (event) => { diff --git a/frontend/js/player.js b/frontend/js/player.js index b7d9dce..93926f9 100644 --- a/frontend/js/player.js +++ b/frontend/js/player.js @@ -29,7 +29,7 @@ App.player = App.player || {}; return host; } - App.player.open = async function(url) { + App.player.open = async function(source) { const modal = document.getElementById('video-modal'); const video = document.getElementById('player'); if (!modal || !video) return; @@ -41,15 +41,30 @@ App.player = App.player || {}; } // Normalize stream URL + optional referer forwarding. - let refererParam = ''; - try { - const origin = new URL(url).origin; - refererParam = `&referer=${encodeURIComponent(origin + '/')}`; - } catch (err) { - refererParam = ''; + let resolved = { url: '', referer: '' }; + if (App.videos && typeof App.videos.resolveStreamSource === 'function') { + resolved = App.videos.resolveStreamSource(source); + } else if (typeof source === 'string') { + resolved.url = source; + } else if (source && typeof source === 'object') { + resolved.url = source.url || ''; } - const streamUrl = `/api/stream?url=${encodeURIComponent(url)}${refererParam}`; - let isHls = /\.m3u8($|\?)/i.test(url); + if (!resolved.referer && resolved.url) { + try { + resolved.referer = `${new URL(resolved.url).origin}/`; + } catch (err) { + resolved.referer = ''; + } + } + if (!resolved.url) { + if (App.ui && App.ui.showError) { + App.ui.showError('Unable to play this stream.'); + } + return; + } + const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; + const streamUrl = `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}`; + let isHls = /\.m3u8($|\?)/i.test(resolved.url); // Cleanup existing player instance to prevent aborted bindings. if (state.hlsPlayer) { diff --git a/frontend/js/videos.js b/frontend/js/videos.js index 69b48f9..a8508d4 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -180,7 +180,7 @@ App.videos = App.videos || {}; App.videos.closeAllMenus(); }; } - card.onclick = () => App.player.open(v.url); + card.onclick = () => App.player.open(v); grid.appendChild(card); state.renderedVideoIds.add(v.id); }); @@ -257,22 +257,91 @@ App.videos = App.videos || {}; } }; - // Builds a proxied stream URL with an optional referer parameter. - App.videos.buildStreamUrl = function(videoUrl) { - let refererParam = ''; - try { - const origin = new URL(videoUrl).origin; - refererParam = `&referer=${encodeURIComponent(origin + '/')}`; - } catch (err) { - refererParam = ''; + App.videos.coerceNumber = function(value) { + if (value === null || value === undefined) return 0; + if (typeof value === 'number') return Number.isFinite(value) ? value : 0; + if (typeof value === 'string') { + const parsed = parseFloat(value); + return Number.isFinite(parsed) ? parsed : 0; } - return `/api/stream?url=${encodeURIComponent(videoUrl)}${refererParam}`; + return 0; + }; + + App.videos.pickBestFormat = function(formats) { + if (!Array.isArray(formats) || formats.length === 0) return null; + const candidates = formats.filter((fmt) => fmt && fmt.url); + if (!candidates.length) return null; + const videoCandidates = candidates.filter((fmt) => { + const videoExt = String(fmt.video_ext || '').toLowerCase(); + const vcodec = String(fmt.vcodec || '').toLowerCase(); + if (videoExt && videoExt !== 'none') return true; + if (vcodec && vcodec !== 'none') return true; + return false; + }); + const pool = videoCandidates.length ? videoCandidates : candidates; + const score = (fmt) => { + const height = App.videos.coerceNumber(fmt.height || fmt.quality); + const width = App.videos.coerceNumber(fmt.width); + const size = height || width; + const bitrate = App.videos.coerceNumber(fmt.tbr || fmt.bitrate); + const fps = App.videos.coerceNumber(fmt.fps); + return [size, bitrate, fps]; + }; + return pool.reduce((best, fmt) => { + if (!best) return fmt; + const bestScore = score(best); + const curScore = score(fmt); + for (let i = 0; i < curScore.length; i++) { + if (curScore[i] > bestScore[i]) return fmt; + if (curScore[i] < bestScore[i]) return best; + } + return best; + }, null); + }; + + App.videos.resolveStreamSource = function(videoOrUrl) { + let sourceUrl = ''; + let referer = ''; + if (typeof videoOrUrl === 'string') { + sourceUrl = videoOrUrl; + } else if (videoOrUrl && typeof videoOrUrl === 'object') { + const meta = videoOrUrl.meta || videoOrUrl; + sourceUrl = meta.url || videoOrUrl.url || ''; + const best = App.videos.pickBestFormat(meta.formats); + if (best && best.url) { + sourceUrl = best.url; + if (best.http_headers && (best.http_headers.Referer || best.http_headers.referer)) { + referer = best.http_headers.Referer || best.http_headers.referer; + } + } + if (!referer && meta.http_headers && (meta.http_headers.Referer || meta.http_headers.referer)) { + referer = meta.http_headers.Referer || meta.http_headers.referer; + } + } + if (!referer && sourceUrl) { + try { + referer = `${new URL(sourceUrl).origin}/`; + } catch (err) { + referer = ''; + } + } + return { url: sourceUrl, referer }; + }; + + // Builds a proxied stream URL with an optional referer parameter. + App.videos.buildStreamUrl = function(videoOrUrl) { + const resolved = App.videos.resolveStreamSource(videoOrUrl); + if (!resolved.url) return ''; + const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; + return `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}`; }; App.videos.downloadVideo = function(video) { - if (!video || !video.url) return; + if (!video) return; + const streamUrl = App.videos.buildStreamUrl(video); + if (!streamUrl) return; const link = document.createElement('a'); - link.href = App.videos.buildStreamUrl(video.url); + link.href = streamUrl; const rawName = (video.title || video.id || 'video').toString(); const safeName = rawName.replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80); link.download = safeName ? `${safeName}.mp4` : 'video.mp4';