preferred quality setting

This commit is contained in:
Simon
2026-06-17 15:44:20 +00:00
parent d73e413352
commit f8072884b2
7 changed files with 73 additions and 9 deletions

View File

@@ -551,6 +551,11 @@ def stream_video():
elif isinstance(info.get('http_headers'), dict): elif isinstance(info.get('http_headers'), dict):
upstream_headers = info['http_headers'] 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 referer_hint = None
if upstream_headers: if upstream_headers:
referer_hint = extract_referer(upstream_headers) referer_hint = extract_referer(upstream_headers)

View File

@@ -78,6 +78,18 @@
<option value="light">Light</option> <option value="light">Light</option>
</select> </select>
</div> </div>
<div class="setting-item">
<label>Preferred Quality</label>
<select id="quality-select">
<option value="auto">Best Available</option>
<option value="2160">2160p</option>
<option value="1440">1440p</option>
<option value="1080">1080p</option>
<option value="720">720p</option>
<option value="480">480p</option>
<option value="360">360p</option>
</select>
</div>
<div class="setting-item setting-toggle"> <div class="setting-item setting-toggle">
<div class="setting-label-row"> <div class="setting-label-row">
<label for="favorites-toggle">Favorites Bar</label> <label for="favorites-toggle">Favorites Bar</label>

View File

