feat(acoustid): opt-in fail-closed mode — only import verified tracks

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 <noreply@anthropic.com>
This commit is contained in:
nick2000713 2026-06-23 12:50:58 +02:00
parent d169818043
commit 674b80972a
3 changed files with 53 additions and 3 deletions

View file

@ -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,

View file

@ -4224,7 +4224,8 @@
<label class="checkbox-label"
style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="acoustid-enabled"
style="width: 16px; height: 16px;">
style="width: 16px; height: 16px;"
onchange="if (typeof syncAcoustidRequireVerifiedVisibility === 'function') syncAcoustidRequireVerifiedVisibility();">
<span>Enable Download Verification</span>
</label>
<div style="color: #888; font-size: 0.8em; margin-top: 4px; margin-left: 24px;">
@ -5118,6 +5119,22 @@
</div>
</div>
<!-- Require hard AcoustID verification — only shown when AcoustID is enabled -->
<div class="form-group" id="acoustid-require-verified-group" style="display:none;">
<label class="checkbox-label">
<input type="checkbox" id="acoustid-require-verified">
Only import AcoustID-verified tracks (reject "could not confirm")
</label>
<div class="help-text" style="margin-top:4px">
When <strong>on</strong>, a download that AcoustID runs but <em>cannot confirm</em>
(no fingerprint match / cross-script metadata — the ⚠ "unverified" case) is
<strong>quarantined and the next candidate is tried</strong>, instead of being
imported as unverified. Only a clean AcoustID <strong>pass</strong> is kept.
Transient lookup errors (rate-limit / outage) still import, so an AcoustID outage
never stalls your downloads. Requires AcoustID to be enabled.
</div>
</div>
<div class="help-text">
<strong>How it works:</strong> Each download source is checked against this list
top-down. The first target a source can satisfy wins; a source that meets no target

View file

@ -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,