feat(quality): derive per-source download tier from the global profile

Remove the per-source download-quality dropdowns (Tidal/HiFi/Qobuz/Deezer/
Amazon) — with the global ranked-targets system they were redundant and
conflicting. Add quality_tier_for_source(): picks the LOWEST source tier
that satisfies the user's top target (respects the quality ceiling, saves
bandwidth) or the source's max as best effort. Every source's search +
download + retry path now derives its tier from the global profile instead
of config_manager.get('<source>_download.quality').

Settings keep the per-source allow_fallback toggles; the quality selects are
replaced with a note pointing at Quality Profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-14 13:18:01 +02:00
parent 6046a814cb
commit fe78a3cdc3
11 changed files with 154 additions and 61 deletions

View file

@ -33,6 +33,7 @@ from core.amazon_client import AmazonClient, AmazonClientError
from core.download_plugins.base import DownloadSourcePlugin from core.download_plugins.base import DownloadSourcePlugin
from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult
from core.quality.model import AudioQuality from core.quality.model import AudioQuality
from core.quality.source_map import quality_tier_for_source
from utils.logging_config import get_logger from utils.logging_config import get_logger
logger = get_logger("amazon_download_client") logger = get_logger("amazon_download_client")
@ -79,7 +80,7 @@ class AmazonDownloadClient(DownloadSourcePlugin):
self.download_path = Path(download_path) self.download_path = Path(download_path)
self.download_path.mkdir(parents=True, exist_ok=True) self.download_path.mkdir(parents=True, exist_ok=True)
self._quality = config_manager.get("amazon_download.quality", "flac") self._quality = quality_tier_for_source("amazon", default="flac")
self._allow_fallback = config_manager.get("amazon_download.allow_fallback", True) self._allow_fallback = config_manager.get("amazon_download.allow_fallback", True)
self._client = AmazonClient(preferred_codec=self._quality) self._client = AmazonClient(preferred_codec=self._quality)

View file

@ -21,7 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple
import requests import requests
from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult
from core.quality.source_map import quality_from_deezer from core.quality.source_map import quality_from_deezer, quality_tier_for_source
from utils.logging_config import get_logger from utils.logging_config import get_logger
logger = get_logger("deezer_download") logger = get_logger("deezer_download")
@ -120,7 +120,7 @@ class DeezerDownloadClient(DownloadSourcePlugin):
self._authenticated = False self._authenticated = False
# Quality preference # Quality preference
self._quality = config_manager.get('deezer_download.quality', 'flac') self._quality = quality_tier_for_source('deezer', default='flac')
# Try to authenticate on init if ARL is configured # Try to authenticate on init if ARL is configured
arl = config_manager.get('deezer_download.arl', '') arl = config_manager.get('deezer_download.arl', '')

View file

@ -102,12 +102,14 @@ class DownloadOrchestrator:
deezer_dl = self.client('deezer_dl') deezer_dl = self.client('deezer_dl')
if deezer_arl and deezer_dl: if deezer_arl and deezer_dl:
deezer_dl.reconnect(deezer_arl) deezer_dl.reconnect(deezer_arl)
deezer_dl._quality = config_manager.get('deezer_download.quality', 'flac') from core.quality.source_map import quality_tier_for_source
deezer_dl._quality = quality_tier_for_source('deezer', default='flac')
# Reload Amazon quality preference (T2Tunes needs no reconnect — public proxy) # Reload Amazon quality preference (T2Tunes needs no reconnect — public proxy)
amazon = self.client('amazon') amazon = self.client('amazon')
if amazon: if amazon:
quality = config_manager.get('amazon_download.quality', 'flac') from core.quality.source_map import quality_tier_for_source
quality = quality_tier_for_source('amazon', default='flac')
amazon._quality = quality amazon._quality = quality
amazon._allow_fallback = config_manager.get('amazon_download.allow_fallback', True) amazon._allow_fallback = config_manager.get('amazon_download.allow_fallback', True)
if hasattr(amazon, '_client') and amazon._client: if hasattr(amazon, '_client') and amazon._client:

View file

