From 8bb749de9c67e13dd24e5b2a1e80857737b0b895 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 15 Jun 2026 15:29:29 +0200 Subject: [PATCH] feat(import): master toggle for quality-filtering on import + collapsible tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answers "does import respect quality?": yes — the pipeline already runs the quality gate (check_quality_target) BEFORE AcoustID and quarantines files that don't meet the profile (unless fallback/downsample is on). This adds an explicit user switch over that behaviour. - New config import.quality_filter_enabled (default True). When False, check_quality_target returns None early so EVERY file imports regardless of quality; the file is still probed and the library Quality Upgrade Scanner still flags below-profile tracks. Default preserves current behaviour. - Settings → Library: the Import Settings group is now a collapsible tile (same pattern as Post-Processing) and gains the "Only import tracks that meet your quality profile" toggle at the top, alongside replace-lower-quality and folder-artist-override. - settings.js populate/collect the new key; config schema default added. - Tests: key-aware config stub (a blanket-False mock would wrongly disable the filter) + a new test pinning toggle-OFF = accept below-target file. Co-Authored-By: Claude Sonnet 4.6 --- config/settings.py | 6 ++++++ core/imports/guards.py | 11 +++++++++++ tests/imports/test_import_guards.py | 11 ++++++++++- tests/imports/test_quality_guard.py | 22 ++++++++++++++++++++-- webui/index.html | 26 +++++++++++++++++++++++++- webui/static/settings.js | 3 +++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/config/settings.py b/config/settings.py index f1482081..a31ed972 100644 --- a/config/settings.py +++ b/config/settings.py @@ -700,6 +700,12 @@ class ConfigManager: }, "import": { "staging_path": "./Staging", + # Master toggle for quality-filtering on import. On by default: + # downloaded files that don't meet the quality profile are + # quarantined instead of imported (same gate the download + # pipeline uses). Off → import everything regardless of quality; + # the library Quality Upgrade Scanner still flags them. + "quality_filter_enabled": True, "replace_lower_quality": False, # Use the top Staging folder as the artist (Artist/Album layouts, # mixtapes). On by default to preserve the long-standing import diff --git a/core/imports/guards.py b/core/imports/guards.py index 7f968daa..12bc9afb 100644 --- a/core/imports/guards.py +++ b/core/imports/guards.py @@ -127,6 +127,17 @@ def check_quality_target(file_path: str, context: dict) -> Optional[str]: from core.imports.file_ops import probe_audio_quality from core.quality.selection import targets_from_profile, quality_meets_profile + # Master toggle (Settings → Import). When OFF, the quality check is skipped + # entirely and files import regardless of quality — the user opted out of + # quality-filtering on import. Default ON preserves existing behaviour. The + # library Quality Upgrade scanner still flags below-profile files either way. + if _get_config_manager().get("import.quality_filter_enabled", True) is False: + logger.debug( + "[QualityGuard] import.quality_filter_enabled=False — skipping quality " + "filter for %s", os.path.basename(file_path), + ) + return None + aq = probe_audio_quality(file_path) if aq is None: logger.debug("[QualityGuard] Could not probe %s — skipping check", os.path.basename(file_path)) diff --git a/tests/imports/test_import_guards.py b/tests/imports/test_import_guards.py index 44cd5bde..22586d5a 100644 --- a/tests/imports/test_import_guards.py +++ b/tests/imports/test_import_guards.py @@ -35,9 +35,18 @@ _WANT_FLAC24 = { def _patch(monkeypatch, aq, profile): monkeypatch.setattr(file_ops, "probe_audio_quality", lambda fp: aq) monkeypatch.setattr(guards, "MusicDatabase", lambda: _FakeDB(profile)) + + # Key-aware config stub: the import quality filter is ON (its default), so + # the guard runs; everything else (downsample, etc.) is OFF. A blanket False + # would wrongly disable the filter itself via import.quality_filter_enabled. + def _cfg_get(key, default=None): + if key == "import.quality_filter_enabled": + return True + return False + monkeypatch.setattr( guards, "_get_config_manager", - lambda: SimpleNamespace(get=lambda _key, default=None: False), + lambda: SimpleNamespace(get=_cfg_get), ) diff --git a/tests/imports/test_quality_guard.py b/tests/imports/test_quality_guard.py index b180bf75..12ac06db 100644 --- a/tests/imports/test_quality_guard.py +++ b/tests/imports/test_quality_guard.py @@ -30,12 +30,20 @@ class _FakeDB: return self._p -def _patch_guard(monkeypatch, probe_aq, profile, downsample=False): +def _patch_guard(monkeypatch, probe_aq, profile, downsample=False, quality_filter=True): monkeypatch.setattr(file_ops, 'probe_audio_quality', lambda fp: probe_aq) monkeypatch.setattr(guards, 'MusicDatabase', lambda: _FakeDB(profile)) + + def _cfg_get(k, d=None): + if 'downsample' in k: + return downsample + if k == 'import.quality_filter_enabled': + return quality_filter + return d + monkeypatch.setattr( guards, '_get_config_manager', - lambda: types.SimpleNamespace(get=lambda k, d=None: downsample if 'downsample' in k else d), + lambda: types.SimpleNamespace(get=_cfg_get), ) @@ -68,6 +76,16 @@ def test_accepts_via_fallback(monkeypatch): assert guards.check_quality_target('/x/song.flac', {}) is None +def test_master_toggle_off_skips_filter(monkeypatch): + # import.quality_filter_enabled = False → a below-target file is accepted + # (imported) regardless of quality, even with fallback off. + _patch_guard( + monkeypatch, AudioQuality('flac', sample_rate=44100, bit_depth=16), + _WANT_FLAC24, quality_filter=False, + ) + assert guards.check_quality_target('/x/song.flac', {}) is None + + def test_skips_when_unprobeable(monkeypatch): _patch_guard(monkeypatch, None, _WANT_FLAC24) assert guards.check_quality_target('/x/song.flac', {}) is None diff --git a/webui/index.html b/webui/index.html index 6e531ba2..236f83a9 100644 --- a/webui/index.html +++ b/webui/index.html @@ -6261,10 +6261,32 @@ - + + + +
diff --git a/webui/static/settings.js b/webui/static/settings.js index e59abe3d..68cbd57b 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -1299,6 +1299,8 @@ async function loadSettingsData() { } // Populate Import settings + const _qualFilterEl = document.getElementById('import-quality-filter-enabled'); + if (_qualFilterEl) _qualFilterEl.checked = settings.import?.quality_filter_enabled !== false; // default ON document.getElementById('import-replace-lower-quality').checked = settings.import?.replace_lower_quality === true; const _folderArtistEl = document.getElementById('import-folder-artist-override'); if (_folderArtistEl) _folderArtistEl.checked = settings.import?.folder_artist_override === true; @@ -3071,6 +3073,7 @@ async function saveSettings(quiet = false) { music_videos_path: document.getElementById('music-videos-path').value || './MusicVideos' }, import: { + quality_filter_enabled: document.getElementById('import-quality-filter-enabled')?.checked !== false, replace_lower_quality: document.getElementById('import-replace-lower-quality').checked, folder_artist_override: document.getElementById('import-folder-artist-override')?.checked === true, staging_path: document.getElementById('staging-path').value || './Staging'