mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 11:04:12 +08:00
634 lines
22 KiB
Python
634 lines
22 KiB
Python
import asyncio
|
||
from pathlib import Path
|
||
from unittest.mock import AsyncMock, Mock, patch
|
||
|
||
from app.chain.acoustid import AcoustIdChain
|
||
from app.chain.listenbrainz import ListenBrainzChain
|
||
from app.chain.media import MediaChain
|
||
from app.chain.musicbrainz import MusicBrainzChain
|
||
from app.chain.recommend import RecommendChain
|
||
from app.chain.search import SearchChain
|
||
from app.core.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
|
||
from app.core.meta import MetaMusic
|
||
from app.modules.musicbrainz import MusicBrainzModule
|
||
from app.schemas.types import MediaSource, MediaType
|
||
|
||
|
||
def test_parse_query_supports_artist_title_format():
|
||
"""艺术家与标题格式应拆分为结构化搜索条件。"""
|
||
meta = MetaMusic.parse_query(" 周杰伦 - 晴天 ")
|
||
|
||
assert meta.artists == ["周杰伦"]
|
||
assert meta.title == "晴天"
|
||
assert meta.org_string == " 周杰伦 - 晴天 "
|
||
|
||
|
||
def test_parse_query_keeps_plain_title():
|
||
"""普通文本应保留为歌曲或专辑标题。"""
|
||
meta = MetaMusic.parse_query(" Random Access Memories ")
|
||
|
||
assert meta.artists == []
|
||
assert meta.title == "Random Access Memories"
|
||
|
||
|
||
def test_parse_query_strips_quality_tokens_before_artist_split():
|
||
"""音质规格不应参与艺术家/曲名拆分,年份括号与格式后缀需剔除。"""
|
||
meta = MetaMusic.parse_query("毛阿敏 - 永遠是朋友(2000) - ALAC [16B-44.1kHz]")
|
||
|
||
assert meta.artists == ["毛阿敏"]
|
||
assert meta.title == "永遠是朋友"
|
||
assert meta.audio_format == "ALAC"
|
||
|
||
|
||
def test_parse_query_does_not_misplit_quality_only_tail():
|
||
"""无艺术家时格式规格段不能被误拆成曲名,曲名不能丢字。"""
|
||
meta = MetaMusic.parse_query("永遠是朋友(2000) - ALAC [16B-44.1kHz]")
|
||
|
||
assert meta.artists == []
|
||
assert meta.title == "永遠是朋友"
|
||
|
||
|
||
def test_parse_query_strips_trailing_artist_suffix():
|
||
"""曲名尾部重复的艺术家署名应被剥离,不作为曲名参与检索。"""
|
||
meta = MetaMusic.parse_query("毛阿敏 - 名人名曲-毛阿敏(2000)")
|
||
|
||
assert meta.artists == ["毛阿敏"]
|
||
assert meta.title == "名人名曲"
|
||
assert meta.year == 2000
|
||
|
||
meta = MetaMusic.parse_query("许茹芸 - 争奇斗艳演唱会实况 2 - 许茹芸 (1996)")
|
||
|
||
assert meta.artists == ["许茹芸"]
|
||
assert meta.title == "争奇斗艳演唱会实况 2"
|
||
|
||
|
||
def test_parse_query_keeps_non_artist_suffix():
|
||
"""曲名尾段与艺术家不一致时不应被误剥离。"""
|
||
meta = MetaMusic.parse_query("毛阿敏 - 思念 - 现场版")
|
||
|
||
assert meta.artists == ["毛阿敏"]
|
||
assert meta.title == "思念 - 现场版"
|
||
|
||
|
||
def test_build_site_keywords_searches_album_before_combined_artist():
|
||
"""专辑先按专辑名扩大召回,再用艺术家与专辑名组合词回退。"""
|
||
info = MusicInfo(
|
||
music_type="album",
|
||
title="Get Lucky",
|
||
artists=["Daft Punk"],
|
||
album="Random Access Memories",
|
||
)
|
||
|
||
assert SearchChain.music_site_keywords(info) == [
|
||
"Random Access Memories",
|
||
"Daft Punk Random Access Memories",
|
||
]
|
||
|
||
|
||
def test_build_site_keywords_searches_track_before_combined_artist():
|
||
"""单曲先按曲名扩大召回,且关键词不能混入所属专辑名。"""
|
||
info = MusicInfo(
|
||
music_type="recording",
|
||
title="Get Lucky",
|
||
artists=["Daft Punk"],
|
||
album="Random Access Memories",
|
||
)
|
||
|
||
assert SearchChain.music_site_keywords(info) == [
|
||
"Get Lucky",
|
||
"Daft Punk Get Lucky",
|
||
]
|
||
|
||
|
||
def test_album_resource_match_requires_selected_album_title():
|
||
"""专辑订阅只接受同时包含目标专辑名和艺术家的站点资源。"""
|
||
album = MusicInfo(
|
||
music_type="album",
|
||
title="Random Access Memories",
|
||
album="Random Access Memories",
|
||
artists=["Daft Punk"],
|
||
names=["Random-Access Memories"],
|
||
)
|
||
|
||
assert SearchChain.matches_music_resource(
|
||
album,
|
||
"Daft.Punk-Random.Access.Memories-2013-FLAC",
|
||
) is True
|
||
assert SearchChain.matches_music_resource(album, "Daft Punk - Discovery - FLAC") is False
|
||
|
||
|
||
def test_recording_resource_match_does_not_treat_album_name_as_track_alias():
|
||
"""单曲候选的兼容 names 即使包含专辑名,也不能让整专标题冒充目标单曲。"""
|
||
recording = MusicInfo(
|
||
music_type="recording",
|
||
title="Get Lucky",
|
||
album="Random Access Memories",
|
||
artists=["Daft Punk"],
|
||
names=["Get Lucky", "Random Access Memories"],
|
||
)
|
||
|
||
assert SearchChain.matches_music_resource(recording, "Daft Punk - Get Lucky FLAC") is True
|
||
assert SearchChain.matches_music_resource(
|
||
recording,
|
||
"Daft Punk - Random Access Memories FLAC",
|
||
) is False
|
||
|
||
|
||
def test_resource_match_requires_artist_when_target_artist_is_known():
|
||
"""同名作品很多,目标已知艺术家时资源标题也必须包含该艺术家。"""
|
||
recording = MusicInfo(
|
||
music_type="recording",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
)
|
||
|
||
assert SearchChain.matches_music_resource(recording, "周杰伦 - 晴天 FLAC") is True
|
||
assert SearchChain.matches_music_resource(recording, "其他艺人 - 晴天 FLAC") is False
|
||
assert SearchChain.matches_music_resource(recording, "晴天 FLAC") is False
|
||
|
||
|
||
def test_album_resource_match_combines_title_description_and_converts_traditional_chinese():
|
||
"""专辑名与艺术家可分处标题和副标题,繁简及干扰符号不影响匹配。"""
|
||
album = MusicInfo(
|
||
music_type="album",
|
||
title="永远是朋友",
|
||
album="永远是朋友",
|
||
artists=["周华健"],
|
||
)
|
||
|
||
assert SearchChain.matches_music_resource(
|
||
album,
|
||
"【永遠・是朋友】24bit/96kHz",
|
||
"專輯藝人:周華健;無損音樂",
|
||
) is True
|
||
|
||
|
||
def test_recording_resource_match_combines_title_description_and_converts_traditional_chinese():
|
||
"""单曲按曲名与艺术家匹配,并统一繁简及全角符号。"""
|
||
recording = MusicInfo(
|
||
music_type="recording",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
)
|
||
|
||
assert SearchChain.matches_music_resource(
|
||
recording,
|
||
"01.晴 天[FLAC]",
|
||
"演唱:周杰倫",
|
||
) is True
|
||
|
||
|
||
def test_music_resource_match_rejects_target_without_artist():
|
||
"""目标缺少艺术家时不能仅凭同名专辑或单曲放行。"""
|
||
recording = MusicInfo(music_type="recording", title="晴天")
|
||
|
||
assert SearchChain.matches_music_resource(recording, "周杰伦 - 晴天 FLAC") is False
|
||
|
||
|
||
def test_normalize_candidates_deduplicates_source_identity():
|
||
"""同一来源和媒体 ID 的音乐候选应只保留一次。"""
|
||
results = MediaChain.normalize_music_candidates(
|
||
[
|
||
MusicInfo(media_source="musicbrainz", media_id="recording-1", title="A"),
|
||
{
|
||
"type": "音乐",
|
||
"media_source": "musicbrainz",
|
||
"media_id": "recording-1",
|
||
"title": "A duplicate",
|
||
},
|
||
]
|
||
)
|
||
|
||
assert len(results) == 1
|
||
assert results[0].title == "A"
|
||
|
||
|
||
def test_normalize_candidates_keeps_different_entities_with_same_source_id():
|
||
"""同一来源 ID 在不同音乐实体命名空间下不能互相去重。"""
|
||
results = MediaChain.normalize_music_candidates(
|
||
[
|
||
MusicInfo(media_source="musicbrainz", media_id="shared-id", music_type="recording", title="Song"),
|
||
MusicInfo(media_source="musicbrainz", media_id="shared-id", music_type="album", title="Album"),
|
||
]
|
||
)
|
||
|
||
assert [item.music_type for item in results] == ["recording", "album"]
|
||
|
||
|
||
def test_normalize_candidates_deduplicates_metadata_without_id():
|
||
"""缺少来源 ID 时应按标题、艺术家和专辑去重。"""
|
||
results = MediaChain.normalize_music_candidates(
|
||
[
|
||
MusicInfo(title="One More Time", artists=["Daft Punk"], album="Discovery"),
|
||
MusicInfo(title=" one more time ", artists=["daft punk"], album="DISCOVERY"),
|
||
]
|
||
)
|
||
|
||
assert len(results) == 1
|
||
|
||
|
||
def test_to_meta_preserves_selected_identity():
|
||
"""候选转换后应保留下载和整理所需的标准身份。"""
|
||
info = MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="recording-1",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
album="叶惠美",
|
||
year=2003,
|
||
track_number=3,
|
||
)
|
||
|
||
meta = MetaMusic.from_music_info(info)
|
||
|
||
assert meta.media_source == "musicbrainz"
|
||
assert meta.media_id == "recording-1"
|
||
assert meta.artists == ["周杰伦"]
|
||
assert meta.album == "叶惠美"
|
||
assert meta.track_number == 3
|
||
|
||
|
||
def test_chart_converts_page_to_listenbrainz_offset(monkeypatch):
|
||
"""音乐榜单处理链应将页码转换为模块需要的偏移量。"""
|
||
chain = ListenBrainzChain()
|
||
requested = {}
|
||
|
||
def fake_run_module(method, **kwargs):
|
||
"""记录榜单模块调用并返回重复候选。"""
|
||
requested.update(method=method, **kwargs)
|
||
return [
|
||
MusicInfo(media_source="musicbrainz", media_id="recording-1", title="晴天"),
|
||
MusicInfo(media_source="musicbrainz", media_id="recording-1", title="晴天"),
|
||
]
|
||
|
||
monkeypatch.setattr(chain, "run_module", fake_run_module)
|
||
|
||
results = chain.music_chart(range_name="this_week", page=2, count=30)
|
||
|
||
assert requested == {
|
||
"method": "music_chart",
|
||
"range_name": "this_week",
|
||
"offset": 30,
|
||
"count": 30,
|
||
"entity": "recording",
|
||
}
|
||
assert len(results) == 2
|
||
|
||
|
||
def test_async_chart_applies_music_explore_filters(monkeypatch):
|
||
"""音乐探索应按收听次数、封面条件和升序设置筛选榜单。"""
|
||
chain = RecommendChain()
|
||
|
||
async def fake_music_chart(**kwargs):
|
||
"""返回包含不同热度和封面状态的榜单候选。"""
|
||
return [
|
||
MusicInfo(media_id="1", media_source="musicbrainz", title="A", listen_count=300),
|
||
MusicInfo(
|
||
media_id="2",
|
||
media_source="musicbrainz",
|
||
title="B",
|
||
listen_count=120,
|
||
cover_url="https://coverartarchive.org/release/2/front-500",
|
||
),
|
||
MusicInfo(
|
||
media_id="3",
|
||
media_source="musicbrainz",
|
||
title="C",
|
||
listen_count=240,
|
||
cover_url="https://coverartarchive.org/release/3/front-500",
|
||
),
|
||
]
|
||
|
||
source_chain = Mock()
|
||
source_chain.async_music_chart = AsyncMock(side_effect=fake_music_chart)
|
||
monkeypatch.setattr("app.chain.recommend.ListenBrainzChain", Mock(return_value=source_chain))
|
||
|
||
results = asyncio.run(
|
||
chain.async_music_chart(
|
||
range_name="this_month",
|
||
count=30,
|
||
sort_by="listen_count.asc",
|
||
min_listen_count=100,
|
||
with_cover=True,
|
||
)
|
||
)
|
||
|
||
assert [item.title for item in results] == ["B", "C"]
|
||
|
||
|
||
def test_musicbrainz_module_select_candidate_prefers_matching_audio_tags():
|
||
"""文件识别应优先选择标题、艺术家和专辑均匹配的 MusicBrainz 候选。"""
|
||
meta = MetaMusic(title="晴天", artists=["周杰伦"], album="叶惠美")
|
||
candidates = [
|
||
MusicInfo(media_source="musicbrainz", media_id="1", title="晴天", artists=["其他歌手"]),
|
||
MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="2",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
album="叶惠美",
|
||
),
|
||
]
|
||
|
||
selected = MusicBrainzModule._select_candidate(meta, candidates, media_source="musicbrainz")
|
||
|
||
assert selected is candidates[1]
|
||
|
||
|
||
def test_async_recognize_by_path_reads_local_audio_tags(tmp_path, monkeypatch):
|
||
"""本地音频识别应使用内嵌标签补全艺术家、专辑并提高封面候选命中率。"""
|
||
audio_path = tmp_path / "02. 眼泪成诗.m4a"
|
||
audio_path.write_bytes(b"audio")
|
||
meta = MetaMusic(
|
||
title="眼泪成诗",
|
||
artists=["孙燕姿"],
|
||
album="完美的一天",
|
||
track_number=2,
|
||
duration=221,
|
||
)
|
||
info = MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="recording-1",
|
||
title="眼泪成诗",
|
||
artists=["孙燕姿"],
|
||
album="完美的一天",
|
||
cover_url="https://coverartarchive.org/release-group/album-1/front-500",
|
||
)
|
||
chain = MediaChain()
|
||
recognize = AsyncMock(return_value=info)
|
||
filename_meta = MetaMusic(title="02. 眼泪成诗")
|
||
monkeypatch.setattr(
|
||
"app.chain.media.AudioMetadataHelper.read_evidence",
|
||
Mock(return_value=(meta, meta, filename_meta)),
|
||
)
|
||
monkeypatch.setattr(
|
||
AcoustIdChain,
|
||
"async_identify_music_by_fingerprint",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(chain, "async_recognize_media", recognize)
|
||
|
||
recognized_meta, recognized_info = asyncio.run(
|
||
chain.async_recognize_music_by_path(audio_path)
|
||
)
|
||
|
||
assert recognized_meta is meta
|
||
assert recognized_info is info
|
||
recognize.assert_awaited_once_with(
|
||
meta=meta,
|
||
media_source=None,
|
||
music_type="recording",
|
||
)
|
||
|
||
|
||
def test_media_chain_default_recognition_only_queries_musicbrainz(monkeypatch):
|
||
"""自动音乐识别只应调用 MusicBrainz 主数据源。"""
|
||
chain = MediaChain()
|
||
meta = MetaMusic(
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
album="叶惠美",
|
||
year=2003,
|
||
)
|
||
expected = MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="mb-1",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
album="叶惠美",
|
||
year=2003,
|
||
)
|
||
recognize_source = Mock(return_value=expected)
|
||
monkeypatch.setattr(chain, "recognize_music_from_source", recognize_source)
|
||
|
||
with patch("app.chain.MoviePilotServerHelper.report_recognize_share"):
|
||
result = chain.recognize_media(meta=meta)
|
||
|
||
assert result is expected
|
||
recognize_source.assert_called_once_with(
|
||
media_source=MediaSource.MusicBrainz,
|
||
meta=meta,
|
||
cache=True,
|
||
music_type="recording",
|
||
)
|
||
|
||
|
||
def test_recognize_from_source_selects_only_declared_music_source_chain(monkeypatch):
|
||
"""单源识别只允许调用声明该音乐来源的模块,忽略同接口影视模块。"""
|
||
chain = MediaChain()
|
||
meta = MetaMusic(title="晴天", artists=["周杰伦"])
|
||
expected = MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="recording-1",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
)
|
||
source_chain = Mock()
|
||
source_chain.recognize_music.return_value = expected
|
||
monkeypatch.setattr(chain, "_music_source_chain", Mock(return_value=source_chain))
|
||
|
||
result = chain.recognize_music_from_source(
|
||
media_source="musicbrainz",
|
||
meta=meta,
|
||
cache=True,
|
||
)
|
||
|
||
assert result is expected
|
||
source_chain.recognize_music.assert_called_once_with(
|
||
meta=meta,
|
||
media_id=None,
|
||
cache=True,
|
||
music_type=None,
|
||
)
|
||
|
||
|
||
def test_default_recognition_does_not_fallback_after_musicbrainz_miss(monkeypatch):
|
||
"""MusicBrainz 未命中时自动识别不得继续请求其它音乐来源。"""
|
||
chain = MediaChain()
|
||
meta = MetaMusic(title="晴天", artists=["周杰伦"])
|
||
recognize_source = Mock(return_value=None)
|
||
monkeypatch.setattr(chain, "recognize_music_from_source", recognize_source)
|
||
|
||
with patch(
|
||
"app.chain.MoviePilotServerHelper.query_recognize_share",
|
||
return_value=None,
|
||
):
|
||
assert chain.recognize_media(meta=meta) is None
|
||
recognize_source.assert_called_once()
|
||
assert recognize_source.call_args.kwargs["media_source"] == MediaSource.MusicBrainz
|
||
|
||
|
||
def test_async_default_recognition_only_queries_musicbrainz(monkeypatch):
|
||
"""异步自动音乐识别也只应调用 MusicBrainz 主数据源。"""
|
||
chain = MediaChain()
|
||
meta = MetaMusic(title="晴天", artists=["周杰伦"])
|
||
expected = MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="recording-1",
|
||
title="晴天",
|
||
artists=["周杰伦"],
|
||
)
|
||
recognize_source = AsyncMock(return_value=expected)
|
||
monkeypatch.setattr(chain, "async_recognize_music_from_source", recognize_source)
|
||
|
||
with patch(
|
||
"app.chain.MoviePilotServerHelper.async_report_recognize_share",
|
||
new=AsyncMock(),
|
||
):
|
||
result = asyncio.run(chain.async_recognize_media(meta=meta))
|
||
|
||
assert result is expected
|
||
recognize_source.assert_awaited_once_with(
|
||
media_source=MediaSource.MusicBrainz,
|
||
meta=meta,
|
||
cache=True,
|
||
music_type="recording",
|
||
)
|
||
|
||
|
||
def test_musicbrainz_source_chain_calls_module_async_method(monkeypatch):
|
||
"""单源异步识别必须直接等待模块异步入口。"""
|
||
chain = MusicBrainzChain()
|
||
expected = MusicInfo(
|
||
media_source="musicbrainz",
|
||
media_id="recording-1",
|
||
title="晴天",
|
||
)
|
||
async_run_module = AsyncMock(return_value=expected)
|
||
run_module = Mock(side_effect=AssertionError("异步识别不应调用同步模块方法"))
|
||
monkeypatch.setattr(chain, "async_run_module", async_run_module)
|
||
monkeypatch.setattr(chain, "run_module", run_module)
|
||
|
||
result = asyncio.run(chain.async_recognize_music(
|
||
meta=MetaMusic(title="晴天"), cache=True
|
||
))
|
||
|
||
assert result is expected
|
||
async_run_module.assert_awaited_once()
|
||
run_module.assert_not_called()
|
||
|
||
|
||
def test_async_identify_by_fingerprint_uses_async_module_contract(monkeypatch):
|
||
"""指纹异步链路应请求模块的异步方法名。"""
|
||
chain = AcoustIdChain()
|
||
async_run_module = AsyncMock(return_value="recording-1")
|
||
monkeypatch.setattr(chain, "async_run_module", async_run_module)
|
||
|
||
result = asyncio.run(chain.async_identify_music_by_fingerprint("/music/track.flac"))
|
||
|
||
assert result == "recording-1"
|
||
async_run_module.assert_awaited_once_with(
|
||
"async_identify_music_by_fingerprint",
|
||
path=Path("/music/track.flac"),
|
||
)
|
||
|
||
|
||
def test_async_chart_forwards_album_entity(monkeypatch):
|
||
"""热门专辑探索应把实体类型透传给 ListenBrainz 榜单模块。"""
|
||
chain = ListenBrainzChain()
|
||
requested = {}
|
||
|
||
async def fake_async_run_module(method, **kwargs):
|
||
"""记录榜单请求参数并返回一个专辑候选。"""
|
||
requested.update(method=method, **kwargs)
|
||
return [
|
||
MusicInfo(
|
||
media_id="release-group-1",
|
||
media_source="musicbrainz",
|
||
music_type="album",
|
||
title="ARIRANG",
|
||
listen_count=10,
|
||
)
|
||
]
|
||
|
||
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
||
|
||
results = asyncio.run(chain.async_music_chart(range_name="week", page=3, count=20, entity="album"))
|
||
|
||
assert requested["method"] == "music_chart"
|
||
assert requested["entity"] == "album"
|
||
assert requested["offset"] == 40
|
||
assert results[0].music_type == "album"
|
||
|
||
|
||
def test_async_fresh_releases_keeps_official_order(monkeypatch):
|
||
"""新发行探索应保留官方排序,只按封面条件过滤。"""
|
||
chain = RecommendChain()
|
||
requested = {}
|
||
|
||
async def fake_fresh_releases(**kwargs):
|
||
"""记录新发行请求参数并返回带封面与不带封面的候选。"""
|
||
requested.update(**kwargs)
|
||
return [
|
||
MusicInfo(media_id="b", media_source="musicbrainz", music_type="album", title="B"),
|
||
MusicInfo(
|
||
media_id="a",
|
||
media_source="musicbrainz",
|
||
music_type="album",
|
||
title="A",
|
||
cover_url="https://coverartarchive.org/release/a/front-500",
|
||
),
|
||
]
|
||
|
||
source_chain = Mock()
|
||
source_chain.async_music_fresh_releases = AsyncMock(side_effect=fake_fresh_releases)
|
||
monkeypatch.setattr("app.chain.recommend.ListenBrainzChain", Mock(return_value=source_chain))
|
||
|
||
results = asyncio.run(
|
||
chain.async_music_fresh_releases(
|
||
days=30, sort="release_name", page=2, count=10, with_cover=True
|
||
)
|
||
)
|
||
|
||
assert requested["page"] == 2
|
||
assert requested["sort"] == "release_name"
|
||
assert [item.title for item in results] == ["A"]
|
||
|
||
|
||
def test_async_artist_related_preserves_source_results(monkeypatch):
|
||
"""关联艺术家来源链应保留来源返回顺序和实体。"""
|
||
chain = MusicBrainzChain()
|
||
|
||
async def fake_async_run_module(method, **kwargs):
|
||
"""返回重复的关联艺术家候选。"""
|
||
assert method == "music_artist_related"
|
||
return [
|
||
MusicArtistInfo(media_source="musicbrainz", media_id="artist-1", name="Brian May"),
|
||
MusicArtistInfo(media_source="musicbrainz", media_id="artist-1", name="Brian May"),
|
||
MusicArtistInfo(media_source="musicbrainz", media_id="artist-2", name="John Deacon"),
|
||
]
|
||
|
||
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
||
|
||
results = asyncio.run(chain.async_get_music_artist_related(media_id="artist-0"))
|
||
|
||
assert [item.media_id for item in results] == ["artist-1", "artist-1", "artist-2"]
|
||
|
||
|
||
def test_async_album_returns_source_chain_result(monkeypatch):
|
||
"""媒体链应按来源和 ID 返回来源链提供的标准专辑对象。"""
|
||
chain = MediaChain()
|
||
|
||
async def fake_get_album(media_id):
|
||
"""模拟插件模块以字典形式返回专辑详情。"""
|
||
assert media_id == "release-group-1"
|
||
return MusicAlbumInfo(
|
||
media_source="musicbrainz",
|
||
media_id="release-group-1",
|
||
title="A Night at the Opera",
|
||
artists=["Queen"],
|
||
release_date="1975-11-21",
|
||
)
|
||
|
||
source_chain = Mock()
|
||
source_chain.async_get_music_album = AsyncMock(side_effect=fake_get_album)
|
||
monkeypatch.setattr(chain, "_music_source_chain", Mock(return_value=source_chain))
|
||
|
||
album = asyncio.run(chain.async_get_music_album(
|
||
media_source="musicbrainz", media_id="release-group-1"
|
||
))
|
||
|
||
assert album is not None
|
||
assert album.year == 1975
|
||
assert album.artists == ["Queen"]
|