From ba6c39bae31e3556c6f20114517e493facf50330 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sun, 31 May 2026 20:10:31 -0700 Subject: [PATCH] AcoustID: report errors honestly instead of masking them as 'Skipped' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An invalid API key (and rate limits / missing chromaprint / fingerprint failures) all collapsed into the same None as a genuine no-match, so: - every download showed a benign 'AcoustID: Skipped', and - the 'Test API key' button reported a dead key as VALID because test_api_key trusted 'no exception = valid' but fingerprint_and_lookup swallows the error and returns None. A broken AcoustID setup looked completely normal — which cost a real debugging session to untangle. - New AcoustIDClient.lookup_with_status() returns a structured result that distinguishes ok / no_match / error / no_backend / fingerprint_error / unavailable. fingerprint_and_lookup() stays dict-or-None (library scanner / auto-import / their tests unchanged) as a thin wrapper over it. - verify_audio_file() uses it: a real error -> new VerificationResult.ERROR (-> _acoustid_result='error' -> the existing red 'Error' history badge), a genuine no-match -> SKIP 'No match in AcoustID database'. ERROR never quarantines (an outage/bad key must not punish good files). - test_api_key() now validates via the authoritative direct API call (error code 4 = invalid key) instead of the swallowed-exception path. Tests: structured-status distinction, legacy-wrapper contract, verify ERROR vs SKIP, and test_api_key invalid-vs-accepted. Existing verify tests updated to stub lookup_with_status (a stub returning just recordings is inferred as ok). --- core/acoustid_client.py | 134 +++++++------ core/acoustid_verification.py | 28 ++- .../test_acoustid_verification_aliases.py | 18 +- tests/test_acoustid_error_reporting.py | 180 ++++++++++++++++++ tests/test_acoustid_skip_logic.py | 6 +- tests/test_acoustid_version_mismatch.py | 4 +- 6 files changed, 281 insertions(+), 89 deletions(-) create mode 100644 tests/test_acoustid_error_reporting.py diff --git a/core/acoustid_client.py b/core/acoustid_client.py index b11c5960..a97f149a 100644 --- a/core/acoustid_client.py +++ b/core/acoustid_client.py @@ -282,8 +282,9 @@ class AcoustIDClient: def test_api_key(self) -> Tuple[bool, str]: """ - Validate the API key by fingerprinting a real audio file and looking it up. - Falls back to a direct API call if no audio files are available. + Validate the API key with a direct AcoustID lookup call. An invalid key + is reported as invalid (error code 4); any other error means the key was + accepted. Returns: Tuple of (success, message) @@ -294,24 +295,12 @@ class AcoustIDClient: import requests try: - # Try to find a real audio file to fingerprint for an end-to-end test - test_file = self._find_test_audio_file() - - if test_file and CHROMAPRINT_AVAILABLE: - logger.info(f"Testing API key with real audio file: {test_file}") - try: - result = self.fingerprint_and_lookup(test_file) - # If we get here without exception, the API key is valid - # (invalid keys raise or return error before results) - return True, "AcoustID API key is valid" - except Exception as e: - error_str = str(e).lower() - if 'invalid' in error_str and 'api' in error_str: - return False, "Invalid AcoustID API key - get one from https://acoustid.org/new-application" - # Fingerprint/lookup failed for non-key reasons, fall through to direct test - logger.warning(f"Real file test failed ({e}), trying direct API call") - - # Fallback: direct API call with minimal fingerprint + # Authoritative key check: a direct API lookup with a dummy + # fingerprint. AcoustID validates the client key first, so an + # invalid key returns error code 4 regardless of the fingerprint. + # (The previous real-file path trusted "no exception = valid", but + # fingerprint_and_lookup swallows the invalid-key error and returns + # None — so it reported broken keys as valid. #756-adjacent.) url = 'https://api.acoustid.org/v2/lookup' params = { 'client': self.api_key, @@ -326,7 +315,6 @@ class AcoustIDClient: if data.get('status') == 'error': error = data.get('error', {}) error_code = error.get('code', 0) - error_msg = error.get('message', 'Unknown error') # Error code 4 is specifically "invalid API key" if error_code == 4: @@ -346,33 +334,33 @@ class AcoustIDClient: logger.error(f"Error testing AcoustID API key: {e}") return False, f"Error: {str(e)}" - def fingerprint_and_lookup(self, audio_file: str) -> Optional[Dict[str, Any]]: - """ - Generate fingerprint and look up recording in AcoustID. + def lookup_with_status(self, audio_file: str) -> Dict[str, Any]: + """Fingerprint + AcoustID lookup returning a STRUCTURED result. - This is the main method - combines fingerprinting and lookup in one call. + Unlike fingerprint_and_lookup() (which collapses every outcome into + dict-or-None), this distinguishes a genuine no-match from an actual + error — an invalid API key, rate limit, missing chromaprint, or a + fingerprint failure. That distinction is what lets the UI show "AcoustID + Error" (something is broken — fix it) instead of a benign-looking + "Skipped" that silently hides a dead key. - Args: - audio_file: Path to the audio file - - Returns: - Dict with: - 'recordings': list of dicts with 'mbid', 'title', 'artist', 'score' - 'best_score': float (highest score across all results) - 'recording_mbids': list of unique MBIDs (for backward compat) - Or None on error. + Returns dict with: + 'status': 'ok' | 'no_match' | 'error' | 'no_backend' + | 'fingerprint_error' | 'unsupported' | 'unavailable' + | 'not_found' + 'recordings': list (meaningful only for 'ok') + 'best_score': float + 'recording_mbids': list + 'error': human-readable detail for any non-'ok' status + 'invalid_key': bool (True when the API specifically rejected the key) """ if not ACOUSTID_AVAILABLE: - logger.debug("Cannot lookup: pyacoustid not available") - return None - + return {'status': 'unavailable', 'recordings': [], 'error': 'pyacoustid library not installed'} if not self.api_key: - logger.debug("Cannot lookup: no API key") - return None - + return {'status': 'unavailable', 'recordings': [], 'error': 'No AcoustID API key configured'} if not os.path.isfile(audio_file): logger.warning(f"Cannot lookup: file not found: {audio_file}") - return None + return {'status': 'not_found', 'recordings': [], 'error': f'File not found: {audio_file}'} # Check channel count — chromaprint crashes (SIGABRT) on >2 channel files (e.g. 5.1 surround) try: @@ -382,7 +370,8 @@ class AcoustIDClient: channels = getattr(mf.info, 'channels', 2) if channels and channels > 2: logger.warning(f"Skipping AcoustID: file has {channels} channels (surround audio): {audio_file}") - return None + return {'status': 'unsupported', 'recordings': [], + 'error': f'{channels}-channel (surround) audio not supported by chromaprint'} except Exception as e: logger.debug(f"Could not check channel count, proceeding anyway: {e}") @@ -392,17 +381,12 @@ class AcoustIDClient: api_key_preview = f"{self.api_key[:8]}..." if self.api_key and len(self.api_key) > 8 else "NOT SET" logger.info(f"Fingerprinting and looking up: {audio_file} (API key: {api_key_preview})") - # Use match() which handles fingerprinting + lookup + parsing logger.debug("Running acoustid.match()...") recordings = [] seen_mbids = set() best_score = 0.0 - for result in acoustid.match( - self.api_key, - audio_file, - parse=True - ): + for result in acoustid.match(self.api_key, audio_file, parse=True): # match() with parse=True returns (score, recording_id, title, artist) if not isinstance(result, tuple) or len(result) < 2: logger.warning(f"Unexpected result format: {result}") @@ -420,45 +404,57 @@ class AcoustIDClient: if recording_id and recording_id not in seen_mbids: seen_mbids.add(recording_id) - recordings.append({ - 'mbid': recording_id, - 'title': title, - 'artist': artist, - 'score': score, - }) + recordings.append({'mbid': recording_id, 'title': title, 'artist': artist, 'score': score}) logger.debug(f"Found match: {title} by {artist} (MBID: {recording_id}, score: {score})") if not recordings: logger.info(f"No AcoustID matches found for: {audio_file}") - return None + return {'status': 'no_match', 'recordings': [], 'best_score': best_score, + 'recording_mbids': [], 'error': 'Track not found in AcoustID database'} logger.info(f"AcoustID found {len(recordings)} recording(s) (best score: {best_score:.2f})") - return { - 'recordings': recordings, - 'best_score': best_score, - 'recording_mbids': list(seen_mbids), - } + return {'status': 'ok', 'recordings': recordings, 'best_score': best_score, + 'recording_mbids': list(seen_mbids)} except acoustid.NoBackendError: logger.error("Chromaprint library not found and fpcalc not available") - return None + return {'status': 'no_backend', 'recordings': [], + 'error': 'Chromaprint/fpcalc not installed (install libchromaprint1)'} except acoustid.FingerprintGenerationError as e: logger.warning(f"Failed to fingerprint {audio_file}: {e}") - return None + return {'status': 'fingerprint_error', 'recordings': [], 'error': f'Could not fingerprint file: {e}'} except acoustid.WebServiceError as e: - # Log more details about the API error api_key_preview = f"{self.api_key[:8]}..." if self.api_key and len(self.api_key) > 8 else "???" logger.warning(f"AcoustID API error (key: {api_key_preview}): {e}") - # Check for common errors error_str = str(e).lower() - if 'invalid' in error_str or 'unknown' in error_str: - logger.error("API key appears to be invalid - check your AcoustID settings") + # Old pyacoustid reports an invalid key as the bare "status: error" + # (it drops the detail), so treat that as an invalid-key signal too. + invalid = ('invalid' in error_str or 'unknown' in error_str or 'status: error' in error_str) + if invalid: + logger.error("AcoustID API key appears to be invalid — check your AcoustID settings") elif 'rate' in error_str or 'limit' in error_str: - logger.warning("Rate limited by AcoustID - will retry later") - return None + logger.warning("Rate limited by AcoustID — will retry later") + return {'status': 'error', 'recordings': [], 'invalid_key': invalid, + 'error': f'AcoustID API error: {e}'} except Exception as e: logger.error(f"Unexpected error in AcoustID lookup: {e}", exc_info=True) - return None + return {'status': 'error', 'recordings': [], 'error': f'Unexpected error: {e}'} + + def fingerprint_and_lookup(self, audio_file: str) -> Optional[Dict[str, Any]]: + """Legacy dict-or-None lookup. Returns the recordings dict on a confirmed + match, else None. Kept for callers that only need "did we identify it" + (library scanner, auto-import). Callers that must report WHY a lookup + didn't match (verification badge, key test) should use + ``lookup_with_status`` so an error isn't mistaken for a no-match. + """ + res = self.lookup_with_status(audio_file) + if res.get('status') == 'ok': + return { + 'recordings': res['recordings'], + 'best_score': res.get('best_score', 0.0), + 'recording_mbids': res.get('recording_mbids', []), + } + return None def refresh_config(self): """Refresh cached config values (call after settings change).""" diff --git a/core/acoustid_verification.py b/core/acoustid_verification.py index d1b87918..24930ec0 100644 --- a/core/acoustid_verification.py +++ b/core/acoustid_verification.py @@ -50,8 +50,9 @@ class VerificationResult(Enum): """Possible outcomes of audio verification.""" PASS = "pass" # Title/artist match - file is correct FAIL = "fail" # Title/artist mismatch - wrong file downloaded - SKIP = "skip" # Could not verify (error or unavailable) - continue normally + SKIP = "skip" # Genuinely couldn't verify (no match in DB) - continue normally DISABLED = "disabled" # Verification not enabled + ERROR = "error" # Lookup errored (invalid key / rate limit / no backend) - continue, but flag it def _normalize(text: str) -> str: @@ -399,18 +400,33 @@ class AcoustIDVerification: logger.debug(f"AcoustID verification skipped: {reason}") return VerificationResult.SKIP, reason - # Step 2: Fingerprint and lookup in AcoustID + # Step 2: Fingerprint and lookup in AcoustID (structured so an + # actual error — invalid key / rate limit / no chromaprint — is + # reported distinctly from a genuine no-match, instead of both + # silently surfacing as "Skipped"). logger.info(f"Fingerprinting and looking up: {audio_file_path}") - acoustid_result = self.acoustid_client.fingerprint_and_lookup(audio_file_path) + lookup = self.acoustid_client.lookup_with_status(audio_file_path) or {} + status = lookup.get('status') + # Infer status by content when absent (a caller/stub that returned + # just recordings): recordings => matched, none => no match. + if status is None: + status = 'ok' if lookup.get('recordings') else 'no_match' - if not acoustid_result: - return VerificationResult.SKIP, "Track not found in AcoustID database" + if status in ('error', 'no_backend', 'fingerprint_error', 'unavailable'): + # Something is broken (not the track's fault) — never quarantine + # on this; surface it so the user can fix it. + return VerificationResult.ERROR, lookup.get('error', 'AcoustID lookup failed') + if status != 'ok': + # no_match / unsupported / not_found — genuinely could not verify. + return VerificationResult.SKIP, lookup.get('error', 'No match in AcoustID database') + + acoustid_result = lookup recordings = acoustid_result.get('recordings', []) best_score = acoustid_result.get('best_score', 0) if not recordings: - return VerificationResult.SKIP, "AcoustID returned no recordings" + return VerificationResult.SKIP, "No match in AcoustID database" logger.debug( f"AcoustID returned {len(recordings)} recording(s) " diff --git a/tests/matching/test_acoustid_verification_aliases.py b/tests/matching/test_acoustid_verification_aliases.py index 2f2748b7..987130c0 100644 --- a/tests/matching/test_acoustid_verification_aliases.py +++ b/tests/matching/test_acoustid_verification_aliases.py @@ -140,7 +140,7 @@ class TestIssue442Regression: verifier, fake_service = stubbed_verifier # AcoustID returns the recording with kanji artist - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'YAMANAIAME', 'artist': '澤野弘之', 'mbid': 'rec-x'}, @@ -165,7 +165,7 @@ class TestIssue442Regression: """Reporter's case 2 — Sergey Lazarev / Сергей Лазарев.""" verifier, fake_service = stubbed_verifier - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'On the Other Side', 'artist': 'Sergey Lazarev', 'mbid': 'rec-y'}, @@ -196,7 +196,7 @@ class TestBackwardCompat: verifier, fake_service = stubbed_verifier # Wrong artist entirely — Latin script both sides, sim ~0 - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'Some Track', 'artist': 'Khalil Turk & Friends'}, @@ -214,7 +214,7 @@ class TestBackwardCompat: """Exact title + artist match → PASS regardless of aliases.""" verifier, fake_service = stubbed_verifier - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'Dirty White Boy', 'artist': 'Foreigner'}, @@ -233,7 +233,7 @@ class TestBackwardCompat: verifier, fake_service = stubbed_verifier fake_service.lookup_artist_aliases.side_effect = Exception("MB down") - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'Dirty White Boy', 'artist': 'Foreigner'}, @@ -261,7 +261,7 @@ class TestAliasLookupCalledOncePerVerify: fire 60+ MB lookups (cached or not, that's wasteful).""" verifier, fake_service = stubbed_verifier - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'X', 'artist': '澤野弘之'}, @@ -295,7 +295,7 @@ class TestLazyAliasResolution: this PR.""" verifier, fake_service = stubbed_verifier - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'Dirty White Boy', 'artist': 'Foreigner'}, @@ -317,7 +317,7 @@ class TestLazyAliasResolution: as expected.""" verifier, fake_service = stubbed_verifier - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'YAMANAIAME', 'artist': '澤野弘之'}, @@ -343,7 +343,7 @@ class TestLazyAliasResolution: # Force a code path that hits multiple sites: title matches # several recordings but the best-match's artist sim is below # threshold (forces secondary scan path). - verifier.acoustid_client.fingerprint_and_lookup.return_value = { + verifier.acoustid_client.lookup_with_status.return_value = { 'best_score': 0.95, 'recordings': [ {'title': 'X', 'artist': 'Different Latin Artist'}, # 0 alias hit diff --git a/tests/test_acoustid_error_reporting.py b/tests/test_acoustid_error_reporting.py new file mode 100644 index 00000000..699a785b --- /dev/null +++ b/tests/test_acoustid_error_reporting.py @@ -0,0 +1,180 @@ +"""AcoustID error-vs-no-match reporting. + +Regression for the masking bug: an invalid API key (and other lookup errors) +used to collapse into the same `None` as a genuine no-match, so the UI showed a +benign "Skipped" and the "Test API key" button reported a dead key as valid. +These tests pin the distinction end to end: + - lookup_with_status separates ok / no_match / error / no_backend / unavailable + - fingerprint_and_lookup (legacy) stays dict-or-None + - verify_audio_file -> ERROR for a real error, SKIP for a genuine no-match + - test_api_key reports an invalid key (API error code 4) as invalid +""" + +from __future__ import annotations + +import sys +import types +from unittest.mock import MagicMock, patch + +import pytest + +import core.acoustid_client as acc +from core.acoustid_client import AcoustIDClient +from core.acoustid_verification import AcoustIDVerification, VerificationResult + + +# ── lookup_with_status: structured status distinction ────────────────────── + +def _client_with_fake_acoustid(monkeypatch, *, match=None, raises=None): + """An AcoustIDClient wired to a fake `acoustid` module so we can drive + match() without network or chromaprint.""" + fake = types.ModuleType("acoustid") + + class WebServiceError(Exception): + pass + + class NoBackendError(Exception): + pass + + class FingerprintGenerationError(Exception): + pass + + fake.WebServiceError = WebServiceError + fake.NoBackendError = NoBackendError + fake.FingerprintGenerationError = FingerprintGenerationError + + def _match(api_key, audio_file, parse=True): + if raises is not None: + raise raises + return match or [] + + fake.match = _match + monkeypatch.setitem(sys.modules, "acoustid", fake) + monkeypatch.setattr(acc, "ACOUSTID_AVAILABLE", True) + + c = AcoustIDClient() + c._api_key = "testkey123" # bypass config + return c, fake + + +def test_lookup_status_ok(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"not audio") # mutagen -> None, channel check skipped + c, _ = _client_with_fake_acoustid(monkeypatch, match=[(0.95, "mbid-1", "Title", "Artist")]) + res = c.lookup_with_status(str(f)) + assert res["status"] == "ok" + assert res["recordings"] and res["recordings"][0]["mbid"] == "mbid-1" + + +def test_lookup_status_no_match(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"not audio") + c, _ = _client_with_fake_acoustid(monkeypatch, match=[]) + res = c.lookup_with_status(str(f)) + assert res["status"] == "no_match" + assert res["recordings"] == [] + + +def test_lookup_status_error_on_webservice(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"not audio") + c, fake = _client_with_fake_acoustid(monkeypatch) + # invalid key surfaces (old pyacoustid) as the bare "status: error" + monkeypatch.setattr(c, "_api_key", "testkey123") + + def _raise(*a, **k): + raise fake.WebServiceError("status: error") + fake.match = _raise + + res = c.lookup_with_status(str(f)) + assert res["status"] == "error" + assert res["invalid_key"] is True + + +def test_lookup_status_no_backend(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"not audio") + c, fake = _client_with_fake_acoustid(monkeypatch) + + def _raise(*a, **k): + raise fake.NoBackendError() + fake.match = _raise + + assert c.lookup_with_status(str(f))["status"] == "no_backend" + + +def test_lookup_status_unavailable_without_key(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"x") + monkeypatch.setattr(acc, "ACOUSTID_AVAILABLE", True) + c = AcoustIDClient() + c._api_key = "" # no key + assert c.lookup_with_status(str(f))["status"] == "unavailable" + + +# ── fingerprint_and_lookup keeps its dict-or-None contract ───────────────── + +def test_legacy_wrapper_returns_dict_on_match(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"not audio") + c, _ = _client_with_fake_acoustid(monkeypatch, match=[(0.9, "mbid-1", "T", "A")]) + out = c.fingerprint_and_lookup(str(f)) + assert out is not None and out["recordings"][0]["mbid"] == "mbid-1" + + +def test_legacy_wrapper_returns_none_on_error(tmp_path, monkeypatch): + f = tmp_path / "a.bin"; f.write_bytes(b"not audio") + c, fake = _client_with_fake_acoustid(monkeypatch) + + def _raise(*a, **k): + raise fake.WebServiceError("status: error") + fake.match = _raise + assert c.fingerprint_and_lookup(str(f)) is None + + +# ── verify_audio_file: ERROR vs SKIP ─────────────────────────────────────── + +def _verifier_with_lookup(result): + v = AcoustIDVerification() + client = MagicMock() + client.is_available.return_value = (True, "ready") + client.lookup_with_status.return_value = result + v.acoustid_client = client + return v + + +def test_verify_reports_error_for_api_error(): + v = _verifier_with_lookup({"status": "error", "recordings": [], "error": "AcoustID API error: invalid"}) + result, msg = v.verify_audio_file("/x.flac", "Song", "Artist") + assert result == VerificationResult.ERROR + assert "error" in msg.lower() or "invalid" in msg.lower() + + +def test_verify_reports_skip_for_no_match(): + v = _verifier_with_lookup({"status": "no_match", "recordings": [], "error": "Track not found in AcoustID database"}) + result, msg = v.verify_audio_file("/x.flac", "Song", "Artist") + assert result == VerificationResult.SKIP + assert "no match" in msg.lower() or "not found" in msg.lower() + + +# ── test_api_key: invalid key reported as invalid ────────────────────────── + +def _api_response(payload): + r = MagicMock() + r.json.return_value = payload + return r + + +def test_test_api_key_invalid_when_code_4(monkeypatch): + c = AcoustIDClient() + c._api_key = "badkey" + with patch("requests.get", + return_value=_api_response({"status": "error", "error": {"code": 4, "message": "invalid API key"}})): + ok, msg = c.test_api_key() + assert ok is False + assert "invalid" in msg.lower() + + +def test_test_api_key_valid_when_accepted(monkeypatch): + c = AcoustIDClient() + c._api_key = "goodkey" + # A non-key error (e.g. bad dummy fingerprint) means the key was accepted. + with patch("requests.get", + return_value=_api_response({"status": "error", "error": {"code": 3, "message": "invalid fingerprint"}})): + ok, msg = c.test_api_key() + assert ok is True + assert "valid" in msg.lower() diff --git a/tests/test_acoustid_skip_logic.py b/tests/test_acoustid_skip_logic.py index 272ca7d7..84c92017 100644 --- a/tests/test_acoustid_skip_logic.py +++ b/tests/test_acoustid_skip_logic.py @@ -52,7 +52,7 @@ def verifier(monkeypatch): def is_available(self): return True, 'available' - def fingerprint_and_lookup(self, path): + def lookup_with_status(self, path): # Each test injects its own desired return value via # monkeypatch on this method; default is empty. return None @@ -62,8 +62,8 @@ def verifier(monkeypatch): def _stub_lookup(verifier, *, recordings, best_score): - """Make `fingerprint_and_lookup` return a fabricated AcoustID result.""" - verifier.acoustid_client.fingerprint_and_lookup = lambda path: { + """Make `lookup_with_status` return a fabricated AcoustID result.""" + verifier.acoustid_client.lookup_with_status = lambda path: { 'recordings': recordings, 'best_score': best_score, 'recording_mbids': [r.get('id') for r in recordings if r.get('id')], diff --git a/tests/test_acoustid_version_mismatch.py b/tests/test_acoustid_version_mismatch.py index 9d542763..60161c20 100644 --- a/tests/test_acoustid_version_mismatch.py +++ b/tests/test_acoustid_version_mismatch.py @@ -38,7 +38,7 @@ def verifier(): def is_available(self): return True, "available" - def fingerprint_and_lookup(self, path): + def lookup_with_status(self, path): return None # tests inject via _stub_lookup below v.acoustid_client = _StubClient() @@ -46,7 +46,7 @@ def verifier(): def _stub_lookup(verifier, *, recordings, best_score=0.95): - verifier.acoustid_client.fingerprint_and_lookup = lambda path: { + verifier.acoustid_client.lookup_with_status = lambda path: { "recordings": recordings, "best_score": best_score, "recording_mbids": [r.get("id") for r in recordings if r.get("id")],