mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-18 04:34:00 +08:00
feat(v3): enhance music server integration with Navidrome support
This commit is contained in:
@@ -24,13 +24,13 @@ def test_recording_to_info_maps_listenbrainz_payload():
|
||||
|
||||
|
||||
def test_music_chart_requests_requested_page(monkeypatch):
|
||||
"""音乐榜单模块应传递周期、偏移量和数量并过滤无身份记录。"""
|
||||
"""音乐榜单模块应传递实体、周期、偏移量和数量并过滤无身份记录。"""
|
||||
module = ListenBrainzModule()
|
||||
requested = {}
|
||||
|
||||
def fake_request(range_name, offset, count):
|
||||
def fake_request(entity, range_name, offset, count):
|
||||
"""记录榜单请求参数并返回一条有效录音。"""
|
||||
requested.update(range_name=range_name, offset=offset, count=count)
|
||||
requested.update(entity=entity, range_name=range_name, offset=offset, count=count)
|
||||
return {
|
||||
"payload": {
|
||||
"recordings": [
|
||||
@@ -48,5 +48,100 @@ def test_music_chart_requests_requested_page(monkeypatch):
|
||||
|
||||
results = module.music_chart(range_name="this_month", offset=30, count=30)
|
||||
|
||||
assert requested == {"range_name": "this_month", "offset": 30, "count": 30}
|
||||
assert requested == {
|
||||
"entity": "recordings",
|
||||
"range_name": "this_month",
|
||||
"offset": 30,
|
||||
"count": 30,
|
||||
}
|
||||
assert [item.media_id for item in results] == ["recording-1"]
|
||||
|
||||
|
||||
def test_music_chart_supports_album_entity(monkeypatch):
|
||||
"""热门专辑榜单应请求官方 release-groups 接口并返回专辑实体。"""
|
||||
module = ListenBrainzModule()
|
||||
requested = {}
|
||||
|
||||
def fake_request(entity, range_name, offset, count):
|
||||
"""记录请求实体并返回一条热门专辑。"""
|
||||
requested.update(entity=entity, range_name=range_name)
|
||||
return {
|
||||
"payload": {
|
||||
"release_groups": [
|
||||
{
|
||||
"artist_name": "BTS",
|
||||
"artist_mbids": ["artist-1"],
|
||||
"listen_count": 999,
|
||||
"release_group_mbid": "release-group-1",
|
||||
"release_group_name": "ARIRANG",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
monkeypatch.setattr(module, "_request_chart", fake_request)
|
||||
|
||||
results = module.music_chart(range_name="week", offset=0, count=10, entity="album")
|
||||
|
||||
assert requested == {"entity": "release-groups", "range_name": "week"}
|
||||
assert [item.media_id for item in results] == ["release-group-1"]
|
||||
assert results[0].music_type == "album"
|
||||
assert results[0].album_id == "release-group-1"
|
||||
assert results[0].artist_ids == ["artist-1"]
|
||||
|
||||
|
||||
def test_music_chart_falls_back_to_supported_range(monkeypatch):
|
||||
"""非官方周期应回退到默认周期,避免请求被官方接口拒绝。"""
|
||||
module = ListenBrainzModule()
|
||||
requested = {}
|
||||
|
||||
def fake_request(entity, range_name, offset, count):
|
||||
"""仅记录周期取值。"""
|
||||
requested.update(range_name=range_name)
|
||||
return {"payload": {"recordings": []}}
|
||||
|
||||
monkeypatch.setattr(module, "_request_chart", fake_request)
|
||||
module.music_chart(range_name="last_decade")
|
||||
|
||||
assert requested == {"range_name": "this_month"}
|
||||
|
||||
|
||||
def test_music_fresh_releases_pages_official_window(monkeypatch):
|
||||
"""新发行探索应按官方排序请求并在结果集上分页。"""
|
||||
module = ListenBrainzModule()
|
||||
requested = {}
|
||||
|
||||
def fake_releases(days, sort, past, future):
|
||||
"""记录官方新发行请求参数并返回两条发行。"""
|
||||
requested.update(days=days, sort=sort, past=past, future=future)
|
||||
return [
|
||||
{
|
||||
"artist_credit_name": "Artist A",
|
||||
"artist_mbids": ["artist-1"],
|
||||
"release_date": "2026-08-01",
|
||||
"release_group_mbid": "release-group-1",
|
||||
"release_group_primary_type": "Album",
|
||||
"release_name": "First",
|
||||
},
|
||||
{
|
||||
"artist_credit_name": "Artist B",
|
||||
"release_date": "2026-08-02",
|
||||
"release_group_mbid": "release-group-2",
|
||||
"release_name": "Second",
|
||||
},
|
||||
]
|
||||
|
||||
monkeypatch.setattr(module, "_fresh_releases", fake_releases)
|
||||
|
||||
results = module.music_fresh_releases(
|
||||
days=200,
|
||||
sort="unsupported",
|
||||
past=True,
|
||||
future=False,
|
||||
offset=1,
|
||||
count=1,
|
||||
)
|
||||
|
||||
assert requested == {"days": 90, "sort": "release_date", "past": True, "future": False}
|
||||
assert [item.media_id for item in results] == ["release-group-2"]
|
||||
assert results[0].music_type == "album"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from app.chain.music import MusicChain
|
||||
from app.core.music import MusicInfo, MusicMeta
|
||||
from app.core.music import MusicAlbumInfo, MusicArtistInfo, MusicInfo, MusicMeta
|
||||
|
||||
|
||||
def test_parse_query_supports_artist_title_format():
|
||||
@@ -171,3 +171,114 @@ def test_select_path_candidate_prefers_matching_audio_tags():
|
||||
selected = MusicChain._select_path_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"]
|
||||
|
||||
@@ -5,18 +5,36 @@ import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api.apiv1 import api_router
|
||||
from app.api.endpoints.music import explore_music, recognize_music, search_music
|
||||
from app.core.music import MusicInfo
|
||||
from app.api.endpoints.music import (
|
||||
explore_music,
|
||||
music_album,
|
||||
music_artist,
|
||||
music_artist_albums,
|
||||
music_artist_related,
|
||||
recognize_music,
|
||||
search_music,
|
||||
)
|
||||
from app.core.music import MusicAlbumInfo, MusicArtistInfo, MusicInfo, MusicRelease
|
||||
from app.schemas.music import MusicRecognizeRequest
|
||||
|
||||
|
||||
def test_music_routes_are_registered():
|
||||
"""V1 API 应注册音乐搜索和详情识别路由。"""
|
||||
"""V1 API 应注册音乐搜索、详情识别、探索及艺术家专辑浏览路由。"""
|
||||
routes = {(route.path, tuple(route.methods or [])) for route in api_router.routes}
|
||||
|
||||
assert any(path == "/music/search" and "GET" in methods for path, methods in routes)
|
||||
assert any(path == "/music/recognize" and "POST" in methods for path, methods in routes)
|
||||
assert any(path == "/music/explore" and "GET" in methods for path, methods in routes)
|
||||
assert any(path == "/music/album/{album_id}" and "GET" in methods for path, methods in routes)
|
||||
assert any(path == "/music/artist/{artist_id}" and "GET" in methods for path, methods in routes)
|
||||
assert any(
|
||||
path == "/music/artist/{artist_id}/albums" and "GET" in methods
|
||||
for path, methods in routes
|
||||
)
|
||||
assert any(
|
||||
path == "/music/artist/{artist_id}/related" and "GET" in methods
|
||||
for path, methods in routes
|
||||
)
|
||||
|
||||
|
||||
def test_search_music_serializes_chain_results():
|
||||
@@ -91,7 +109,7 @@ def test_recognize_music_returns_404_for_unknown_item():
|
||||
|
||||
|
||||
def test_explore_music_forwards_filters_and_serializes_chart():
|
||||
"""音乐探索接口应传递周期、排序、热度和封面筛选条件。"""
|
||||
"""音乐探索接口应传递实体、周期、排序、热度和封面筛选条件。"""
|
||||
chain = Mock()
|
||||
chain.async_chart = AsyncMock(
|
||||
return_value=[
|
||||
@@ -126,4 +144,165 @@ def test_explore_music_forwards_filters_and_serializes_chart():
|
||||
sort_by="listen_count.asc",
|
||||
min_listen_count=100,
|
||||
with_cover=True,
|
||||
entity="recording",
|
||||
)
|
||||
|
||||
|
||||
def test_explore_music_supports_official_fresh_release_mode():
|
||||
"""新发行模式应按 ListenBrainz 官方排序和时间窗口请求探索数据。"""
|
||||
chain = Mock()
|
||||
chain.async_fresh_releases = AsyncMock(
|
||||
return_value=[
|
||||
MusicInfo(
|
||||
source="musicbrainz",
|
||||
media_id="release-group-1",
|
||||
music_type="album",
|
||||
title="ARIRANG",
|
||||
artists=["BTS"],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with patch("app.api.endpoints.music.MusicChain", return_value=chain):
|
||||
result = asyncio.run(
|
||||
explore_music(
|
||||
page=1,
|
||||
count=30,
|
||||
mode="fresh",
|
||||
sort="artist_credit_name",
|
||||
days=30,
|
||||
past=True,
|
||||
future=False,
|
||||
with_cover=True,
|
||||
_=Mock(),
|
||||
)
|
||||
)
|
||||
|
||||
assert result[0].music_type == "album"
|
||||
chain.async_fresh_releases.assert_awaited_once_with(
|
||||
days=30,
|
||||
sort="artist_credit_name",
|
||||
past=True,
|
||||
future=False,
|
||||
page=1,
|
||||
count=30,
|
||||
with_cover=True,
|
||||
)
|
||||
|
||||
|
||||
def test_music_album_returns_tracks_and_releases():
|
||||
"""专辑接口应返回专辑详情、曲目和发行版本。"""
|
||||
chain = Mock()
|
||||
chain.async_album = AsyncMock(
|
||||
return_value=MusicAlbumInfo(
|
||||
source="musicbrainz",
|
||||
media_id="release-group-1",
|
||||
title="A Night at the Opera",
|
||||
artists=["Queen"],
|
||||
artist_ids=["artist-1"],
|
||||
album_type="Album",
|
||||
release_date="1975-11-21",
|
||||
tracks=[MusicInfo(source="musicbrainz", media_id="recording-1", title="Love of My Life")],
|
||||
releases=[MusicRelease(media_id="release-1", title="A Night at the Opera", date="1975")],
|
||||
)
|
||||
)
|
||||
|
||||
with patch("app.api.endpoints.music.MusicChain", return_value=chain):
|
||||
result = asyncio.run(music_album(album_id="release-group-1", _=Mock()))
|
||||
|
||||
assert result.music_type == "album"
|
||||
assert result.year == 1975
|
||||
assert result.total_tracks == 1
|
||||
assert result.tracks[0].media_id == "recording-1"
|
||||
assert result.releases[0].media_id == "release-1"
|
||||
chain.async_album.assert_awaited_once_with(source="musicbrainz", media_id="release-group-1")
|
||||
|
||||
|
||||
def test_music_album_returns_404_for_unknown_album():
|
||||
"""专辑不存在时接口应返回 404。"""
|
||||
chain = Mock()
|
||||
chain.async_album = AsyncMock(return_value=None)
|
||||
|
||||
with (
|
||||
patch("app.api.endpoints.music.MusicChain", return_value=chain),
|
||||
pytest.raises(HTTPException) as error,
|
||||
):
|
||||
asyncio.run(music_album(album_id="missing", _=Mock()))
|
||||
|
||||
assert error.value.status_code == 404
|
||||
|
||||
|
||||
def test_music_artist_returns_detail():
|
||||
"""艺术家接口应返回名称、类型和活跃时间。"""
|
||||
chain = Mock()
|
||||
chain.async_artist = AsyncMock(
|
||||
return_value=MusicArtistInfo(
|
||||
source="musicbrainz",
|
||||
media_id="artist-1",
|
||||
name="Queen",
|
||||
artist_type="Group",
|
||||
begin_date="1970-06-27",
|
||||
)
|
||||
)
|
||||
|
||||
with patch("app.api.endpoints.music.MusicChain", return_value=chain):
|
||||
result = asyncio.run(music_artist(artist_id="artist-1", _=Mock()))
|
||||
|
||||
assert result.name == "Queen"
|
||||
assert result.title == "Queen"
|
||||
assert result.music_type == "artist"
|
||||
chain.async_artist.assert_awaited_once_with(source="musicbrainz", media_id="artist-1")
|
||||
|
||||
|
||||
def test_music_artist_albums_forwards_pagination_and_type():
|
||||
"""艺术家专辑接口应传递分页和专辑类型筛选。"""
|
||||
chain = Mock()
|
||||
chain.async_artist_albums = AsyncMock(
|
||||
return_value=[
|
||||
MusicInfo(
|
||||
source="musicbrainz",
|
||||
media_id="release-group-1",
|
||||
music_type="album",
|
||||
title="News of the World",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with patch("app.api.endpoints.music.MusicChain", return_value=chain):
|
||||
result = asyncio.run(
|
||||
music_artist_albums(artist_id="artist-1", page=2, count=10, album_type="ep", _=Mock())
|
||||
)
|
||||
|
||||
assert result[0].media_id == "release-group-1"
|
||||
chain.async_artist_albums.assert_awaited_once_with(
|
||||
source="musicbrainz",
|
||||
media_id="artist-1",
|
||||
page=2,
|
||||
count=10,
|
||||
album_type="ep",
|
||||
)
|
||||
|
||||
|
||||
def test_music_artist_related_returns_relationship_text():
|
||||
"""关联艺术家接口应返回关系说明,供详情页展示。"""
|
||||
chain = Mock()
|
||||
chain.async_artist_related = AsyncMock(
|
||||
return_value=[
|
||||
MusicArtistInfo(
|
||||
source="musicbrainz",
|
||||
media_id="artist-2",
|
||||
name="Freddie Mercury",
|
||||
relation="member of band",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with patch("app.api.endpoints.music.MusicChain", return_value=chain):
|
||||
result = asyncio.run(music_artist_related(artist_id="artist-1", count=5, _=Mock()))
|
||||
|
||||
assert result[0].relation == "member of band"
|
||||
chain.async_artist_related.assert_awaited_once_with(
|
||||
source="musicbrainz",
|
||||
media_id="artist-1",
|
||||
count=5,
|
||||
)
|
||||
|
||||
@@ -62,6 +62,7 @@ def test_build_subscribe_meta_returns_music_meta():
|
||||
assert isinstance(meta, MusicMeta)
|
||||
assert meta.type == MediaType.MUSIC
|
||||
assert meta.media_id == "recording-1"
|
||||
assert meta.original_name == "晴天"
|
||||
|
||||
|
||||
def test_music_subscribe_reuses_search_download_and_finish_flow():
|
||||
|
||||
@@ -116,3 +116,255 @@ def test_recognize_music_fetches_recording_detail(monkeypatch):
|
||||
assert result is not None
|
||||
assert result.media_id == "recording-1"
|
||||
assert result.title == "晴天"
|
||||
|
||||
|
||||
def test_recognize_music_falls_back_to_album(monkeypatch):
|
||||
"""单曲 ID 不存在时应按专辑再识别一次,保证专辑订阅可恢复目标。"""
|
||||
module = MusicBrainzModule()
|
||||
requested = []
|
||||
|
||||
def fake_request(path, params=None):
|
||||
"""单曲请求返回空,专辑请求返回最小 Release Group 数据。"""
|
||||
requested.append(path)
|
||||
if path.startswith("/recording/"):
|
||||
return None
|
||||
if path.startswith("/release-group/"):
|
||||
return {
|
||||
"id": "release-group-1",
|
||||
"title": "A Night at the Opera",
|
||||
"primary-type": "Album",
|
||||
"first-release-date": "1975-11-21",
|
||||
"artist-credit": [{"artist": {"id": "artist-1", "name": "Queen"}}],
|
||||
"releases": [],
|
||||
}
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(module, "_request_json", fake_request)
|
||||
monkeypatch.setattr(MusicBrainzModule, "_request_json", staticmethod(fake_request))
|
||||
|
||||
info = module.recognize_music("musicbrainz", "release-group-1")
|
||||
|
||||
assert info is not None
|
||||
assert info.music_type == "album"
|
||||
assert info.album_id == "release-group-1"
|
||||
assert info.artists == ["Queen"]
|
||||
assert info.artist_ids == ["artist-1"]
|
||||
assert requested[0].startswith("/recording/")
|
||||
|
||||
|
||||
def test_music_album_builds_tracks_and_release_variants(monkeypatch):
|
||||
"""专辑详情应带上曲目列表、发行版本和 10 分制评分。"""
|
||||
module = MusicBrainzModule()
|
||||
|
||||
def fake_request(path, params=None):
|
||||
"""按路径分别返回 Release Group 与代表性 Release 数据。"""
|
||||
if path == "/release-group/release-group-1":
|
||||
return {
|
||||
"id": "release-group-1",
|
||||
"title": "A Night at the Opera",
|
||||
"primary-type": "Album",
|
||||
"secondary-types": ["Live"],
|
||||
"first-release-date": "1975-11-21",
|
||||
"rating": {"value": 4.25, "votes-count": 44},
|
||||
"genres": [{"name": "rock", "count": 13}, {"name": "art rock", "count": 4}],
|
||||
"tags": [{"name": "british", "count": 2}],
|
||||
"artist-credit": [{"artist": {"id": "artist-1", "name": "Queen"}}],
|
||||
"releases": [
|
||||
{
|
||||
"id": "release-early",
|
||||
"title": "A Night at the Opera",
|
||||
"status": "Official",
|
||||
"date": "1975-11-21",
|
||||
"country": "GB",
|
||||
"packaging": "Gatefold Cover",
|
||||
"media": [{"format": "12\" Vinyl", "track-count": 2}],
|
||||
},
|
||||
{
|
||||
"id": "release-late",
|
||||
"title": "A Night at the Opera",
|
||||
"status": "Official",
|
||||
"date": "1991",
|
||||
"media": [{"format": "CD", "track-count": 2}],
|
||||
},
|
||||
],
|
||||
}
|
||||
if path == "/release/release-early":
|
||||
return {
|
||||
"media": [
|
||||
{
|
||||
"position": 1,
|
||||
"track-count": 2,
|
||||
"tracks": [
|
||||
{
|
||||
"position": 1,
|
||||
"title": "Death on Two Legs",
|
||||
"length": 223733,
|
||||
"recording": {"id": "recording-1", "title": "Death on Two Legs"},
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"title": "Lazing on a Sunday Afternoon",
|
||||
"recording": {"id": "recording-2", "length": 68000},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(MusicBrainzModule, "_request_json", staticmethod(fake_request))
|
||||
|
||||
album = module.music_album("musicbrainz", "release-group-1")
|
||||
|
||||
assert album is not None
|
||||
assert album.category == "Album / Live"
|
||||
assert album.rating == 8.5
|
||||
assert album.genres[:2] == ["rock", "art rock"]
|
||||
assert [track.media_id for track in album.tracks] == ["recording-1", "recording-2"]
|
||||
assert album.tracks[0].duration == 224
|
||||
assert album.tracks[0].track_number == 1
|
||||
assert album.tracks[0].disc_number == 1
|
||||
assert album.tracks[0].album_id == "release-group-1"
|
||||
assert album.tracks[1].title == "Lazing on a Sunday Afternoon"
|
||||
assert [release.media_id for release in album.releases] == ["release-early", "release-late"]
|
||||
assert album.releases[0].formats == ['12" Vinyl']
|
||||
assert album.track_count == 2
|
||||
|
||||
|
||||
def test_music_artist_maps_profile_links_and_image(monkeypatch):
|
||||
"""艺术家详情应整理活跃时间、别名、外链并把维基共享页转为图片直链。"""
|
||||
module = MusicBrainzModule()
|
||||
monkeypatch.setattr(
|
||||
MusicBrainzModule,
|
||||
"_request_json",
|
||||
staticmethod(
|
||||
lambda path, params=None: {
|
||||
"id": "artist-1",
|
||||
"name": "Queen",
|
||||
"sort-name": "Queen",
|
||||
"type": "Group",
|
||||
"disambiguation": "UK rock group",
|
||||
"country": "GB",
|
||||
"area": {"name": "United Kingdom"},
|
||||
"life-span": {"begin": "1970-06-27", "ended": True},
|
||||
"genres": [{"name": "rock", "count": 20}, {"name": "glam rock", "count": 9}],
|
||||
"tags": [{"name": "british", "count": 15}],
|
||||
"aliases": [{"name": "皇后乐队", "count": 0}],
|
||||
"relations": [
|
||||
{
|
||||
"type": "image",
|
||||
"target-type": "url",
|
||||
"url": {"resource": "https://commons.wikimedia.org/wiki/File:Queen.jpg"},
|
||||
},
|
||||
{
|
||||
"type": "official homepage",
|
||||
"target-type": "url",
|
||||
"url": {"resource": "http://www.queenonline.com/"},
|
||||
},
|
||||
{
|
||||
"type": "wikidata",
|
||||
"target-type": "url",
|
||||
"url": {"resource": "https://www.wikidata.org/wiki/Q15862"},
|
||||
},
|
||||
{
|
||||
"type": "creative commons licensed download",
|
||||
"target-type": "url",
|
||||
"url": {"resource": "https://example.com/ignored"},
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
artist = module.music_artist("musicbrainz", "artist-1")
|
||||
|
||||
assert artist is not None
|
||||
assert artist.artist_type == "Group"
|
||||
assert artist.area == "United Kingdom"
|
||||
assert artist.life_span == "1970-06-27"
|
||||
assert artist.genres == ["rock", "glam rock"]
|
||||
assert artist.aliases == ["皇后乐队"]
|
||||
assert artist.image_url == (
|
||||
"https://commons.wikimedia.org/wiki/Special:FilePath/Queen.jpg?width=500"
|
||||
)
|
||||
assert set(artist.external_links) == {"official homepage", "wikidata"}
|
||||
|
||||
|
||||
def test_music_artist_albums_sorts_page_by_release_date(monkeypatch):
|
||||
"""艺术家专辑列表应按发行日期倒序,并带上专辑类型筛选参数。"""
|
||||
module = MusicBrainzModule()
|
||||
requested = {}
|
||||
|
||||
def fake_request(path, params=None):
|
||||
"""记录浏览请求参数并返回两个乱序专辑。"""
|
||||
requested.update(path=path, params=params)
|
||||
return {
|
||||
"release-groups": [
|
||||
{
|
||||
"id": "release-group-old",
|
||||
"title": "Queen",
|
||||
"primary-type": "Album",
|
||||
"first-release-date": "1973-07-13",
|
||||
"artist-credit": [{"artist": {"id": "artist-1", "name": "Queen"}}],
|
||||
},
|
||||
{
|
||||
"id": "release-group-new",
|
||||
"title": "News of the World",
|
||||
"primary-type": "Album",
|
||||
"first-release-date": "1977-10-28",
|
||||
"artist-credit": [{"artist": {"id": "artist-1", "name": "Queen"}}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
monkeypatch.setattr(MusicBrainzModule, "_request_json", staticmethod(fake_request))
|
||||
|
||||
albums = module.music_artist_albums("musicbrainz", "artist-1", page=2, count=10, album_type="album")
|
||||
|
||||
assert requested["path"] == "/release-group"
|
||||
assert requested["params"]["artist"] == "artist-1"
|
||||
assert requested["params"]["offset"] == 10
|
||||
assert requested["params"]["type"] == "album"
|
||||
assert [album.media_id for album in albums] == ["release-group-new", "release-group-old"]
|
||||
assert albums[0].music_type == "album"
|
||||
|
||||
|
||||
def test_music_artist_related_prefers_meaningful_relations(monkeypatch):
|
||||
"""关联艺术家应优先返回成员与子团体关系,致敬乐队排在最后。"""
|
||||
module = MusicBrainzModule()
|
||||
monkeypatch.setattr(
|
||||
MusicBrainzModule,
|
||||
"_request_json",
|
||||
staticmethod(
|
||||
lambda path, params=None: {
|
||||
"relations": [
|
||||
{
|
||||
"type": "tribute",
|
||||
"target-type": "artist",
|
||||
"artist": {"id": "artist-tribute", "name": "Queen Tribute"},
|
||||
},
|
||||
{
|
||||
"type": "member of band",
|
||||
"target-type": "artist",
|
||||
"artist": {"id": "artist-member", "name": "Brian May"},
|
||||
},
|
||||
{
|
||||
"type": "member of band",
|
||||
"target-type": "artist",
|
||||
"artist": {"id": "artist-member", "name": "Brian May"},
|
||||
},
|
||||
{
|
||||
"type": "allmusic",
|
||||
"target-type": "url",
|
||||
"url": {"resource": "https://example.com"},
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
related = module.music_artist_related("musicbrainz", "artist-1", count=5)
|
||||
|
||||
assert [item.media_id for item in related] == ["artist-member", "artist-tribute"]
|
||||
assert related[0].relation == "member of band"
|
||||
assert related[0].music_type == "artist"
|
||||
|
||||
32
tests/test_navidrome_module.py
Normal file
32
tests/test_navidrome_module.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Navidrome 媒体服务器模块接入测试。"""
|
||||
from app.core.module import ModuleManager
|
||||
from app.modules.navidrome import NavidromeModule
|
||||
from app.schemas.types import MediaServerType, ModuleType
|
||||
|
||||
|
||||
def test_navidrome_module_declares_media_server_identity():
|
||||
"""Navidrome 应以媒体服务器身份注册,供统一媒体服务器链调用。"""
|
||||
assert NavidromeModule.get_name() == "Navidrome"
|
||||
assert NavidromeModule.get_type() == ModuleType.MediaServer
|
||||
assert NavidromeModule.get_subtype() == MediaServerType.Navidrome
|
||||
|
||||
|
||||
def test_navidrome_module_has_no_system_switch():
|
||||
"""Navidrome 由服务配置控制启用,不能返回无效的系统开关名。"""
|
||||
assert NavidromeModule().init_setting() is None
|
||||
|
||||
|
||||
def test_navidrome_module_is_loaded_by_module_manager():
|
||||
"""模块管理器应能加载 Navidrome,否则媒体服务器列表里不会出现该类型。"""
|
||||
assert "NavidromeModule" in ModuleManager()._running_modules
|
||||
|
||||
|
||||
def test_navidrome_module_ignores_non_music_media():
|
||||
"""Navidrome 只管理音乐,影视存在性检查应交给其它媒体服务器。"""
|
||||
from app.core.context import MediaInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
mediainfo = MediaInfo()
|
||||
mediainfo.type = MediaType.MOVIE
|
||||
|
||||
assert NavidromeModule().media_exists(mediainfo) is None
|
||||
Reference in New Issue
Block a user