@ -760,7 +760,8 @@ class WebUIDownloadMonitor:
# used_sources keys are formatted as "{username}_{filename}", so startswith is exact. # used_sources keys are formatted as "{username}_{filename}", so startswith is exact.
is_tidal = any(s.startswith('tidal_') for s in tried_sources) is_tidal = any(s.startswith('tidal_') for s in tried_sources)
if is_tidal: if is_tidal:
tidal_quality = config_manager.get('tidal_download.quality', 'lossless') from core.quality.source_map import quality_tier_for_source
tidal_quality = quality_tier_for_source('tidal', default='lossless')
allow_fb = config_manager.get('tidal_download.allow_fallback', True) allow_fb = config_manager.get('tidal_download.allow_fallback', True)
if tidal_quality == 'hires' and not allow_fb: if tidal_quality == 'hires' and not allow_fb:
task['error_message'] = ( task['error_message'] = (

View file

@ -36,7 +36,7 @@ import requests as http_requests
from utils.logging_config import get_logger from utils.logging_config import get_logger
from config.settings import config_manager from config.settings import config_manager
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
from core.quality.source_map import quality_from_tidal_tier from core.quality.source_map import quality_from_tidal_tier, quality_tier_for_source
logger = get_logger("hifi_client") logger = get_logger("hifi_client")
@ -752,7 +752,7 @@ class HiFiClient(DownloadSourcePlugin):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
tracks = await loop.run_in_executor(None, lambda: self.search_raw(query)) tracks = await loop.run_in_executor(None, lambda: self.search_raw(query))
quality_key = config_manager.get('hifi_download.quality', 'lossless') quality_key = quality_tier_for_source('hifi', default='lossless')
q_info = HLS_QUALITY_MAP.get(quality_key, HLS_QUALITY_MAP['lossless']) q_info = HLS_QUALITY_MAP.get(quality_key, HLS_QUALITY_MAP['lossless'])
# HiFi is Tidal-backed; stamp the configured tier so the global # HiFi is Tidal-backed; stamp the configured tier so the global
@ -834,7 +834,7 @@ class HiFiClient(DownloadSourcePlugin):
) )
def _download_sync(self, download_id: str, track_id: int, display_name: str) -> Optional[str]: def _download_sync(self, download_id: str, track_id: int, display_name: str) -> Optional[str]:
quality_key = config_manager.get('hifi_download.quality', 'lossless') quality_key = quality_tier_for_source('hifi', default='lossless')
chain = ['hires', 'lossless', 'high', 'low'] chain = ['hires', 'lossless', 'high', 'low']
start = chain.index(quality_key) if quality_key in chain else 1 start = chain.index(quality_key) if quality_key in chain else 1
allow_fallback = config_manager.get('hifi_download.allow_fallback', True) allow_fallback = config_manager.get('hifi_download.allow_fallback', True)

View file

@ -29,7 +29,7 @@ from config.settings import config_manager
# Import Soulseek data structures for drop-in replacement compatibility # Import Soulseek data structures for drop-in replacement compatibility
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
from core.quality.source_map import quality_from_qobuz from core.quality.source_map import quality_from_qobuz, quality_tier_for_source
logger = get_logger("qobuz_client") logger = get_logger("qobuz_client")
@ -988,7 +988,7 @@ class QobuzClient(DownloadSourcePlugin):
return ([], []) return ([], [])
# Get configured quality for display # Get configured quality for display
quality_key = config_manager.get('qobuz.quality', 'lossless') quality_key = quality_tier_for_source('qobuz', default='lossless')
quality_info = QOBUZ_QUALITY_MAP.get(quality_key, QOBUZ_QUALITY_MAP['lossless']) quality_info = QOBUZ_QUALITY_MAP.get(quality_key, QOBUZ_QUALITY_MAP['lossless'])
track_results = [] track_results = []
@ -1190,7 +1190,7 @@ class QobuzClient(DownloadSourcePlugin):
try: try:
# Determine quality # Determine quality
quality_key = config_manager.get('qobuz.quality', 'lossless') quality_key = quality_tier_for_source('qobuz', default='lossless')
quality_info = QOBUZ_QUALITY_MAP.get(quality_key, QOBUZ_QUALITY_MAP['lossless']) quality_info = QOBUZ_QUALITY_MAP.get(quality_key, QOBUZ_QUALITY_MAP['lossless'])
# Quality fallback chain: hires_max → hires → lossless → mp3 # Quality fallback chain: hires_max → hires → lossless → mp3

View file

