From 674b80972a4834b963a73aa86db02ecbb3d32e49 Mon Sep 17 00:00:00 2001 From: nick2000713 Date: Tue, 23 Jun 2026 12:50:58 +0200 Subject: [PATCH] =?UTF-8?q?feat(acoustid):=20opt-in=20fail-closed=20mode?= =?UTF-8?q?=20=E2=80=94=20only=20import=20verified=20tracks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New setting acoustid.require_verified (default off), shown under Settings → Quality Profile only when AcoustID is enabled. When on, an AcoustID SKIP (ran but couldn't confirm — no fingerprint match or cross-script metadata, the ⚠ "unverified" case) is treated like a FAIL: the file is quarantined and the next-best candidate is tried, instead of importing an unverified file. Only a clean AcoustID PASS is kept. Transient ERROR results (rate-limit / outage) are deliberately NOT blocked — that would stall the whole pipeline during an AcoustID outage. Those still import with their existing flag. - pipeline.py: SKIP routes through the existing FAIL quarantine + retry path (trigger 'acoustid') when require_verified is on. - UI: checkbox under Quality Profile, visibility tied to acoustid-enabled via syncAcoustidRequireVerifiedVisibility(); load/save wired in settings.js. Co-Authored-By: Claude Opus 4.8 --- core/imports/pipeline.py | 21 ++++++++++++++++++++- webui/index.html | 19 ++++++++++++++++++- webui/static/settings.js | 16 +++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/core/imports/pipeline.py b/core/imports/pipeline.py index 69ed30b1..50c774b2 100644 --- a/core/imports/pipeline.py +++ b/core/imports/pipeline.py @@ -503,7 +503,26 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta logger.info(f"AcoustID verification result: {verification_result.value} - {verification_msg}") context['_acoustid_result'] = verification_result.value - if verification_result == VerificationResult.FAIL: + # Fail-closed mode: when the user requires a hard AcoustID + # PASS, a SKIP (ran but couldn't confirm — no fingerprint + # match / cross-script metadata) is treated like a FAIL: + # quarantine + try the next candidate, instead of importing + # an unverified file. ERROR (rate-limit / infra) is NOT + # blocked — that would stall the whole pipeline during an + # outage; those still import with their existing flag. + require_verified = config_manager.get('acoustid.require_verified', False) + _skip_as_fail = ( + require_verified + and verification_result == VerificationResult.SKIP + ) + if _skip_as_fail: + verification_msg = ( + f"AcoustID could not confirm the track and 'require verified' " + f"is on — rejecting unverified file ({verification_msg})" + ) + logger.warning("[AcoustID] Require-verified: SKIP treated as FAIL — %s", verification_msg) + + if verification_result == VerificationResult.FAIL or _skip_as_fail: try: quarantine_path = move_to_quarantine( file_path, diff --git a/webui/index.html b/webui/index.html index ccc75be9..9023a52e 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4224,7 +4224,8 @@
@@ -5118,6 +5119,22 @@
+ + +
How it works: Each download source is checked against this list top-down. The first target a source can satisfy wins; a source that meets no target diff --git a/webui/static/settings.js b/webui/static/settings.js index 272e5f01..3da92eb4 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -40,6 +40,15 @@ async function copyAddress(address, cryptoName) { let settingsAutoSaveTimer = null; +// The "Only import AcoustID-verified tracks" toggle (under Quality Profile) is +// meaningless when AcoustID itself is off — only show it when verification is on. +function syncAcoustidRequireVerifiedVisibility() { + const group = document.getElementById('acoustid-require-verified-group'); + const enabled = document.getElementById('acoustid-enabled'); + if (group) group.style.display = (enabled && enabled.checked) ? '' : 'none'; +} +window.syncAcoustidRequireVerifiedVisibility = syncAcoustidRequireVerifiedVisibility; + function debouncedAutoSaveSettings() { // Ignore changes made while the page is programmatically populating its // fields on load — those aren't user edits and must not trigger a full @@ -1078,6 +1087,10 @@ async function loadSettingsData() { // Populate AcoustID settings document.getElementById('acoustid-api-key').value = settings.acoustid?.api_key || ''; document.getElementById('acoustid-enabled').checked = settings.acoustid?.enabled || false; + const _acoustidRequireVerified = document.getElementById('acoustid-require-verified'); + if (_acoustidRequireVerified) _acoustidRequireVerified.checked = settings.acoustid?.require_verified === true; + // Show the "require verified" toggle (under Quality Profile) only when AcoustID is on. + if (typeof syncAcoustidRequireVerifiedVisibility === 'function') syncAcoustidRequireVerifiedVisibility(); // Populate Last.fm settings document.getElementById('lastfm-api-key').value = settings.lastfm?.api_key || ''; @@ -3001,7 +3014,8 @@ async function saveSettings(quiet = false) { }, acoustid: { api_key: document.getElementById('acoustid-api-key').value, - enabled: document.getElementById('acoustid-enabled').checked + enabled: document.getElementById('acoustid-enabled').checked, + require_verified: document.getElementById('acoustid-require-verified')?.checked === true }, lastfm: { api_key: document.getElementById('lastfm-api-key').value,