diff --git a/app/modules/acoustid/__init__.py b/app/modules/acoustid/__init__.py index 09fe79002..c4862682b 100644 --- a/app/modules/acoustid/__init__.py +++ b/app/modules/acoustid/__init__.py @@ -20,6 +20,8 @@ class AcoustIdModule(_ModuleBase): """通过 Chromaprint 本地指纹和 AcoustID API 识别 MusicBrainz Recording ID。""" _base_url = "https://api.acoustid.org/v2/lookup" + # 退出码 3 表示解码期间出现非致命错误,结果仍须通过 JSON 内容校验。 + _usable_fpcalc_returncodes = frozenset({0, 3}) _minimum_score = 0.9 _request_interval = 0.34 _fingerprint_timeout = 60 @@ -196,7 +198,7 @@ class AcoustIdModule(_ModuleBase): except (OSError, subprocess.TimeoutExpired) as err: logger.warning(f"生成音频指纹失败:{path} - {err}") return None - if result.returncode != 0: + if result.returncode not in self._usable_fpcalc_returncodes: logger.warning( f"生成音频指纹失败:{path} - fpcalc 退出码 {result.returncode}" ) @@ -235,7 +237,7 @@ class AcoustIdModule(_ModuleBase): except OSError as err: logger.warning(f"生成音频指纹失败:{path} - {err}") return None - if process.returncode != 0: + if process.returncode not in self._usable_fpcalc_returncodes: logger.warning( f"生成音频指纹失败:{path} - fpcalc 退出码 {process.returncode}" ) diff --git a/tests/test_acoustid_module.py b/tests/test_acoustid_module.py index 55bd2b6fd..290f19029 100644 --- a/tests/test_acoustid_module.py +++ b/tests/test_acoustid_module.py @@ -93,6 +93,46 @@ def test_identify_music_by_fingerprint_queries_acoustid_and_caches_result( assert response.closed is True +def test_generate_fingerprint_accepts_valid_output_on_nonfatal_exit( + tmp_path, + monkeypatch, +): + """fpcalc 报告非致命解码错误时仍应使用通过校验的指纹。""" + audio_path = tmp_path / "track.mp3" + audio_path.write_bytes(b"audio") + module = AcoustIdModule() + module._fpcalc_path = "/usr/bin/fpcalc" + monkeypatch.setattr( + "app.modules.acoustid.subprocess.run", + Mock(return_value=SimpleNamespace( + returncode=3, + stdout=json.dumps({ + "duration": 243.4, + "fingerprint": "AQADtM...", + }), + )), + ) + + assert module._generate_fingerprint(audio_path) == (243, "AQADtM...") + + +def test_generate_fingerprint_rejects_invalid_output_on_nonfatal_exit( + tmp_path, + monkeypatch, +): + """非致命退出码不能绕过时长和指纹内容校验。""" + audio_path = tmp_path / "track.mp3" + audio_path.write_bytes(b"audio") + module = AcoustIdModule() + module._fpcalc_path = "/usr/bin/fpcalc" + monkeypatch.setattr( + "app.modules.acoustid.subprocess.run", + Mock(return_value=SimpleNamespace(returncode=3, stdout="{}")), + ) + + assert module._generate_fingerprint(audio_path) is None + + def test_select_recording_id_requires_high_score_and_valid_uuid(): """低置信结果和异常外部 ID 不得进入 MusicBrainz 详情查询。""" payload = { @@ -190,9 +230,9 @@ def test_async_identify_music_by_fingerprint_uses_async_process_and_http( module._fpcalc_path = "/usr/bin/fpcalc" class FakeProcess: - """模拟已成功执行的异步 fpcalc 子进程。""" + """模拟返回有效指纹并报告非致命解码错误的 fpcalc 子进程。""" - returncode = 0 + returncode = 3 async def communicate(self): """返回 fpcalc JSON 标准输出和空错误输出。"""