Files
MoviePilot/tests/test_lrclib_module.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

310 lines
8.8 KiB
Python

"""
验证 LRCLIB 歌词查询、匹配和返回值映射。
"""
from app.domain.context import MusicInfo
from app.modules.lrclib import LrclibModule
class _FakeLrclibResponse:
"""
模拟 LRCLIB HTTP 响应,供解析、缓存和限流测试复用。
"""
def __init__(self, payload, status_code=200, headers=None):
self._payload = payload
self.status_code = status_code
self.headers = headers or {}
self.text = ""
self.closed = False
def json(self):
"""
返回预设 JSON 负载。
"""
return self._payload
def close(self):
"""
记录响应已关闭。
"""
self.closed = True
def __bool__(self):
"""
复现 requests.Response 对 4xx/5xx 响应返回 False 的行为。
"""
return self.status_code < 400
def test_music_lyrics_prefers_exact_signature_and_synced_lyrics(monkeypatch) -> None:
"""
元数据完整时应调用精确接口并优先返回同步歌词。
"""
module = LrclibModule()
requested = []
def fake_request(path, params=None):
"""
记录精确查询参数并返回同步歌词。
"""
requested.append((path, params))
return {
"id": 3396226,
"instrumental": False,
"plainLyrics": "plain",
"syncedLyrics": "[00:01.00]synced",
}
monkeypatch.setattr(module, "_request_json", fake_request)
lyrics = module.music_lyrics(
MusicInfo(
title="I Want to Live",
artists=["Borislav Slavov"],
album="Baldur's Gate 3",
duration=233,
)
)
assert lyrics is not None
assert lyrics.provider == "lrclib"
assert lyrics.provider_id == "3396226"
assert lyrics.content == "[00:01.00]synced"
assert lyrics.extension == ".lrc"
assert requested == [
(
"/api/get",
{
"track_name": "I Want to Live",
"artist_name": "Borislav Slavov",
"album_name": "Baldur's Gate 3",
"duration": 233,
},
)
]
def test_music_lyrics_search_fallback_rejects_wrong_duration(monkeypatch) -> None:
"""
精确接口未命中后只能选取标题、艺术家和时长均可信的搜索结果。
"""
module = LrclibModule()
def fake_request(path, params=None):
"""
精确查询返回未命中,搜索返回一条错误版本和一条正确版本。
"""
if path == "/api/get":
return {}
return [
{
"id": 1,
"trackName": "晴天",
"artistName": "周杰伦",
"albumName": "演唱会",
"duration": 310,
"plainLyrics": "wrong",
},
{
"id": 2,
"trackName": "晴天",
"artistName": "周杰伦",
"albumName": "叶惠美",
"duration": 269,
"plainLyrics": "correct",
},
]
monkeypatch.setattr(module, "_request_json", fake_request)
lyrics = module.music_lyrics(
MusicInfo(
title="晴天",
artists=["周杰伦"],
album="叶惠美",
duration=270,
)
)
assert lyrics is not None
assert lyrics.provider_id == "2"
assert lyrics.content == "correct"
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(
[
_FakeLrclibResponse(None, status_code=503, headers={"Retry-After": "2"}),
_FakeLrclibResponse([{"id": 1}], status_code=200),
]
)
sleeps = []
monkeypatch.setattr(LrclibModule, "_request_once", lambda *_args, **_kwargs: next(responses))
monkeypatch.setattr("app.modules.lrclib.time.sleep", lambda seconds: sleeps.append(seconds))
LrclibModule._request_json.cache_clear()
result = LrclibModule._request_json(
"/api/search",
params={"track_name": "晴天", "artist_name": "周杰伦"},
)
assert result == [{"id": 1}]
assert sleeps == [2.0]
def test_request_json_retry_network_failure_is_not_cached(monkeypatch) -> None:
"""
过载重试遇到网络失败时应安全返回且不把失败写入缓存。
"""
responses = iter(
[
_FakeLrclibResponse(None, status_code=429, headers={"Retry-After": "0"}),
None,
_FakeLrclibResponse([{"id": 3}], status_code=200),
]
)
monkeypatch.setattr(LrclibModule, "_request_once", lambda *_args, **_kwargs: next(responses))
monkeypatch.setattr("app.modules.lrclib.time.sleep", lambda _seconds: None)
LrclibModule._request_json.cache_clear()
params = {"track_name": "retry"}
first = LrclibModule._request_json("/api/search", params=params)
second = LrclibModule._request_json("/api/search", params=params)
assert first is None
assert second == [{"id": 3}]
def test_request_json_caches_not_found_response(monkeypatch) -> None:
"""
未匹配结果也应缓存,避免整库重复刮削持续请求同一首歌。
"""
calls = {"count": 0}
def fake_request(*_args, **_kwargs):
"""
记录请求次数并返回 404。
"""
calls["count"] += 1
return _FakeLrclibResponse(None, status_code=404)
monkeypatch.setattr(LrclibModule, "_request_once", fake_request)
LrclibModule._request_json.cache_clear()
first = LrclibModule._request_json("/api/get", params={"track_name": "missing"})
second = LrclibModule._request_json("/api/get", params={"track_name": "missing"})
assert first == second == {}
assert calls["count"] == 1
def test_request_json_caches_success_response(monkeypatch) -> None:
"""
成功歌词响应应进入有界 TLRU 缓存,相同签名只访问一次外部 API。
"""
calls = {"count": 0}
def fake_request(*_args, **_kwargs):
"""
记录请求次数并返回固定歌词。
"""
calls["count"] += 1
return _FakeLrclibResponse(
{"id": 2, "syncedLyrics": "[00:01.00]晴天"},
status_code=200,
)
monkeypatch.setattr(LrclibModule, "_request_once", fake_request)
LrclibModule._request_json.cache_clear()
params = {
"track_name": "晴天",
"artist_name": "周杰伦",
"album_name": "叶惠美",
"duration": 269,
}
first = LrclibModule._request_json("/api/get", params=params)
second = LrclibModule._request_json("/api/get", params=params)
assert first == second == {"id": 2, "syncedLyrics": "[00:01.00]晴天"}
assert calls["count"] == 1