feat(quality): make every audio format controllable via ranked targets

The ranked-target list is now the single source of truth for which formats
download, in the user's exact priority order, for ALL sources — no hardcoded
format hierarchy decides anything. A candidate passes only if it matches a
ranked target; if nothing matches, the existing Use-Fallback toggle decides.

- source_map: new shared format_from_extension() + AUDIO_EXTENSIONS — one
  source of truth for extension→format used by every extension-based source, so
  adding a format lights it up everywhere. Soulseek now classifies through it
  (opus/wav/aiff were previously dropped as 'unknown').
- file_ops.probe_audio_quality (generic import-time guard, all sources): add
  WMA; detect ALAC from the real codec (an .m4a is AAC or ALAC).
- soulseek: drop the AAC-specific opt-in gate — AAC now follows the same
  universal rule as every format.
- model.tier_score: documented as ONLY a same-format tiebreak + fallback order,
  never cross-format priority (the list owns that); add opus/alac bases.
- UI: ranked-target editor offers all formats (FLAC/ALAC/WAV·AIFF lossless with
  bit-depth+sample-rate; MP3/AAC/OGG/Opus/WMA lossy with min-bitrate).
- tests: AAC retargeted to the universal model; new coverage for
  format_from_extension and matches_target across all formats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-24 23:01:20 +02:00
parent 17137cea5b
commit 774d0f6c65
9 changed files with 177 additions and 62 deletions

View file

@ -261,6 +261,17 @@ def probe_audio_quality(file_path: str):
if ext in ('m4a', 'aac', 'mp4'):
from mutagen.mp4 import MP4
audio = MP4(file_path)
# .m4a can carry AAC (lossy) OR ALAC (lossless) — only the real
# codec tells them apart, which is why extension-based classification
# defaults to 'aac' and we correct it here from the probed file.
codec = (getattr(audio.info, 'codec', '') or '').lower()
if 'alac' in codec:
return AudioQuality(
format='alac',
bitrate=audio.info.bitrate // 1000 if audio.info.bitrate else None,
sample_rate=audio.info.sample_rate,
bit_depth=getattr(audio.info, 'bits_per_sample', None) or None,
)
return AudioQuality(
format='aac',
bitrate=audio.info.bitrate // 1000,
@ -302,6 +313,15 @@ def probe_audio_quality(file_path: str):
bit_depth=getattr(audio.info, 'bits_per_sample', None),
)
if ext == 'wma':
from mutagen.asf import ASF
audio = ASF(file_path)
return AudioQuality(
format='wma',
bitrate=audio.info.bitrate // 1000 if audio.info.bitrate else None,
sample_rate=getattr(audio.info, 'sample_rate', None),
)
return None
except Exception as e:
logger.debug("probe_audio_quality failed for %s: %s", file_path, e)

View file

