From f8072884b204ede2a764a2f5def2b3b75d7be2cc Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 17 Jun 2026 15:44:20 +0000 Subject: [PATCH] preferred quality setting --- backend/main.py | 5 +++++ frontend/index.html | 12 ++++++++++++ frontend/js/main.js | 1 + frontend/js/state.js | 3 ++- frontend/js/storage.js | 13 ++++++++++++- frontend/js/ui.js | 12 ++++++++++++ frontend/js/videos.js | 36 +++++++++++++++++++++++++++++------- 7 files changed, 73 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index f1e67f0..1a77dea 100644 --- a/backend/main.py +++ b/backend/main.py @@ -551,6 +551,11 @@ def stream_video(): elif isinstance(info.get('http_headers'), dict): upstream_headers = info['http_headers'] + if request.method == 'HEAD' and selected_format: + manifest_url = selected_format.get('manifest_url') + if manifest_url: + return Response("", status=301, headers=[('Location', f"/api/stream?url={manifest_url}")]) + referer_hint = None if upstream_headers: referer_hint = extract_referer(upstream_headers) diff --git a/frontend/index.html b/frontend/index.html index aa7e852..5735bdf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -78,6 +78,18 @@ +
+ + +
diff --git a/frontend/js/main.js b/frontend/js/main.js index 49891de..4355d88 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -5,6 +5,7 @@ window.App = window.App || {}; async function initApp() { await App.storage.ensureDefaults(); App.ui.applyTheme(); + App.ui.applyPreferredQuality(); App.ui.renderMenu(); App.favorites.renderBar(); App.ui.bindGlobalHandlers(); diff --git a/frontend/js/state.js b/frontend/js/state.js index 7e52eb2..606dbae 100644 --- a/frontend/js/state.js +++ b/frontend/js/state.js @@ -19,5 +19,6 @@ App.state = { // Local storage keys used across modules. App.constants = { FAVORITES_KEY: 'favorites', - FAVORITES_VISIBILITY_KEY: 'favoritesVisible' + FAVORITES_VISIBILITY_KEY: 'favoritesVisible', + PREFERRED_QUALITY_KEY: 'preferredQuality' }; diff --git a/frontend/js/storage.js b/frontend/js/storage.js index 4aa4f4d..1da8b1c 100644 --- a/frontend/js/storage.js +++ b/frontend/js/storage.js @@ -3,7 +3,7 @@ App.storage = App.storage || {}; App.session = App.session || {}; (function() { - const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY } = App.constants; + const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY, PREFERRED_QUALITY_KEY } = App.constants; // Basic localStorage helpers. App.storage.getConfig = function() { @@ -30,6 +30,14 @@ App.session = App.session || {}; localStorage.setItem('preferences', JSON.stringify(nextPreferences)); }; + App.storage.getPreferredQuality = function() { + return localStorage.getItem(PREFERRED_QUALITY_KEY) || '1080'; + }; + + App.storage.setPreferredQuality = function(nextQuality) { + localStorage.setItem(PREFERRED_QUALITY_KEY, nextQuality); + }; + App.storage.getServerEntries = function() { const config = App.storage.getConfig(); if (!config.servers || !Array.isArray(config.servers)) return []; @@ -113,6 +121,9 @@ App.session = App.session || {}; if (!localStorage.getItem('theme')) { localStorage.setItem('theme', 'dark'); } + if (!localStorage.getItem(PREFERRED_QUALITY_KEY)) { + localStorage.setItem(PREFERRED_QUALITY_KEY, '1080'); + } if (!localStorage.getItem(FAVORITES_KEY)) { localStorage.setItem(FAVORITES_KEY, JSON.stringify([])); } diff --git a/frontend/js/ui.js b/frontend/js/ui.js index 0761ce4..a0e73ce 100644 --- a/frontend/js/ui.js +++ b/frontend/js/ui.js @@ -11,6 +11,11 @@ App.ui = App.ui || {}; if (select) select.value = theme; }; + App.ui.applyPreferredQuality = function() { + const select = document.getElementById('quality-select'); + if (select) select.value = App.storage.getPreferredQuality(); + }; + // Toast helper for playback + network errors. App.ui.showError = function(message) { const toast = document.getElementById('error-toast'); @@ -234,6 +239,13 @@ App.ui = App.ui || {}; }; } + const qualitySelect = document.getElementById('quality-select'); + if (qualitySelect) { + qualitySelect.onchange = () => { + App.storage.setPreferredQuality(qualitySelect.value); + }; + } + if (favoritesToggle) { favoritesToggle.checked = App.favorites.isVisible(); favoritesToggle.onchange = () => { diff --git a/frontend/js/videos.js b/frontend/js/videos.js index 19a090f..be259ff 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -428,7 +428,7 @@ App.videos = App.videos || {}; return 0; }; - App.videos.pickBestFormat = function(formats) { + App.videos.pickBestFormat = function(formats, preferredHeight) { if (!Array.isArray(formats) || formats.length === 0) return null; const candidates = formats.filter((fmt) => fmt && fmt.url); if (!candidates.length) return null; @@ -439,7 +439,7 @@ App.videos = App.videos || {}; if (vcodec && vcodec !== 'none') return true; return false; }); - const pool = videoCandidates.length ? videoCandidates : candidates; + let pool = videoCandidates.length ? videoCandidates : candidates; const score = (fmt) => { const height = App.videos.coerceNumber(fmt.height || fmt.quality); const width = App.videos.coerceNumber(fmt.width); @@ -448,6 +448,22 @@ App.videos = App.videos || {}; const fps = App.videos.coerceNumber(fmt.fps); return [size, bitrate, fps]; }; + if (preferredHeight) { + const atOrBelow = pool.filter((fmt) => { + const size = score(fmt)[0]; + return size > 0 && size <= preferredHeight; + }); + if (atOrBelow.length) { + pool = atOrBelow; + } else { + // Nothing at or below the preferred quality, fall back to the lowest available. + const lowest = pool.reduce((min, fmt) => { + if (!min) return fmt; + return score(fmt)[0] < score(min)[0] ? fmt : min; + }, null); + return lowest; + } + } return pool.reduce((best, fmt) => { if (!best) return fmt; const bestScore = score(best); @@ -460,7 +476,8 @@ App.videos = App.videos || {}; }, null); }; - App.videos.resolveStreamSource = function(videoOrUrl) { + App.videos.resolveStreamSource = function(videoOrUrl, options) { + const applyPreferredQuality = !options || options.applyPreferredQuality !== false; let sourceUrl = ''; let referer = ''; if (typeof videoOrUrl === 'string') { @@ -468,7 +485,12 @@ App.videos = App.videos || {}; } else if (videoOrUrl && typeof videoOrUrl === 'object') { const meta = videoOrUrl.meta || videoOrUrl; sourceUrl = meta.url || videoOrUrl.url || ''; - const best = App.videos.pickBestFormat(meta.formats); + let preferredHeight = null; + if (applyPreferredQuality) { + const preferredQuality = App.storage.getPreferredQuality(); + preferredHeight = preferredQuality === 'auto' ? null : App.videos.coerceNumber(preferredQuality); + } + const best = App.videos.pickBestFormat(meta.formats, preferredHeight); if (best && best.url) { sourceUrl = best.url; if (best.http_headers && (best.http_headers.Referer || best.http_headers.referer)) { @@ -490,8 +512,8 @@ App.videos = App.videos || {}; }; // Builds a proxied stream URL with an optional referer parameter. - App.videos.buildStreamUrl = function(videoOrUrl) { - const resolved = App.videos.resolveStreamSource(videoOrUrl); + App.videos.buildStreamUrl = function(videoOrUrl, options) { + const resolved = App.videos.resolveStreamSource(videoOrUrl, options); if (!resolved.url) return ''; const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; return `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}`; @@ -499,7 +521,7 @@ App.videos = App.videos || {}; App.videos.downloadVideo = function(video) { if (!video) return; - const streamUrl = App.videos.buildStreamUrl(video); + const streamUrl = App.videos.buildStreamUrl(video, { applyPreferredQuality: false }); if (!streamUrl) return; const link = document.createElement('a'); link.href = streamUrl;