mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 19:14:01 +08:00
- 音乐识别统一走 recognize_media,移除 MusicChain.recognize/async_recognize 及 /music/search 独立端点 - 音乐刮削收拢到 MediaChain.scrape_music_metadata,消除 MusicChain 对 MediaChain 的嵌套引用 - MetaInfo/MetaInfoPath 增加 force_video 参数,影视附加音轨保留季集归属 - 整理、订阅、搜索、API 层音乐识别入口全部统一
287 lines
9.3 KiB
Python
287 lines
9.3 KiB
Python
from app.chain.music import MusicChain
|
|
from app.core.meta import MetaMusic
|
|
from app.core.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
|
|
from app.modules.musicbrainz import MusicBrainzModule
|
|
|
|
|
|
def test_parse_query_supports_artist_title_format():
|
|
"""艺术家与标题格式应拆分为结构化搜索条件。"""
|
|
meta = MusicChain.parse_query(" 周杰伦 - 晴天 ")
|
|
|
|
assert meta.artists == ["周杰伦"]
|
|
assert meta.title == "晴天"
|
|
assert meta.org_string == " 周杰伦 - 晴天 "
|
|
|
|
|
|
def test_parse_query_keeps_plain_title():
|
|
"""普通文本应保留为歌曲或专辑标题。"""
|
|
meta = MusicChain.parse_query(" Random Access Memories ")
|
|
|
|
assert meta.artists == []
|
|
assert meta.title == "Random Access Memories"
|
|
|
|
|
|
def test_build_site_keywords_prefers_artist_album():
|
|
"""站点关键词应优先使用艺术家和专辑组合。"""
|
|
info = MusicInfo(
|
|
title="Get Lucky",
|
|
artists=["Daft Punk"],
|
|
album="Random Access Memories",
|
|
)
|
|
|
|
assert MusicChain.build_site_keywords(info) == [
|
|
"Daft Punk Random Access Memories",
|
|
"Daft Punk Get Lucky",
|
|
"Random Access Memories",
|
|
"Get Lucky",
|
|
]
|
|
|
|
|
|
def test_normalize_candidates_deduplicates_source_identity():
|
|
"""同一来源和媒体 ID 的音乐候选应只保留一次。"""
|
|
results = MusicChain.normalize_candidates(
|
|
[
|
|
MusicInfo(source="musicbrainz", media_id="recording-1", title="A"),
|
|
{
|
|
"type": "音乐",
|
|
"source": "musicbrainz",
|
|
"media_id": "recording-1",
|
|
"title": "A duplicate",
|
|
},
|
|
]
|
|
)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].title == "A"
|
|
|
|
|
|
def test_normalize_candidates_deduplicates_metadata_without_id():
|
|
"""缺少来源 ID 时应按标题、艺术家和专辑去重。"""
|
|
results = MusicChain.normalize_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(
|
|
source="musicbrainz",
|
|
media_id="recording-1",
|
|
title="晴天",
|
|
artists=["周杰伦"],
|
|
album="叶惠美",
|
|
year=2003,
|
|
track_number=3,
|
|
)
|
|
|
|
meta = MusicChain.to_meta(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 = MusicChain()
|
|
requested = {}
|
|
|
|
def fake_run_module(method, **kwargs):
|
|
"""记录榜单模块调用并返回重复候选。"""
|
|
requested.update(method=method, **kwargs)
|
|
return [
|
|
MusicInfo(source="musicbrainz", media_id="recording-1", title="晴天"),
|
|
MusicInfo(source="musicbrainz", media_id="recording-1", title="晴天"),
|
|
]
|
|
|
|
monkeypatch.setattr(chain, "run_module", fake_run_module)
|
|
|
|
results = chain.chart(range_name="this_week", page=2, count=30)
|
|
|
|
assert requested == {
|
|
"method": "music_chart",
|
|
"range_name": "this_week",
|
|
"offset": 30,
|
|
"count": 30,
|
|
}
|
|
assert len(results) == 1
|
|
|
|
|
|
def test_async_chart_applies_music_explore_filters(monkeypatch):
|
|
"""音乐探索应按收听次数、封面条件和升序设置筛选榜单。"""
|
|
chain = MusicChain()
|
|
|
|
async def fake_async_run_module(method, **kwargs):
|
|
"""返回包含不同热度和封面状态的榜单候选。"""
|
|
assert method == "music_chart"
|
|
return [
|
|
MusicInfo(media_id="1", source="musicbrainz", title="A", listen_count=300),
|
|
MusicInfo(
|
|
media_id="2",
|
|
source="musicbrainz",
|
|
title="B",
|
|
listen_count=120,
|
|
cover_url="https://coverartarchive.org/release/2/front-500",
|
|
),
|
|
MusicInfo(
|
|
media_id="3",
|
|
source="musicbrainz",
|
|
title="C",
|
|
listen_count=240,
|
|
cover_url="https://coverartarchive.org/release/3/front-500",
|
|
),
|
|
]
|
|
|
|
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
|
|
|
import asyncio
|
|
|
|
results = asyncio.run(
|
|
chain.async_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(source="musicbrainz", media_id="1", title="晴天", artists=["其他歌手"]),
|
|
MusicInfo(
|
|
source="musicbrainz",
|
|
media_id="2",
|
|
title="晴天",
|
|
artists=["周杰伦"],
|
|
album="叶惠美",
|
|
),
|
|
]
|
|
|
|
selected = MusicBrainzModule._select_candidate(meta, candidates, source="musicbrainz")
|
|
|
|
assert selected is candidates[1]
|
|
|
|
|
|
def test_async_chart_forwards_album_entity(monkeypatch):
|
|
"""热门专辑探索应把实体类型透传给 ListenBrainz 榜单模块。"""
|
|
chain = MusicChain()
|
|
requested = {}
|
|
|
|
async def fake_async_run_module(method, **kwargs):
|
|
"""记录榜单请求参数并返回一个专辑候选。"""
|
|
requested.update(method=method, **kwargs)
|
|
return [
|
|
MusicInfo(
|
|
media_id="release-group-1",
|
|
source="musicbrainz",
|
|
music_type="album",
|
|
title="ARIRANG",
|
|
listen_count=10,
|
|
)
|
|
]
|
|
|
|
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
|
|
|
import asyncio
|
|
|
|
results = asyncio.run(chain.async_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 = MusicChain()
|
|
requested = {}
|
|
|
|
async def fake_async_run_module(method, **kwargs):
|
|
"""记录新发行请求参数并返回带封面与不带封面的候选。"""
|
|
requested.update(method=method, **kwargs)
|
|
return [
|
|
MusicInfo(media_id="b", source="musicbrainz", music_type="album", title="B"),
|
|
MusicInfo(
|
|
media_id="a",
|
|
source="musicbrainz",
|
|
music_type="album",
|
|
title="A",
|
|
cover_url="https://coverartarchive.org/release/a/front-500",
|
|
),
|
|
]
|
|
|
|
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
|
|
|
import asyncio
|
|
|
|
results = asyncio.run(
|
|
chain.async_fresh_releases(days=30, sort="release_name", page=2, count=10, with_cover=True)
|
|
)
|
|
|
|
assert requested["method"] == "music_fresh_releases"
|
|
assert requested["offset"] == 10
|
|
assert requested["sort"] == "release_name"
|
|
assert [item.title for item in results] == ["A"]
|
|
|
|
|
|
def test_async_artist_related_deduplicates_artists(monkeypatch):
|
|
"""关联艺术家应按标准 ID 去重,避免同一成员重复出现。"""
|
|
chain = MusicChain()
|
|
|
|
async def fake_async_run_module(method, **kwargs):
|
|
"""返回重复的关联艺术家候选。"""
|
|
assert method == "music_artist_related"
|
|
return [
|
|
MusicArtistInfo(source="musicbrainz", media_id="artist-1", name="Brian May"),
|
|
MusicArtistInfo(source="musicbrainz", media_id="artist-1", name="Brian May"),
|
|
MusicArtistInfo(source="musicbrainz", media_id="artist-2", name="John Deacon"),
|
|
]
|
|
|
|
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
|
|
|
import asyncio
|
|
|
|
results = asyncio.run(chain.async_artist_related(source="musicbrainz", media_id="artist-0"))
|
|
|
|
assert [item.media_id for item in results] == ["artist-1", "artist-2"]
|
|
|
|
|
|
def test_async_album_restores_dataclass_from_plugin_dict(monkeypatch):
|
|
"""插件返回字典时专辑链应恢复为标准专辑对象。"""
|
|
chain = MusicChain()
|
|
|
|
async def fake_async_run_module(method, **kwargs):
|
|
"""模拟插件模块以字典形式返回专辑详情。"""
|
|
assert method == "music_album"
|
|
return MusicAlbumInfo(
|
|
source="musicbrainz",
|
|
media_id="release-group-1",
|
|
title="A Night at the Opera",
|
|
artists=["Queen"],
|
|
release_date="1975-11-21",
|
|
).to_dict()
|
|
|
|
monkeypatch.setattr(chain, "async_run_module", fake_async_run_module)
|
|
|
|
import asyncio
|
|
|
|
album = asyncio.run(chain.async_album(source="musicbrainz", media_id="release-group-1"))
|
|
|
|
assert album is not None
|
|
assert album.year == 1975
|
|
assert album.artists == ["Queen"]
|