@ -19,6 +19,7 @@ from __future__ import annotations
from typing import Optional from typing import Optional
from core.quality.model import AudioQuality from core.quality.model import AudioQuality
from core.quality.selection import load_profile_targets
# ── Tidal / HiFi (Monochrome is Tidal-backed) ────────────────────────────── # ── Tidal / HiFi (Monochrome is Tidal-backed) ──────────────────────────────
@ -102,3 +103,66 @@ def quality_from_amazon(
sample_rate=sample_rate if sample_rate is not None else base.sample_rate, sample_rate=sample_rate if sample_rate is not None else base.sample_rate,
bit_depth=bit_depth if bit_depth is not None else base.bit_depth, bit_depth=bit_depth if bit_depth is not None else base.bit_depth,
) )
# ── Profile-driven download tier (replaces per-source quality settings) ─────
#
# Each source's selectable download tiers, ordered best → worst, with the
# AudioQuality the tier delivers. ``quality_tier_for_source`` walks these to
# request the LOWEST tier that satisfies the user's top global target — so the
# global quality profile, not a per-source dropdown, decides what each source
# fetches.
_SOURCE_TIER_LADDERS: dict[str, list[tuple[str, AudioQuality]]] = {
'tidal': [
('hires', AudioQuality('flac', sample_rate=96000, bit_depth=24)),
('lossless', AudioQuality('flac', sample_rate=44100, bit_depth=16)),
('high', AudioQuality('aac', bitrate=320)),
('low', AudioQuality('aac', bitrate=96)),
],
'hifi': [
('hires', AudioQuality('flac', sample_rate=96000, bit_depth=24)),
('lossless', AudioQuality('flac', sample_rate=44100, bit_depth=16)),
('high', AudioQuality('aac', bitrate=320)),
('low', AudioQuality('aac', bitrate=96)),
],
'qobuz': [
('hires_max', AudioQuality('flac', sample_rate=192000, bit_depth=24)),
('hires', AudioQuality('flac', sample_rate=96000, bit_depth=24)),
('lossless', AudioQuality('flac', sample_rate=44100, bit_depth=16)),
('mp3', AudioQuality('mp3', bitrate=320)),
],
'deezer': [
('flac', AudioQuality('flac', sample_rate=44100, bit_depth=16)),
('mp3_320', AudioQuality('mp3', bitrate=320)),
('mp3_128', AudioQuality('mp3', bitrate=128)),
],
'amazon': [
('flac', AudioQuality('flac', sample_rate=48000, bit_depth=24)),
('opus', AudioQuality('aac', bitrate=320)),
],
}
def quality_tier_for_source(source_name: str, *, default: Optional[str] = None) -> Optional[str]:
"""Return the source tier key to request, derived from the global profile.
Picks the lowest tier in the source's ladder that satisfies the user's
top (most-preferred) target respecting the quality ceiling and saving
bandwidth. Falls back to the source's max tier when none can satisfy it
(best effort), or to the source's max when no targets are configured.
Returns *default* for an unknown source.
"""
ladder = _SOURCE_TIER_LADDERS.get(source_name)
if not ladder:
return default
targets, _ = load_profile_targets()
if not targets:
return ladder[0][0]
top = targets[0]
for key, aq in reversed(ladder): # low → high
if aq.matches_target(top):
return key
return ladder[0][0] # best effort: max tier

View file

@ -33,7 +33,7 @@ from config.settings import config_manager
# Import Soulseek data structures for drop-in replacement compatibility # Import Soulseek data structures for drop-in replacement compatibility
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
from core.quality.source_map import quality_from_tidal_tier from core.quality.source_map import quality_from_tidal_tier, quality_tier_for_source
logger = get_logger("tidal_download_client") logger = get_logger("tidal_download_client")
@ -456,7 +456,7 @@ class TidalDownloadClient(DownloadSourcePlugin):
if successful_query and successful_query != query: if successful_query and successful_query != query:
logger.info(f"Tidal fallback query succeeded: '{successful_query}' (original: '{query}')") logger.info(f"Tidal fallback query succeeded: '{successful_query}' (original: '{query}')")
quality_key = config_manager.get('tidal_download.quality', 'lossless') quality_key = quality_tier_for_source('tidal', default='lossless')
quality_info = QUALITY_MAP.get(quality_key, QUALITY_MAP['lossless']) quality_info = QUALITY_MAP.get(quality_key, QUALITY_MAP['lossless'])
# Stamp the configured tier (what will actually be downloaded) so # Stamp the configured tier (what will actually be downloaded) so
# the global ranker sees real sample_rate/bit_depth. # the global ranker sees real sample_rate/bit_depth.
@ -779,7 +779,7 @@ class TidalDownloadClient(DownloadSourcePlugin):
logger.error("Tidal session not authenticated") logger.error("Tidal session not authenticated")
return None return None
quality_key = config_manager.get('tidal_download.quality', 'lossless') quality_key = quality_tier_for_source('tidal', default='lossless')
chain = ['hires', 'lossless', 'high', 'low'] chain = ['hires', 'lossless', 'high', 'low']
start = chain.index(quality_key) if quality_key in chain else 1 start = chain.index(quality_key) if quality_key in chain else 1
allow_fallback = config_manager.get('tidal_download.allow_fallback', True) allow_fallback = config_manager.get('tidal_download.allow_fallback', True)

