fix(lyrics): normalize LRCLIB music metadata

This commit is contained in:
jxxghp
2026-10-05 13:10:10 +08:00
parent 22704e9224
commit fa5fa33db1
5 changed files with 93 additions and 15 deletions

View File

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

View File

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

View File

@@ -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 等基线均为零 |

View File

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

View File

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