#886: AAC as an opt-in Soulseek quality tier (purely additive, off by default)

radoslav-orlov: add AAC as a download quality option. AAC is more efficient than
MP3, so it's useful for Soulseek/torrents (streaming sources pick their own
codec; Amazon — the AAC-heavy one — is down).

Additive by construction: every quality tier already defaults enabled=false and
the waterfall is built only from enabled tiers, so AAC ships OFF and the bucketer
routes a not-enabled AAC file to the 'other' bucket EXACTLY as today (where it was
silently dropped). Only a user who turns AAC on makes it a first-class tier,
ranked above MP3 / below FLAC (priority 1.5, min-kbps gate so junk AAC can't beat
a good MP3).

- music_database: aac tier (disabled) in the default profile + all 3 presets.
- soulseek_client: map .m4a -> 'aac' in both result parsers (was 'unknown' ->
  dropped); add the 'aac' bucket + a gated branch + a fallback size limit.
- settings UI: an 'AAC' tier toggle (unchecked) between FLAC and MP3; save
  defaults its priority to 1.5 so upgraded profiles rank it right on first save.

7 seam tests pinning the additive guarantee (aac absent/disabled -> dropped as
before; FLAC/MP3 selection unchanged; aac on -> selectable, below FLAC, above
MP3); 81 quality/soulseek tests pass, ruff clean. quality_upgrade left untouched
(its AAC handling is unchanged).
This commit is contained in:
BoulderBadgeDad 2026-06-18 13:45:52 -07:00
parent 56da2e105c
commit 400b35d655
5 changed files with 194 additions and 7 deletions

View file

@ -426,8 +426,11 @@ class SoulseekClient(DownloadSourcePlugin):
if f'.{file_ext}' not in audio_extensions:
continue
quality = file_ext if file_ext in ['flac', 'mp3', 'ogg', 'aac', 'wma'] else 'unknown'
# .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')
# Create TrackResult
# Convert duration from seconds to milliseconds (slskd returns seconds, Spotify uses ms)
raw_duration = file_data.get('length')
@ -1147,7 +1150,9 @@ class SoulseekClient(DownloadSourcePlugin):
ext = Path(filename).suffix.lower()
if ext not in audio_extensions:
continue
quality = ext.lstrip('.') if ext.lstrip('.') in ['flac', 'mp3', 'ogg', 'aac', 'wma'] else 'unknown'
_qext = ext.lstrip('.')
quality = 'aac' if _qext == 'm4a' else (
_qext if _qext in ['flac', 'mp3', 'ogg', 'aac', 'wma'] else 'unknown')
raw_duration = file_data.get('length')
duration_ms = raw_duration * 1000 if raw_duration else None
results.append(TrackResult(
@ -1957,6 +1962,7 @@ class SoulseekClient(DownloadSourcePlugin):
'mp3_320': (1, 50),
'mp3_256': (1, 40),
'mp3_192': (1, 30),
'aac': (1, 50),
'other': (0, 500),
}
@ -2040,6 +2046,7 @@ class SoulseekClient(DownloadSourcePlugin):
'mp3_320': [],
'mp3_256': [],
'mp3_192': [],
'aac': [],
'other': []
}
@ -2067,6 +2074,17 @@ class SoulseekClient(DownloadSourcePlugin):
else:
quality_buckets['other'].append(candidate)
continue
elif track_format in ('aac', 'm4a'):
# Opt-in AAC tier. ADDITIVE: when AAC isn't enabled in the
# profile (the default, and every profile that predates this),
# route exactly where AAC went before — the 'other' bucket — so
# behaviour is byte-identical. Only a user who turns AAC on lets
# it become a first-class, selectable tier.
aac_cfg = profile['qualities'].get('aac')
if not (aac_cfg and aac_cfg.get('enabled')):
quality_buckets['other'].append(candidate)
continue
quality_key = 'aac'
else:
quality_buckets['other'].append(candidate)
continue

View file

@ -8676,6 +8676,15 @@ class MusicDatabase:
"min_kbps": 150,
"max_kbps": 300,
"priority": 4
},
# AAC (incl. .m4a): opt-in, OFF by default. Priority 1.5 sits it
# above MP3 but below FLAC (AAC is more efficient than MP3); the
# min_kbps gate keeps junk-bitrate AAC from beating a good MP3.
"aac": {
"enabled": False,
"min_kbps": 128,
"max_kbps": 400,
"priority": 1.5
}
},
"fallback_enabled": True
@ -8725,6 +8734,12 @@ class MusicDatabase:
"min_kbps": 150,
"max_kbps": 300,
"priority": 4
},
"aac": {
"enabled": False,
"min_kbps": 128,
"max_kbps": 400,
"priority": 1.5
}
},
"fallback_enabled": False
@ -8757,6 +8772,12 @@ class MusicDatabase:
"min_kbps": 150,
"max_kbps": 300,
"priority": 4
},
"aac": {
"enabled": False,
"min_kbps": 128,
"max_kbps": 400,
"priority": 1.5
}
},
"fallback_enabled": True
@ -8789,6 +8810,14 @@ class MusicDatabase:
"min_kbps": 150,
"max_kbps": 300,
"priority": 3
},
# Space-saver favours small files, where AAC shines — but it
# still ships OFF (opt-in). Priority 0.5 puts it above MP3.
"aac": {
"enabled": False,
"min_kbps": 128,
"max_kbps": 400,
"priority": 0.5
}
},
"fallback_enabled": True