View file

@ -0,0 +1,62 @@
"""quality_tier_for_source — derive a source's requested download tier from
the GLOBAL quality profile instead of a per-source setting.
Rule: pick the LOWEST source tier that satisfies the user's top (most
preferred) target respecting the user's quality ceiling and saving
bandwidth or the source's max tier when none can satisfy it (best effort).
"""
import pytest
import core.quality.source_map as sm
from core.quality.model import QualityTarget
def _patch_targets(monkeypatch, targets, fallback=True):
monkeypatch.setattr(sm, 'load_profile_targets', lambda: (targets, fallback))
T_FLAC24_96 = [QualityTarget(label='', format='flac', bit_depth=24, min_sample_rate=96000)]
T_FLAC24_192 = [QualityTarget(label='', format='flac', bit_depth=24, min_sample_rate=192000)]
T_FLAC16 = [QualityTarget(label='', format='flac', bit_depth=16)]
T_MP3_320 = [QualityTarget(label='', format='mp3', min_bitrate=320)]
def test_tidal_hires_when_top_wants_24_96(monkeypatch):
_patch_targets(monkeypatch, T_FLAC24_96)
assert sm.quality_tier_for_source('tidal') == 'hires'
def test_tidal_lossless_respects_16bit_ceiling(monkeypatch):
# User caps at 16-bit → request lossless, NOT hires (saves bandwidth).
_patch_targets(monkeypatch, T_FLAC16)
assert sm.quality_tier_for_source('tidal') == 'lossless'
def test_tidal_best_effort_max_when_unsatisfiable(monkeypatch):
# Source maxes at 24/96 but user wants 24/192 → best effort = max tier.
_patch_targets(monkeypatch, T_FLAC24_192)
assert sm.quality_tier_for_source('tidal') == 'hires'
def test_no_targets_requests_max(monkeypatch):
_patch_targets(monkeypatch, [])
assert sm.quality_tier_for_source('tidal') == 'hires'
assert sm.quality_tier_for_source('deezer') == 'flac'
def test_deezer_flac_and_mp3(monkeypatch):
_patch_targets(monkeypatch, T_FLAC16)
assert sm.quality_tier_for_source('deezer') == 'flac'
_patch_targets(monkeypatch, T_MP3_320)
assert sm.quality_tier_for_source('deezer') == 'mp3_320'
def test_qobuz_hires_max(monkeypatch):
_patch_targets(monkeypatch, T_FLAC24_192)
assert sm.quality_tier_for_source('qobuz') == 'hires_max'
def test_unknown_source_returns_default(monkeypatch):
_patch_targets(monkeypatch, T_FLAC16)
assert sm.quality_tier_for_source('nope', default='x') == 'x'

View file

