honoring formats and http headers

This commit is contained in:
Simon
2026-02-09 19:16:44 +00:00
parent c2872c1883
commit d2e1e3adea
3 changed files with 106 additions and 22 deletions

View File

@@ -120,7 +120,7 @@ App.favorites = App.favorites || {};
if (App.videos && typeof App.videos.attachNoReferrerRetry === 'function') { if (App.videos && typeof App.videos.attachNoReferrerRetry === 'function') {
App.videos.attachNoReferrerRetry(thumb); 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'); const favoriteBtn = card.querySelector('.favorite-btn');
if (favoriteBtn) { if (favoriteBtn) {
favoriteBtn.onclick = (event) => { favoriteBtn.onclick = (event) => {

View File

@@ -29,7 +29,7 @@ App.player = App.player || {};
return host; return host;
} }
App.player.open = async function(url) { App.player.open = async function(source) {
const modal = document.getElementById('video-modal'); const modal = document.getElementById('video-modal');
const video = document.getElementById('player'); const video = document.getElementById('player');
if (!modal || !video) return; if (!modal || !video) return;
@@ -41,15 +41,30 @@ App.player = App.player || {};
} }
// Normalize stream URL + optional referer forwarding. // Normalize stream URL + optional referer forwarding.
let refererParam = ''; let resolved = { url: '', referer: '' };
try { if (App.videos && typeof App.videos.resolveStreamSource === 'function') {
const origin = new URL(url).origin; resolved = App.videos.resolveStreamSource(source);
refererParam = `&referer=${encodeURIComponent(origin + '/')}`; } else if (typeof source === 'string') {
} catch (err) { resolved.url = source;
refererParam = ''; } else if (source && typeof source === 'object') {
resolved.url = source.url || '';
} }
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}${refererParam}`; if (!resolved.referer && resolved.url) {
let isHls = /\.m3u8($|\?)/i.test(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. // Cleanup existing player instance to prevent aborted bindings.
if (state.hlsPlayer) { if (state.hlsPlayer) {

View File

@@ -180,7 +180,7 @@ App.videos = App.videos || {};
App.videos.closeAllMenus(); App.videos.closeAllMenus();
}; };
} }
card.onclick = () => App.player.open(v.url); card.onclick = () => App.player.open(v);
grid.appendChild(card); grid.appendChild(card);
state.renderedVideoIds.add(v.id); 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.coerceNumber = function(value) {
App.videos.buildStreamUrl = function(videoUrl) { if (value === null || value === undefined) return 0;
let refererParam = ''; if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
try { if (typeof value === 'string') {
const origin = new URL(videoUrl).origin; const parsed = parseFloat(value);
refererParam = `&referer=${encodeURIComponent(origin + '/')}`; return Number.isFinite(parsed) ? parsed : 0;
} catch (err) {
refererParam = '';
} }
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) { 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'); const link = document.createElement('a');
link.href = App.videos.buildStreamUrl(video.url); link.href = streamUrl;
const rawName = (video.title || video.id || 'video').toString(); const rawName = (video.title || video.id || 'video').toString();
const safeName = rawName.replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80); const safeName = rawName.replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80);
link.download = safeName ? `${safeName}.mp4` : 'video.mp4'; link.download = safeName ? `${safeName}.mp4` : 'video.mp4';