Files
MoviePilot/tests/test_lrclib_module.py

204 lines
6.6 KiB
Python

from app.core.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_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