@ -4740,21 +4740,15 @@
<div id="tidal-download-settings-container" style="display: none;"> <div id="tidal-download-settings-container" style="display: none;">
<div class="form-group"> <div class="form-group">
<label>Tidal Download Quality:</label> <label>Tidal Download Quality:</label>
<select id="tidal-download-quality" class="form-select">
<option value="low">Low (AAC 96kbps)</option>
<option value="high">High (AAC 320kbps)</option>
<option value="lossless">Lossless (FLAC 16-bit/44.1kHz)</option>
<option value="hires">HiRes (FLAC 24-bit/96kHz)</option>
</select>
<div class="setting-help-text"> <div class="setting-help-text">
Audio quality for Tidal downloads. HiRes requires a Tidal HiFi Plus subscription. Quality is set globally in <strong>Quality Profile</strong> (ranked targets) — Tidal fetches the highest tier your profile accepts.
</div> </div>
<label class="checkbox-inline" style="margin-top: 8px;"> <label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="tidal-allow-fallback" checked> <input type="checkbox" id="tidal-allow-fallback" checked>
Allow quality fallback Allow quality fallback
</label> </label>
<div class="setting-help-text"> <div class="setting-help-text">
When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source. When disabled, only downloads at the exact derived quality. If unavailable, skips to the next source.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -4774,14 +4768,8 @@
<div id="qobuz-settings-container" style="display: none;"> <div id="qobuz-settings-container" style="display: none;">
<div class="form-group"> <div class="form-group">
<label>Qobuz Download Quality:</label> <label>Qobuz Download Quality:</label>
<select id="qobuz-quality" class="form-select">
<option value="mp3">MP3 320kbps</option>
<option value="lossless">Lossless (FLAC 16-bit/44.1kHz)</option>
<option value="hires">Hi-Res (FLAC 24-bit/96kHz)</option>
<option value="hires_max">Hi-Res Max (FLAC 24-bit/192kHz)</option>
</select>
<div class="setting-help-text"> <div class="setting-help-text">
Audio quality for Qobuz downloads. Hi-Res requires a Qobuz Studio or Sublime subscription. Quality is set globally in <strong>Quality Profile</strong> (ranked targets) — Qobuz fetches the highest tier your profile accepts. Hi-Res requires a Qobuz Studio or Sublime subscription.
</div> </div>
<label class="checkbox-inline" style="margin-top: 8px;"> <label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="qobuz-allow-fallback" checked> <input type="checkbox" id="qobuz-allow-fallback" checked>
@ -4840,14 +4828,8 @@
<div id="hifi-download-settings-container" style="display: none;"> <div id="hifi-download-settings-container" style="display: none;">
<div class="form-group"> <div class="form-group">
<label>HiFi Download Quality:</label> <label>HiFi Download Quality:</label>
<select id="hifi-download-quality" class="form-select">
<option value="low">Low (AAC 96kbps)</option>
<option value="high">High (AAC 320kbps)</option>
<option value="lossless">Lossless (FLAC 16-bit/44.1kHz)</option>
<option value="hires">HiRes (FLAC 24-bit/96kHz)</option>
</select>
<div class="setting-help-text"> <div class="setting-help-text">
Audio quality for HiFi downloads. Uses public API instances — no account required. Quality is set globally in <strong>Quality Profile</strong> (ranked targets) — HiFi fetches the highest tier your profile accepts. Uses public API instances — no account required.
</div> </div>
<label class="checkbox-inline" style="margin-top: 8px;"> <label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="hifi-allow-fallback" checked> <input type="checkbox" id="hifi-allow-fallback" checked>
@ -4888,14 +4870,8 @@
<div id="deezer-download-settings-container" style="display: none;"> <div id="deezer-download-settings-container" style="display: none;">
<div class="form-group"> <div class="form-group">
<label>Deezer Download Quality:</label> <label>Deezer Download Quality:</label>
<select id="deezer-download-quality" class="form-select">
<option value="mp3_128">MP3 128kbps (Free)</option>
<option value="mp3_320">MP3 320kbps (Premium)</option>
<option value="flac">FLAC Lossless (HiFi)</option>
</select>
<div class="setting-help-text"> <div class="setting-help-text">
Audio quality for Deezer downloads. FLAC requires a Deezer HiFi subscription. Quality is set globally in <strong>Quality Profile</strong> (ranked targets) — Deezer fetches the highest tier your profile accepts. FLAC requires a Deezer HiFi subscription.
MP3 320 requires Premium or higher.
</div> </div>
<label class="checkbox-inline" style="margin-top: 8px;"> <label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="deezer-allow-fallback" checked> <input type="checkbox" id="deezer-allow-fallback" checked>
@ -4930,14 +4906,8 @@
<div id="amazon-download-settings-container" style="display: none;"> <div id="amazon-download-settings-container" style="display: none;">
<div class="form-group"> <div class="form-group">
<label>Amazon Music Quality:</label> <label>Amazon Music Quality:</label>
<select id="amazon-quality" class="form-select">
<option value="flac">FLAC Lossless (24-bit/48kHz Hi-Res)</option>
<option value="opus">Opus (320kbps)</option>
<option value="eac3">EAC3 Dolby Atmos (768kbps 5.1)</option>
</select>
<div class="setting-help-text"> <div class="setting-help-text">
Preferred codec tier. FLAC is 24-bit/48kHz Hi-Res — no subscription required. Quality is set globally in <strong>Quality Profile</strong> (ranked targets) — Amazon fetches FLAC (24-bit/48kHz Hi-Res) when your profile wants lossless, otherwise a lossy tier. Downloads via T2Tunes proxy.
Downloads via T2Tunes proxy.
</div> </div>
<label class="checkbox-inline" style="margin-top: 8px;"> <label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="amazon-allow-fallback" checked> <input type="checkbox" id="amazon-allow-fallback" checked>

