mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
fix(acoustid): accept non-fatal fpcalc output (#6362)
This commit is contained in:
@@ -20,6 +20,8 @@ class AcoustIdModule(_ModuleBase):
|
|||||||
"""通过 Chromaprint 本地指纹和 AcoustID API 识别 MusicBrainz Recording ID。"""
|
"""通过 Chromaprint 本地指纹和 AcoustID API 识别 MusicBrainz Recording ID。"""
|
||||||
|
|
||||||
_base_url = "https://api.acoustid.org/v2/lookup"
|
_base_url = "https://api.acoustid.org/v2/lookup"
|
||||||
|
# 退出码 3 表示解码期间出现非致命错误,结果仍须通过 JSON 内容校验。
|
||||||
|
_usable_fpcalc_returncodes = frozenset({0, 3})
|
||||||
_minimum_score = 0.9
|
_minimum_score = 0.9
|
||||||
_request_interval = 0.34
|
_request_interval = 0.34
|
||||||
_fingerprint_timeout = 60
|
_fingerprint_timeout = 60
|
||||||
@@ -196,7 +198,7 @@ class AcoustIdModule(_ModuleBase):
|
|||||||
except (OSError, subprocess.TimeoutExpired) as err:
|
except (OSError, subprocess.TimeoutExpired) as err:
|
||||||
logger.warning(f"生成音频指纹失败:{path} - {err}")
|
logger.warning(f"生成音频指纹失败:{path} - {err}")
|
||||||
return None
|
return None
|
||||||
if result.returncode != 0:
|
if result.returncode not in self._usable_fpcalc_returncodes:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"生成音频指纹失败:{path} - fpcalc 退出码 {result.returncode}"
|
f"生成音频指纹失败:{path} - fpcalc 退出码 {result.returncode}"
|
||||||
)
|
)
|
||||||
@@ -235,7 +237,7 @@ class AcoustIdModule(_ModuleBase):
|
|||||||
except OSError as err:
|
except OSError as err:
|
||||||
logger.warning(f"生成音频指纹失败:{path} - {err}")
|
logger.warning(f"生成音频指纹失败:{path} - {err}")
|
||||||
return None
|
return None
|
||||||
if process.returncode != 0:
|
if process.returncode not in self._usable_fpcalc_returncodes:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"生成音频指纹失败:{path} - fpcalc 退出码 {process.returncode}"
|
f"生成音频指纹失败:{path} - fpcalc 退出码 {process.returncode}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,46 @@ def test_identify_music_by_fingerprint_queries_acoustid_and_caches_result(
|
|||||||
assert response.closed is True
|
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():
|
def test_select_recording_id_requires_high_score_and_valid_uuid():
|
||||||
"""低置信结果和异常外部 ID 不得进入 MusicBrainz 详情查询。"""
|
"""低置信结果和异常外部 ID 不得进入 MusicBrainz 详情查询。"""
|
||||||
payload = {
|
payload = {
|
||||||
@@ -190,9 +230,9 @@ def test_async_identify_music_by_fingerprint_uses_async_process_and_http(
|
|||||||
module._fpcalc_path = "/usr/bin/fpcalc"
|
module._fpcalc_path = "/usr/bin/fpcalc"
|
||||||
|
|
||||||
class FakeProcess:
|
class FakeProcess:
|
||||||
"""模拟已成功执行的异步 fpcalc 子进程。"""
|
"""模拟返回有效指纹并报告非致命解码错误的 fpcalc 子进程。"""
|
||||||
|
|
||||||
returncode = 0
|
returncode = 3
|
||||||
|
|
||||||
async def communicate(self):
|
async def communicate(self):
|
||||||
"""返回 fpcalc JSON 标准输出和空错误输出。"""
|
"""返回 fpcalc JSON 标准输出和空错误输出。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user