@@ -5,6 +5,7 @@ window.App = window.App || {};
async function initApp() { async function initApp() {
await App.storage.ensureDefaults(); await App.storage.ensureDefaults();
App.ui.applyTheme(); App.ui.applyTheme();
App.ui.applyPreferredQuality();
App.ui.renderMenu(); App.ui.renderMenu();
App.favorites.renderBar(); App.favorites.renderBar();
App.ui.bindGlobalHandlers(); App.ui.bindGlobalHandlers();

View File

@@ -19,5 +19,6 @@ App.state = {
// Local storage keys used across modules. // Local storage keys used across modules.
App.constants = { App.constants = {
FAVORITES_KEY: 'favorites', FAVORITES_KEY: 'favorites',
FAVORITES_VISIBILITY_KEY: 'favoritesVisible' FAVORITES_VISIBILITY_KEY: 'favoritesVisible',
PREFERRED_QUALITY_KEY: 'preferredQuality'
}; };

View File

@@ -3,7 +3,7 @@ App.storage = App.storage || {};
App.session = App.session || {}; App.session = App.session || {};
(function() { (function() {
const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY } = App.constants; const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY, PREFERRED_QUALITY_KEY } = App.constants;
// Basic localStorage helpers. // Basic localStorage helpers.
App.storage.getConfig = function() { App.storage.getConfig = function() {
@@ -30,6 +30,14 @@ App.session = App.session || {};
localStorage.setItem('preferences', JSON.stringify(nextPreferences)); 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() { App.storage.getServerEntries = function() {
const config = App.storage.getConfig(); const config = App.storage.getConfig();
if (!config.servers || !Array.isArray(config.servers)) return []; if (!config.servers || !Array.isArray(config.servers)) return [];
@@ -113,6 +121,9 @@ App.session = App.session || {};
if (!localStorage.getItem('theme')) { if (!localStorage.getItem('theme')) {
localStorage.setItem('theme', 'dark'); localStorage.setItem('theme', 'dark');
} }
if (!localStorage.getItem(PREFERRED_QUALITY_KEY)) {
localStorage.setItem(PREFERRED_QUALITY_KEY, '1080');
}
if (!localStorage.getItem(FAVORITES_KEY)) { if (!localStorage.getItem(FAVORITES_KEY)) {
localStorage.setItem(FAVORITES_KEY, JSON.stringify([])); localStorage.setItem(FAVORITES_KEY, JSON.stringify([]));
} }

View File

@@ -11,6 +11,11 @@ App.ui = App.ui || {};
if (select) select.value = theme; 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. // Toast helper for playback + network errors.
App.ui.showError = function(message) { App.ui.showError = function(message) {
const toast = document.getElementById('error-toast'); 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) { if (favoritesToggle) {
favoritesToggle.checked = App.favorites.isVisible(); favoritesToggle.checked = App.favorites.isVisible();
favoritesToggle.onchange = () => { favoritesToggle.onchange = () => {

View File

@@ -428,7 +428,7 @@ App.videos = App.videos || {};
return 0; return 0;
}; };
App.videos.pickBestFormat = function(formats) { App.videos.pickBestFormat = function(formats, preferredHeight) {
if (!Array.isArray(formats) || formats.length === 0) return null; if (!Array.isArray(formats) || formats.length === 0) return null;
const candidates = formats.filter((fmt) => fmt && fmt.url); const candidates = formats.filter((fmt) => fmt && fmt.url);
if (!candidates.length) return null; if (!candidates.length) return null;
@@ -439,7 +439,7 @@ App.videos = App.videos || {};
if (vcodec && vcodec !== 'none') return true; if (vcodec && vcodec !== 'none') return true;
return false; return false;
}); });
const pool = videoCandidates.length ? videoCandidates : candidates; let pool = videoCandidates.length ? videoCandidates : candidates;
const score = (fmt) => { const score = (fmt) => {
const height = App.videos.coerceNumber(fmt.height || fmt.quality); const height = App.videos.coerceNumber(fmt.height || fmt.quality);
const width = App.videos.coerceNumber(fmt.width); const width = App.videos.coerceNumber(fmt.width);
@@ -448,6 +448,22 @@ App.videos = App.videos || {};
const fps = App.videos.coerceNumber(fmt.fps); const fps = App.videos.coerceNumber(fmt.fps);
return [size, bitrate, 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) => { return pool.reduce((best, fmt) => {
if (!best) return fmt; if (!best) return fmt;
const bestScore = score(best); const bestScore = score(best);
@@ -460,7 +476,8 @@ App.videos = App.videos || {};
}, null); }, null);
}; };
App.videos.resolveStreamSource = function(videoOrUrl) { App.videos.resolveStreamSource = function(videoOrUrl, options) {
const applyPreferredQuality = !options || options.applyPreferredQuality !== false;
let sourceUrl = ''; let sourceUrl = '';
let referer = ''; let referer = '';
if (typeof videoOrUrl === 'string') { if (typeof videoOrUrl === 'string') {
@@ -468,7 +485,12 @@ App.videos = App.videos || {};
} else if (videoOrUrl && typeof videoOrUrl === 'object') { } else if (videoOrUrl && typeof videoOrUrl === 'object') {
const meta = videoOrUrl.meta || videoOrUrl; const meta = videoOrUrl.meta || videoOrUrl;
sourceUrl = meta.url || videoOrUrl.url || ''; 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) { if (best && best.url) {
sourceUrl = best.url; sourceUrl = best.url;
if (best.http_headers && (best.http_headers.Referer || best.http_headers.referer)) { 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. // Builds a proxied stream URL with an optional referer parameter.
App.videos.buildStreamUrl = function(videoOrUrl) { App.videos.buildStreamUrl = function(videoOrUrl, options) {
const resolved = App.videos.resolveStreamSource(videoOrUrl); const resolved = App.videos.resolveStreamSource(videoOrUrl, options);
if (!resolved.url) return ''; if (!resolved.url) return '';
const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : '';
return `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}`; return `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}`;
@@ -499,7 +521,7 @@ App.videos = App.videos || {};
App.videos.downloadVideo = function(video) { App.videos.downloadVideo = function(video) {
if (!video) return; if (!video) return;
const streamUrl = App.videos.buildStreamUrl(video); const streamUrl = App.videos.buildStreamUrl(video, { applyPreferredQuality: false });
if (!streamUrl) return; if (!streamUrl) return;
const link = document.createElement('a'); const link = document.createElement('a');
link.href = streamUrl; link.href = streamUrl;