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.
+
@@ -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)