From fa5fa33db149d98549ade8ceb9edc61e7706635b Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 5 Oct 2026 13:10:10 +0800 Subject: [PATCH] fix(lyrics): normalize LRCLIB music metadata --- app/modules/lrclib/__init__.py | 32 +++++---- docs/architecture-overview.md | 2 +- docs/refactor/optimization-checklist.md | 2 +- .../architecture/dependency-baseline.json | 6 +- tests/test_lrclib_module.py | 66 +++++++++++++++++++ 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/app/modules/lrclib/__init__.py b/app/modules/lrclib/__init__.py index 02d3601a9..72255dbba 100644 --- a/app/modules/lrclib/__init__.py +++ b/app/modules/lrclib/__init__.py @@ -6,6 +6,7 @@ from typing import Any, Optional, Tuple, Union from app.adapters.network.http import RequestUtils from app.domain.context import MusicInfo, MusicLyrics from app.domain.meta.metamusic import MetaMusic +from app.foundation.text import convert as zhconv_convert from app.modules import _ModuleBase from app.runtime.cache import cached from app.runtime.log import logger @@ -70,12 +71,15 @@ class LrclibModule(_ModuleBase): if not title or not artist: return None + query_title = self._query_text(title) + query_artist = self._query_text(artist) + query_album = self._query_text(album) exact_params: dict[str, Any] = { - "track_name": title, - "artist_name": artist, + "track_name": query_title, + "artist_name": query_artist, } - if album: - exact_params["album_name"] = album + if query_album: + exact_params["album_name"] = query_album if duration: exact_params["duration"] = duration payload = self._request_json("/api/get", params=exact_params) @@ -84,16 +88,16 @@ class LrclibModule(_ModuleBase): results = self._request_json( "/api/search", params={ - "track_name": title, - "artist_name": artist, - **({"album_name": album} if album else {}), + "track_name": query_title, + "artist_name": query_artist, + **({"album_name": query_album} if query_album else {}), }, ) payload = self._select_result( results if isinstance(results, list) else [], - title=title, - artist=artist, - album=album, + title=query_title, + artist=query_artist, + album=query_album, duration=duration, ) match_score = 90 @@ -146,7 +150,13 @@ class LrclibModule(_ModuleBase): @classmethod def _normalize_text(cls, value: Any) -> str: """移除大小写、标点和空白差异,生成歌词匹配文本。""" - return cls._match_pattern.sub("", str(value or "").casefold()) + simplified = zhconv_convert(str(value or "").casefold(), "zh-hans") + return cls._match_pattern.sub("", str(simplified)) + + @staticmethod + def _query_text(value: str) -> str: + """将歌词查询字段统一为简体,适配 LRCLIB 的主流索引写法。""" + return str(zhconv_convert(value, "zh-hans")).strip() @staticmethod def _compatible_text(expected: str, candidate: str) -> bool: diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 1718b7614..5f04d9669 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -763,7 +763,7 @@ flowchart LR | 指标 | 当前值 | |---|---:| | Python 模块 | 1105 | -| 内部导入边 | 9,316 | +| 内部导入边 | 9,318 | | 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) | | Application / Chain 具体 Adapter 直连 | 0 / 0 | | Direct egress | 49(债务已清零,49 条精确 containment) | diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md index 1ea646db7..c8f1f2bb0 100644 --- a/docs/refactor/optimization-checklist.md +++ b/docs/refactor/optimization-checklist.md @@ -98,7 +98,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁 | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 1105 / 9,316 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档 | +| 宿主 Python 模块 / 内部依赖边 | 1105 / 9,318 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档 | | 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 | | 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | | Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 8370a42ea..79d4b7acb 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -941,8 +941,8 @@ "runtime_only": true } }, - "edge_count": 9316, - "edge_sha256": "67e1155ee7b6151823fb42cabbe48a5dc7ec611d88165668c7b0379a449931e3", + "edge_count": 9318, + "edge_sha256": "b4db68c1bb06095ba35d2c6e96fd42f7255d40109e39f5a7077fa1de9b48a2d2", "edges": [ "app -> app.foundation", "app -> app.foundation.environment", @@ -7659,6 +7659,8 @@ "app.modules.lrclib -> app.domain.context", "app.modules.lrclib -> app.domain.meta", "app.modules.lrclib -> app.domain.meta.metamusic", + "app.modules.lrclib -> app.foundation", + "app.modules.lrclib -> app.foundation.text", "app.modules.lrclib -> app.modules", "app.modules.lrclib -> app.runtime", "app.modules.lrclib -> app.runtime.cache", diff --git a/tests/test_lrclib_module.py b/tests/test_lrclib_module.py index da39ca904..deaade8ab 100644 --- a/tests/test_lrclib_module.py +++ b/tests/test_lrclib_module.py @@ -113,6 +113,72 @@ def test_music_lyrics_search_fallback_rejects_wrong_duration(monkeypatch) -> Non assert lyrics.extension == ".txt" +def test_music_lyrics_queries_simplified_metadata(monkeypatch) -> None: + """繁体音乐信息应使用简体标题、艺术家和专辑查询 LRCLIB。""" + module = LrclibModule() + requested = [] + + def fake_request(path, params=None): + """记录归一后的精确查询参数并返回歌词。""" + requested.append((path, params)) + return { + "id": 3, + "plainLyrics": "安静的歌词", + } + + monkeypatch.setattr(module, "_request_json", fake_request) + lyrics = module.music_lyrics( + MusicInfo( + title="安靜", + artists=["周杰倫"], + album="范特西", + duration=334, + ) + ) + + assert lyrics is not None + assert lyrics.provider_id == "3" + assert requested == [ + ( + "/api/get", + { + "track_name": "安静", + "artist_name": "周杰伦", + "album_name": "范特西", + "duration": 334, + }, + ) + ] + + +def test_music_lyrics_search_matches_traditional_candidate(monkeypatch) -> None: + """搜索结果使用繁体艺术家或标题时仍应匹配简体查询身份。""" + module = LrclibModule() + + def fake_request(path, params=None): + """模拟精确接口未命中和繁体搜索结果。""" + if path == "/api/get": + return {} + return [ + { + "id": 4, + "trackName": "安靜", + "artistName": "周杰倫", + "albumName": "范特西", + "duration": 334, + "plainLyrics": "安静的歌词", + } + ] + + monkeypatch.setattr(module, "_request_json", fake_request) + lyrics = module.music_lyrics( + MusicInfo(title="安靜", artists=["周杰倫"], album="范特西", duration=334) + ) + + assert lyrics is not None + assert lyrics.provider_id == "4" + + def test_request_json_honors_retry_after_once(monkeypatch) -> None: """LRCLIB 返回带 Retry-After 的过载响应时应等待并串行重试一次。""" responses = iter(