@ -34,10 +34,16 @@ class AudioQuality:
"""Continuous score for ranking within a matched target bucket.
Higher = better. Used as a tiebreaker after target-list matching.
"""
# NOTE: this only orders the *fallback* path (nothing matched a ranked
# target) and tie-breaks candidates of the SAME format within one
# matched target. Cross-format PRIORITY is decided solely by the user's
# ranked-target list (target index), never by these numbers.
format_base: dict[str, float] = {
'flac': 100.0,
'alac': 98.0, # lossless (Apple)
'wav': 95.0,
'ogg': 70.0,
'opus': 65.0,
'aac': 60.0,
'mp3': 50.0,
'wma': 30.0,

View file

@ -22,6 +22,41 @@ from core.quality.model import AudioQuality
from core.quality.selection import load_profile_targets
# ── Extension → format string (source-agnostic) ────────────────────────────
#
# The single source of truth for mapping a file extension to the unified
# AudioQuality ``format``. Every extension-based download source (Soulseek,
# torrent/usenet file lists, …) classifies through this, so the ranked-target
# system behaves identically across sources and adding a format here lights it
# up everywhere at once. Unknown extensions → 'unknown' (never matches a
# target, so it only ever comes through via the fallback toggle).
#
# AIFF/AIF are uncompressed PCM like WAV → the same 'wav' tier. ``.m4a``
# defaults to 'aac'; an ALAC-in-m4a file can't be told apart by extension
# alone, so probe_audio_quality corrects it from the real codec post-download.
_EXTENSION_FORMAT_MAP = {
'flac': 'flac',
'alac': 'alac',
'wav': 'wav', 'wave': 'wav',
'aiff': 'wav', 'aif': 'wav', 'aifc': 'wav',
'mp3': 'mp3',
'm4a': 'aac', 'mp4': 'aac', 'aac': 'aac',
'ogg': 'ogg', 'oga': 'ogg',
'opus': 'opus',
'wma': 'wma',
}
# Audio extensions worth probing/classifying at all — derived from the map so
# the allow-list and the classifier never drift apart.
AUDIO_EXTENSIONS = {f'.{e}' for e in _EXTENSION_FORMAT_MAP}
def format_from_extension(ext: str) -> str:
"""Map a file extension (with or without leading dot) to the unified
AudioQuality format string. Unknown 'unknown'."""
return _EXTENSION_FORMAT_MAP.get(str(ext or '').lower().lstrip('.'), 'unknown')
# ── Tidal / HiFi (Monochrome is Tidal-backed) ──────────────────────────────
#
# Tidal exposes UPPER_SNAKE tier strings (``HI_RES_LOSSLESS``); HiFi's config

View file

@ -25,6 +25,7 @@ from core.download_plugins.album_bundle import (
)
from core.download_plugins.base import DownloadSourcePlugin
from core.quality.model import QualityTarget, filter_and_rank, v2_qualities_to_ranked_targets
from core.quality.source_map import AUDIO_EXTENSIONS, format_from_extension
from utils.async_helpers import run_async
logger = get_logger("soulseek_client")
@ -410,7 +411,7 @@ class SoulseekClient(DownloadSourcePlugin):
# Audio file extensions to filter for
audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'}
audio_extensions = AUDIO_EXTENSIONS
for response_data in responses_data:
username = response_data.get('username', '')
@ -427,10 +428,9 @@ class SoulseekClient(DownloadSourcePlugin):
if f'.{file_ext}' not in audio_extensions:
continue
# .m4a is the usual AAC container — bucket it as 'aac' (the
# quality filter treats AAC as an opt-in tier; off by default).
quality = 'aac' if file_ext == 'm4a' else (
file_ext if file_ext in ['flac', 'mp3', 'ogg', 'aac', 'wma'] else 'unknown')
# Source-agnostic extension → format (shared with every other
# extension-based source). Ranked targets do the rest.
quality = format_from_extension(file_ext)
# Create TrackResult
# Convert duration from seconds to milliseconds (slskd returns seconds, Spotify uses ms)
@ -1140,7 +1140,7 @@ class SoulseekClient(DownloadSourcePlugin):
Returns:
List of TrackResult objects for audio files
"""
audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'}
audio_extensions = AUDIO_EXTENSIONS
results = []
if files:
logger.debug(f"Browse raw file sample: {files[0]}")
@ -1154,9 +1154,7 @@ class SoulseekClient(DownloadSourcePlugin):
ext = Path(filename).suffix.lower()
if ext not in audio_extensions:
continue
_qext = ext.lstrip('.')
quality = 'aac' if _qext == 'm4a' else (
_qext if _qext in ['flac', 'mp3', 'ogg', 'aac', 'wma'] else 'unknown')
quality = format_from_extension(ext)
raw_duration = file_data.get('length')
duration_ms = raw_duration * 1000 if raw_duration else None
slskd_attrs = {a['type']: a['value'] for a in file_data.get('attributes', [])}
@ -2048,15 +2046,9 @@ class SoulseekClient(DownloadSourcePlugin):
targets = [QualityTarget.from_dict(t) for t in (raw_targets or [])]
fallback_enabled = profile.get('fallback_enabled', True)
# AAC (#886) is an opt-in tier: keep AAC/.m4a candidates ONLY when the
# active profile explicitly targets AAC. Otherwise drop them BEFORE
# ranking — without this, fallback_enabled would hand back AAC files the
# user never asked for (pre-#886 such files landed in the dropped
# 'other' bucket and were never returned).
if not any((t.format or '').lower() == 'aac' for t in targets):
results = [r for r in results if (r.audio_quality.format or '').lower() != 'aac']
if not results:
return []
# Every format (AAC included) follows the SAME universal rule: a
# candidate passes only if it matches a ranked target; if nothing
# matches, the fallback toggle decides. No per-format special-casing.
logger.debug(
"Quality Filter: profile='%s', %d targets, %d candidates",

View file

@ -38,14 +38,14 @@ def _cand(quality, size_mb, bitrate=None):
queue_length=0, artist='A', title='Song', album='B', track_number=1)
def _q(enabled_flac=True, enabled_mp3=True, aac=None):
def _q(enabled_flac=True, enabled_mp3=True, aac=None, fallback=True):
qualities = {
'flac': {'enabled': enabled_flac, 'min_kbps': 500, 'max_kbps': 10000, 'priority': 1, 'bit_depth': 'any'},
'mp3_320': {'enabled': enabled_mp3, 'min_kbps': 280, 'max_kbps': 500, 'priority': 2},
}
if aac is not None: # None => omit the tier entirely (pre-existing profile)
if aac is not None: # None => omit the tier entirely
qualities['aac'] = {'enabled': aac, 'min_kbps': 128, 'max_kbps': 400, 'priority': 1.5}
return {'preset': 'custom', 'qualities': qualities, 'fallback_enabled': True}
return {'preset': 'custom', 'qualities': qualities, 'fallback_enabled': fallback}
def _filter(candidates, profile):
@ -56,51 +56,44 @@ def _filter(candidates, profile):
return c.filter_results_by_quality_preference(candidates)
# ── additive proof: AAC off == today (dropped) ────────────────────────────────
def test_aac_dropped_when_tier_absent_pre_existing_profile():
# A profile saved before this feature has no 'aac' key at all.
out = _filter([_cand('aac', 5)], _q(aac=None))
assert out == [] # AAC went to 'other' -> never returned, exactly as before
# ── AAC follows the UNIVERSAL rule (no per-format special-casing) ──────────────
# AAC is just another format: it passes only if it matches a ranked target;
# when nothing matches, the fallback toggle decides — exactly like every format.
def test_aac_not_targeted_fallback_off_is_dropped():
out = _filter([_cand('aac', 5)], _q(aac=None, fallback=False))
assert out == [] # no AAC target + fallback off → nothing comes through
def test_aac_dropped_when_tier_present_but_disabled():
out = _filter([_cand('aac', 5)], _q(aac=False))
assert out == []
def test_aac_not_targeted_fallback_on_comes_through():
out = _filter([_cand('aac', 5)], _q(aac=None, fallback=True))
assert len(out) == 1 and out[0].quality == 'aac' # fallback grabs it
def test_aac_disabled_tier_behaves_like_not_targeted():
# An explicitly-disabled aac tier is not a target → same as absent.
assert _filter([_cand('aac', 5)], _q(aac=False, fallback=False)) == []
def test_flac_mp3_selection_unchanged_when_aac_absent():
# The headline no-regression guard: a normal FLAC/MP3 mix is unaffected.
flac, mp3 = _cand('flac', 30), _cand('mp3', 5, bitrate=320)
out = _filter([mp3, flac], _q(aac=None))
assert out and out[0].quality == 'flac' # FLAC still wins, as before
assert out and out[0].quality == 'flac' # FLAC still wins
# ── opt-in: AAC on makes it a real tier ───────────────────────────────────────
def test_aac_selected_when_enabled():
# ── AAC as a real ranked target ───────────────────────────────────────────────
def test_aac_selected_when_targeted():
out = _filter([_cand('aac', 5)], _q(aac=True))
assert len(out) == 1 and out[0].quality == 'aac'
def test_flac_beats_aac_when_both_present():
def test_flac_beats_aac_when_listed_higher():
flac, aac = _cand('flac', 30), _cand('aac', 5)
out = _filter([aac, flac], _q(aac=True))
assert out[0].quality == 'flac' # priority 1 < 1.5
assert out[0].quality == 'flac' # flac target ranked above aac (priority 1 < 1.5)
def test_aac_beats_mp3_when_both_present():
def test_aac_beats_mp3_when_listed_higher():
mp3, aac = _cand('mp3', 5, bitrate=320), _cand('aac', 5)
out = _filter([mp3, aac], _q(aac=True))
assert out[0].quality == 'aac' # priority 1.5 < 2
def test_default_and_presets_ship_aac_disabled_above_mp3():
from database.music_database import MusicDatabase
db = MusicDatabase.__new__(MusicDatabase) # no DB init
profiles = [db._get_default_quality_profile()]
profiles += [db.get_quality_preset(p) for p in ('audiophile', 'balanced', 'space_saver')]
for prof in profiles:
aac = prof['qualities']['aac']
assert aac['enabled'] is False # opt-in everywhere
# above MP3: lower priority number than the best MP3 tier present
mp3_prios = [v['priority'] for k, v in prof['qualities'].items() if k.startswith('mp3')]
assert aac['priority'] < min(mp3_prios)
assert out[0].quality == 'aac' # aac target (1.5) ranked above mp3 (2)

View file

@ -84,6 +84,37 @@ def test_metadata_less_flac_matches_16bit_baseline_target():
assert AudioQuality('flac').matches_target(QualityTarget(format='flac', bit_depth=16)) is True
# ── ranked targets work for EVERY format, not just flac/mp3 (universal) ────
def test_opus_target_matches_opus_candidate():
t = QualityTarget(format='opus', min_bitrate=128)
assert AudioQuality('opus', bitrate=160).matches_target(t) is True
assert AudioQuality('opus', bitrate=96).matches_target(t) is False
assert AudioQuality('mp3', bitrate=320).matches_target(t) is False # wrong format
def test_wav_target_matches_on_bit_depth_and_sample_rate():
t = QualityTarget(format='wav', bit_depth=24, min_sample_rate=96000)
assert AudioQuality('wav', sample_rate=96000, bit_depth=24).matches_target(t) is True
assert AudioQuality('wav', sample_rate=44100, bit_depth=16).matches_target(t) is False
def test_wma_and_alac_targets_match_their_formats():
assert AudioQuality('wma', bitrate=192).matches_target(QualityTarget(format='wma', min_bitrate=128)) is True
assert AudioQuality('alac', sample_rate=96000, bit_depth=24).matches_target(
QualityTarget(format='alac', bit_depth=24)) is True
def test_only_listed_format_passes_others_rank_last():
"""An Opus-only target list: only opus matches; everything else ranks last
(index == len(targets)), so with fallback off the caller drops them."""
from core.quality.model import rank_candidate
targets = [QualityTarget(format='opus')]
assert rank_candidate(AudioQuality('opus', bitrate=160), targets)[0] == 0
assert rank_candidate(AudioQuality('flac', sample_rate=96000, bit_depth=24), targets)[0] == 1
assert rank_candidate(AudioQuality('mp3', bitrate=320), targets)[0] == 1
# ── v2 -> v3 migration preserves the user's priority order ─────────────────
def test_v2_to_v3_preserves_order_and_maps_fields():

View file

@ -13,9 +13,34 @@ from core.quality.source_map import (
quality_from_qobuz,
quality_from_deezer,
quality_from_amazon,
format_from_extension,
AUDIO_EXTENSIONS,
)
# ── Shared extension → format (used by every extension-based source) ────────
@pytest.mark.parametrize("ext,fmt", [
("flac", "flac"), (".flac", "flac"),
("mp3", "mp3"),
("m4a", "aac"), ("aac", "aac"), ("mp4", "aac"),
("ogg", "ogg"), ("oga", "ogg"),
("opus", "opus"),
("wav", "wav"), ("wave", "wav"),
("aiff", "wav"), ("aif", "wav"), # PCM → wav tier
("wma", "wma"),
("alac", "alac"),
("xyz", "unknown"), ("", "unknown"), (None, "unknown"),
])
def test_format_from_extension(ext, fmt):
assert format_from_extension(ext) == fmt
def test_audio_extensions_cover_all_known_formats():
for e in (".flac", ".mp3", ".m4a", ".ogg", ".opus", ".wav", ".aiff", ".wma"):
assert e in AUDIO_EXTENSIONS
# ── Tidal / HiFi (Tidal-backed) ────────────────────────────────────────────
@pytest.mark.parametrize("tier", ["HI_RES_LOSSLESS", "HI_RES", "hi_res", "hires", "HIRES"])

View file

@ -5057,9 +5057,18 @@
<!-- Add a new target -->
<div class="ranked-target-add">
<select id="rt-add-format" onchange="onRtAddFormatChange()">
<option value="flac">FLAC (lossless)</option>
<optgroup label="Lossless">
<option value="flac">FLAC</option>
<option value="alac">ALAC</option>
<option value="wav">WAV / AIFF</option>
</optgroup>
<optgroup label="Lossy">
<option value="mp3">MP3</option>
<option value="aac">AAC</option>
<option value="ogg">OGG (Vorbis)</option>
<option value="opus">Opus</option>
<option value="wma">WMA</option>
</optgroup>
</select>
<span class="rt-lossless-fields">
<select id="rt-add-bitdepth" title="Minimum bit depth">

View file

@ -1975,7 +1975,7 @@ let currentRankedTargets = [];
function rtLabel(t) {
const fmt = (t.format || 'any').toUpperCase();
if (t.format === 'flac' || t.format === 'wav') {
if (RT_LOSSLESS_FORMATS.includes(t.format)) {
const bd = t.bit_depth ? `${t.bit_depth}-bit` : '';
const sr = t.min_sample_rate ? `${t.min_sample_rate / 1000}kHz` : '';
const detail = [bd, sr].filter(Boolean).join('/');
@ -2066,8 +2066,12 @@ function deleteRankedTarget(i) {
debouncedSaveQualityProfile();
}
// Lossless formats take bit-depth + sample-rate constraints; lossy take a
// minimum bitrate. Single source of truth for the add-target field toggle.
const RT_LOSSLESS_FORMATS = ['flac', 'alac', 'wav'];
function onRtAddFormatChange() {
const lossless = document.getElementById('rt-add-format')?.value === 'flac';
const lossless = RT_LOSSLESS_FORMATS.includes(document.getElementById('rt-add-format')?.value);
const llFields = document.querySelector('.rt-lossless-fields');
const lyFields = document.querySelector('.rt-lossy-fields');
if (llFields) llFields.style.display = lossless ? '' : 'none';
@ -2077,7 +2081,7 @@ function onRtAddFormatChange() {
function addRankedTarget() {
const fmt = document.getElementById('rt-add-format')?.value || 'flac';
const t = { format: fmt };
if (fmt === 'flac') {
if (RT_LOSSLESS_FORMATS.includes(fmt)) {
const bd = document.getElementById('rt-add-bitdepth')?.value;
const sr = document.getElementById('rt-add-samplerate')?.value;
if (bd) t.bit_depth = parseInt(bd, 10);