View file

@ -0,0 +1,106 @@
"""#886: AAC as an opt-in Soulseek quality tier.
The whole point is "purely additive": with AAC OFF (the default, and every
profile that predates this), an AAC candidate must behave EXACTLY as before
it lands in the 'other' bucket, which the waterfall never returns, so it's
dropped. Only a profile that explicitly enables AAC makes it a selectable tier,
ranked above MP3 and below FLAC.
filter_results_by_quality_preference reads db.get_quality_profile() and walks the
buckets; we stub the db + the quarantine sweep so it runs offline.
"""
from __future__ import annotations
import types
from pathlib import Path
from unittest.mock import patch
import pytest
from core.soulseek_client import SoulseekClient
from core.download_plugins.types import TrackResult
def _client():
c = SoulseekClient.__new__(SoulseekClient)
c.base_url = 'http://localhost:5030'
c.api_key = 'k'
c.download_path = Path('./test_downloads')
return c
def _cand(quality, size_mb, bitrate=None):
return TrackResult(
username='peer', filename=f'A/B/01 - Song.{quality}',
size=int(size_mb * 1024 * 1024), bitrate=bitrate, duration=None,
quality=quality, free_upload_slots=1, upload_speed=1_000_000,
queue_length=0, artist='A', title='Song', album='B', track_number=1)
def _q(enabled_flac=True, enabled_mp3=True, aac=None):
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)
qualities['aac'] = {'enabled': aac, 'min_kbps': 128, 'max_kbps': 400, 'priority': 1.5}
return {'preset': 'custom', 'qualities': qualities, 'fallback_enabled': True}
def _filter(candidates, profile):
c = _client()
fake_db = types.SimpleNamespace(get_quality_profile=lambda: profile)
with patch('database.music_database.MusicDatabase', return_value=fake_db), \
patch.object(SoulseekClient, '_drop_quarantined_sources', lambda self, r: r):
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
def test_aac_dropped_when_tier_present_but_disabled():
out = _filter([_cand('aac', 5)], _q(aac=False))
assert out == []
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
# ── opt-in: AAC on makes it a real tier ───────────────────────────────────────
def test_aac_selected_when_enabled():
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():
flac, aac = _cand('flac', 30), _cand('aac', 5)
out = _filter([aac, flac], _q(aac=True))
assert out[0].quality == 'flac' # priority 1 < 1.5
def test_aac_beats_mp3_when_both_present():
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)

View file

@ -5135,6 +5135,37 @@
</div>
</div>
<!-- AAC Quality (opt-in; Soulseek/torrents — off by default) -->
<div class="quality-tier">
<div class="quality-tier-header">
<label class="checkbox-label">
<input type="checkbox" id="quality-aac-enabled"
onchange="toggleQuality('aac')">
<span class="quality-tier-name">AAC <span style="opacity:.6;font-weight:400;">(.m4a/.aac — Soulseek)</span></span>
</label>
<span class="quality-tier-priority" id="priority-aac">Priority: 1.5</span>
</div>
<div class="quality-tier-sliders" id="sliders-aac">
<div class="slider-group">
<label>Bitrate Range:</label>
<div class="dual-slider-container">
<input type="range" class="range-slider range-slider-min"
id="aac-min" min="0" max="400" value="128" step="10"
oninput="updateQualityRange('aac')">
<input type="range" class="range-slider range-slider-max"
id="aac-max" min="0" max="400" value="400" step="10"
oninput="updateQualityRange('aac')">
<div class="range-slider-track"></div>
</div>
<div class="slider-values">
<span id="aac-min-value">128 kbps</span>
<span>-</span>
<span id="aac-max-value">400 kbps</span>
</div>
</div>
</div>
</div>
<!-- MP3 320 Quality -->
<div class="quality-tier">
<div class="quality-tier-header">

View file

@ -1934,7 +1934,7 @@ function populateQualityProfileUI(profile) {
}
// Populate each quality tier
const qualities = ['flac', 'mp3_320', 'mp3_256', 'mp3_192'];
const qualities = ['flac', 'aac', 'mp3_320', 'mp3_256', 'mp3_192'];
qualities.forEach(quality => {
const config = profile.qualities[quality];
if (config) {
@ -2128,15 +2128,18 @@ function collectQualityProfileFromUI() {
fallback_enabled: document.getElementById('quality-fallback-enabled')?.checked ?? true
};
const qualities = ['flac', 'mp3_320', 'mp3_256', 'mp3_192'];
const qualities = ['flac', 'aac', 'mp3_320', 'mp3_256', 'mp3_192'];
qualities.forEach((quality, index) => {
const enabled = document.getElementById(`quality-${quality}-enabled`)?.checked || false;
const minSlider = document.getElementById(`${quality}-min`);
const maxSlider = document.getElementById(`${quality}-max`);
// Preserve priority from the currently loaded profile instead of using array order
const existingPriority = currentQualityProfile?.qualities?.[quality]?.priority ?? (index + 1);
// Preserve priority from the currently loaded profile instead of using array order.
// AAC's default is 1.5 (above MP3, below FLAC) — not index+1 — so an upgraded
// profile that never had an aac tier still ranks it correctly on first save.
const _defaultPriority = quality === 'aac' ? 1.5 : (index + 1);
const existingPriority = currentQualityProfile?.qualities?.[quality]?.priority ?? _defaultPriority;
profile.qualities[quality] = {
enabled: enabled,