From 89cfea0fe7bc430a5a574591000a96471e8eaf35 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:42:47 -0700 Subject: [PATCH] Add per-source quality fallback toggle for streaming downloads (#187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each streaming source (Tidal, Qobuz, HiFi, Deezer) now has an "Allow quality fallback" checkbox in Settings. When disabled, the source only tries the exact quality selected — if unavailable, it skips and lets the orchestrator try the next source. Default is ON (current behavior). --- core/deezer_download_client.py | 18 +++++++++++------- core/hifi_client.py | 3 ++- core/qobuz_client.py | 3 ++- core/tidal_download_client.py | 3 ++- webui/index.html | 28 ++++++++++++++++++++++++++++ webui/static/script.js | 14 +++++++++++--- 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/core/deezer_download_client.py b/core/deezer_download_client.py index 191ff042..d85d44b5 100644 --- a/core/deezer_download_client.py +++ b/core/deezer_download_client.py @@ -451,14 +451,18 @@ class DeezerDownloadClient: # Determine quality and get media URL with fallback media_url = None actual_quality = None - quality_order = _QUALITY_ORDER.copy() + allow_fallback = config_manager.get('deezer_download.allow_fallback', True) - # Start from user's preferred quality - try: - pref_idx = quality_order.index(self._quality) - quality_order = quality_order[pref_idx:] + quality_order[:pref_idx] - except ValueError: - pass + if allow_fallback: + quality_order = _QUALITY_ORDER.copy() + # Start from user's preferred quality + try: + pref_idx = quality_order.index(self._quality) + quality_order = quality_order[pref_idx:] + quality_order[:pref_idx] + except ValueError: + pass + else: + quality_order = [self._quality] for q in quality_order: url = self._get_media_url(track_token, q) diff --git a/core/hifi_client.py b/core/hifi_client.py index d9ddaccd..4eafa51f 100644 --- a/core/hifi_client.py +++ b/core/hifi_client.py @@ -568,7 +568,8 @@ class HiFiClient: quality_key = config_manager.get('hifi_download.quality', 'lossless') chain = ['hires', 'lossless', 'high', 'low'] start = chain.index(quality_key) if quality_key in chain else 1 - chain = chain[start:] + allow_fallback = config_manager.get('hifi_download.allow_fallback', True) + chain = chain[start:] if allow_fallback else [quality_key] MIN_AUDIO_SIZE = 100 * 1024 # 100KB diff --git a/core/qobuz_client.py b/core/qobuz_client.py index 8c696d51..f617f44e 100644 --- a/core/qobuz_client.py +++ b/core/qobuz_client.py @@ -894,7 +894,8 @@ class QobuzClient: # Quality fallback chain: hires_max → hires → lossless → mp3 quality_chain = ['hires_max', 'hires', 'lossless', 'mp3'] start_idx = quality_chain.index(quality_key) if quality_key in quality_chain else 2 - chain = quality_chain[start_idx:] + allow_fallback = config_manager.get('qobuz.allow_fallback', True) + chain = quality_chain[start_idx:] if allow_fallback else [quality_key] stream_data = None actual_quality = None diff --git a/core/tidal_download_client.py b/core/tidal_download_client.py index ce2b04c3..57a54929 100644 --- a/core/tidal_download_client.py +++ b/core/tidal_download_client.py @@ -443,7 +443,8 @@ class TidalDownloadClient: # files (stubs, empty HiRes responses) trigger a retry at the next tier. quality_chain = ['hires', 'lossless', 'high', 'low'] start_idx = quality_chain.index(quality_key) if quality_key in quality_chain else 1 - chain = quality_chain[start_idx:] + allow_fallback = config_manager.get('tidal_download.allow_fallback', True) + chain = quality_chain[start_idx:] if allow_fallback else [quality_key] MIN_AUDIO_SIZE = 100 * 1024 # 100KB diff --git a/webui/index.html b/webui/index.html index 8dea6c5d..c31de481 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4047,6 +4047,13 @@
Audio quality for Tidal downloads. HiRes requires a Tidal HiFi Plus subscription.
+ +
+ When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source. +
@@ -4074,6 +4081,13 @@
Audio quality for Qobuz downloads. Hi-Res requires a Qobuz Studio or Sublime subscription.
+ +
+ When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source. +
@@ -4116,6 +4130,13 @@
Audio quality for HiFi downloads. Uses public API instances — no account required.
+ +
+ When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source. +
@@ -4144,6 +4165,13 @@ Audio quality for Deezer downloads. FLAC requires a Deezer HiFi subscription. MP3 320 requires Premium or higher.
+ +
+ When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source. +
diff --git a/webui/static/script.js b/webui/static/script.js index cf860332..981ee05c 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -5549,9 +5549,13 @@ async function loadSettingsData() { document.getElementById('download-source-mode').value = settings.download_source?.mode || 'soulseek'; loadHybridSourceOrder(settings); document.getElementById('tidal-download-quality').value = settings.tidal_download?.quality || 'lossless'; + document.getElementById('tidal-allow-fallback').checked = settings.tidal_download?.allow_fallback !== false; document.getElementById('qobuz-quality').value = settings.qobuz?.quality || 'lossless'; + document.getElementById('qobuz-allow-fallback').checked = settings.qobuz?.allow_fallback !== false; document.getElementById('hifi-download-quality').value = settings.hifi_download?.quality || 'lossless'; + document.getElementById('hifi-allow-fallback').checked = settings.hifi_download?.allow_fallback !== false; document.getElementById('deezer-download-quality').value = settings.deezer_download?.quality || 'flac'; + document.getElementById('deezer-allow-fallback').checked = settings.deezer_download?.allow_fallback !== false; document.getElementById('deezer-download-arl').value = settings.deezer_download?.arl || ''; // Populate YouTube settings @@ -6549,19 +6553,23 @@ async function saveSettings(quiet = false) { hybrid_order: getHybridOrder(), }, tidal_download: { - quality: document.getElementById('tidal-download-quality').value || 'lossless' + quality: document.getElementById('tidal-download-quality').value || 'lossless', + allow_fallback: document.getElementById('tidal-allow-fallback').checked, }, hifi_download: { - quality: document.getElementById('hifi-download-quality').value || 'lossless' + quality: document.getElementById('hifi-download-quality').value || 'lossless', + allow_fallback: document.getElementById('hifi-allow-fallback').checked, }, deezer_download: { quality: document.getElementById('deezer-download-quality').value || 'flac', arl: document.getElementById('deezer-download-arl').value || '', + allow_fallback: document.getElementById('deezer-allow-fallback').checked, }, qobuz: { quality: document.getElementById('qobuz-quality').value || 'lossless', embed_tags: document.getElementById('embed-qobuz').checked, - tags: _collectServiceTags('qobuz') + tags: _collectServiceTags('qobuz'), + allow_fallback: document.getElementById('qobuz-allow-fallback').checked, }, database: { max_workers: parseInt(document.getElementById('max-workers').value)