View file

@ -1138,17 +1138,14 @@ async function loadSettingsData() {
document.getElementById('max-concurrent-downloads').value = settings.download_source?.max_concurrent || '3'; document.getElementById('max-concurrent-downloads').value = settings.download_source?.max_concurrent || '3';
loadHybridSourceOrder(settings); loadHybridSourceOrder(settings);
loadArtSourceOrder(settings); loadArtSourceOrder(settings);
document.getElementById('tidal-download-quality').value = settings.tidal_download?.quality || 'lossless'; // Per-source download quality is now derived from the global Quality
// Profile (ranked targets) — the per-source quality selects were removed.
document.getElementById('tidal-allow-fallback').checked = settings.tidal_download?.allow_fallback !== false; 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('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('hifi-allow-fallback').checked = settings.hifi_download?.allow_fallback !== false;
loadHiFiInstances(); loadHiFiInstances();
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-allow-fallback').checked = settings.deezer_download?.allow_fallback !== false;
document.getElementById('deezer-download-arl').value = settings.deezer_download?.arl || ''; document.getElementById('deezer-download-arl').value = settings.deezer_download?.arl || '';
document.getElementById('amazon-quality').value = settings.amazon_download?.quality || 'flac';
document.getElementById('amazon-allow-fallback').checked = settings.amazon_download?.allow_fallback !== false; document.getElementById('amazon-allow-fallback').checked = settings.amazon_download?.allow_fallback !== false;
document.getElementById('lidarr-url').value = settings.lidarr_download?.url || ''; document.getElementById('lidarr-url').value = settings.lidarr_download?.url || '';
document.getElementById('lidarr-api-key').value = settings.lidarr_download?.api_key || ''; document.getElementById('lidarr-api-key').value = settings.lidarr_download?.api_key || '';
@ -2946,11 +2943,10 @@ async function saveSettings(quiet = false) {
usenet_download_path: document.getElementById('usenet-download-path')?.value || '', usenet_download_path: document.getElementById('usenet-download-path')?.value || '',
}, },
tidal_download: { tidal_download: {
quality: document.getElementById('tidal-download-quality').value || 'lossless', // quality derived from the global Quality Profile (ranked targets)
allow_fallback: document.getElementById('tidal-allow-fallback').checked, allow_fallback: document.getElementById('tidal-allow-fallback').checked,
}, },
hifi_download: { hifi_download: {
quality: document.getElementById('hifi-download-quality').value || 'lossless',
allow_fallback: document.getElementById('hifi-allow-fallback').checked, allow_fallback: document.getElementById('hifi-allow-fallback').checked,
}, },
hifi: { hifi: {
@ -2958,12 +2954,10 @@ async function saveSettings(quiet = false) {
tags: _collectServiceTags('hifi') tags: _collectServiceTags('hifi')
}, },
deezer_download: { deezer_download: {
quality: document.getElementById('deezer-download-quality').value || 'flac',
arl: document.getElementById('deezer-download-arl').value || '', arl: document.getElementById('deezer-download-arl').value || '',
allow_fallback: document.getElementById('deezer-allow-fallback').checked, allow_fallback: document.getElementById('deezer-allow-fallback').checked,
}, },
amazon_download: { amazon_download: {
quality: document.getElementById('amazon-quality').value || 'flac',
allow_fallback: document.getElementById('amazon-allow-fallback').checked, allow_fallback: document.getElementById('amazon-allow-fallback').checked,
}, },
lidarr_download: { lidarr_download: {
@ -2997,7 +2991,6 @@ async function saveSettings(quiet = false) {
// to migrate existing configs. // to migrate existing configs.
}, },
qobuz: { qobuz: {
quality: document.getElementById('qobuz-quality').value || 'lossless',
embed_tags: document.getElementById('embed-qobuz').checked, embed_tags: document.getElementById('embed-qobuz').checked,
tags: _collectServiceTags('qobuz'), tags: _collectServiceTags('qobuz'),
allow_fallback: document.getElementById('qobuz-allow-fallback').checked, allow_fallback: document.getElementById('qobuz-allow-fallback').checked,