Files
MoviePilot/tests/test_media_scrape_endpoint.py

232 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from unittest.mock import AsyncMock, Mock, patch
from app.api.endpoints.media import recognize_file, scrape
from app.core.context import Context, MediaInfo
from app.core.meta import MetaBase, MetaMusic
from app.core.context import MusicInfo
from app.schemas import FileItem, MediaType
def test_scrape_uses_explicit_media_source_and_id() -> None:
"""手动刮削应使用请求指定的数据源原生ID并传给后续刮削流程。"""
fileitem = FileItem(storage="alist", path="/movies/Test Movie (2026).mkv", type="file")
media_info = MediaInfo(title="测试电影", type=MediaType.MOVIE)
chain = Mock()
chain.recognize_media.return_value = media_info
with patch("app.api.endpoints.media.MediaChain", return_value=chain) as mock_chain:
# mkv 非音频文件,需显式关闭 Mock 的 is_audio_path 避免误入音乐分支
mock_chain.is_audio_path.return_value = False
result = scrape(
fileitem=fileitem,
storage="alist",
media_source="douban",
media_id="123456",
type_name=MediaType.MOVIE,
_=Mock(),
)
assert result.success is True
chain.recognize_by_path.assert_not_called()
recognize_kwargs = chain.recognize_media.call_args.kwargs
assert recognize_kwargs["source"] == "douban"
assert recognize_kwargs["mediaid"] == "123456"
assert recognize_kwargs["mtype"] == MediaType.MOVIE
chain.obtain_images.assert_called_once_with(mediainfo=media_info)
assert media_info.scrape_source == "douban"
scrape_kwargs = chain.scrape_metadata.call_args.kwargs
assert scrape_kwargs["fileitem"] is fileitem
assert scrape_kwargs["mediainfo"] is media_info
assert scrape_kwargs["overwrite"] is True
def test_scrape_keeps_automatic_recognition_compatible() -> None:
"""未指定媒体ID时应继续按路径识别并允许仅限定请求级数据源。"""
fileitem = FileItem(storage="alist", path="/tv/Test Show S01E01.mkv", type="file")
meta_info = MetaBase("Test Show S01E01")
media_info = MediaInfo(title="测试剧集", type=MediaType.TV)
chain = Mock()
chain.recognize_by_path.return_value = Context(meta_info=meta_info, media_info=media_info)
with patch("app.api.endpoints.media.MediaChain", return_value=chain) as mock_chain:
# mkv 非音频文件,需显式关闭 Mock 的 is_audio_path 避免误入音乐分支
mock_chain.is_audio_path.return_value = False
result = scrape(
fileitem=fileitem,
storage="alist",
media_source="bangumi",
_=Mock(),
)
assert result.success is True
chain.recognize_by_path.assert_called_once_with(
fileitem.path,
source="bangumi",
obtain_images=True,
)
chain.recognize_media.assert_not_called()
assert media_info.scrape_source == "bangumi"
chain.scrape_metadata.assert_called_once_with(
fileitem=fileitem,
meta=meta_info,
mediainfo=media_info,
overwrite=True,
)
def test_scrape_rejects_media_id_without_source() -> None:
"""原生媒体ID缺少所属数据源时应直接返回明确错误。"""
result = scrape(
fileitem=FileItem(storage="alist", path="/movies/Test.mkv", type="file"),
storage="alist",
media_id="123456",
_=Mock(),
)
assert result.success is False
assert result.message == "指定媒体ID时必须同时指定媒体数据源"
def test_recognize_file_routes_audio_to_music_chain() -> None:
"""文件管理识别音频文件时应经统一路径识别入口返回音乐专属上下文。"""
chain = Mock()
chain.async_recognize_by_path = AsyncMock(
return_value=Context(
meta_info=MetaMusic(title="晴天", artists=["周杰伦"]),
media_info=MusicInfo(
media_source="musicbrainz",
media_id="977e6978-139d-425c-bb98-6b0c62d1e45e",
title="晴天",
artists=["周杰伦"],
),
)
)
import asyncio
with patch("app.api.endpoints.media.MediaChain", return_value=chain):
result = asyncio.run(recognize_file(path="/music/晴天.flac", _=Mock()))
assert result["meta_info"]["type"] == "音乐"
assert result["media_info"]["title"] == "晴天"
chain.async_recognize_by_path.assert_awaited_once_with(
"/music/晴天.flac", source=None
)
def test_scrape_music_uses_musicbrainz_uuid_and_music_scraper() -> None:
"""手动音乐刮削应接受 MusicBrainz UUID 并经统一识别入口后写入音乐标签。"""
fileitem = FileItem(storage="local", path="/music/晴天.flac", type="file")
info = MusicInfo(
media_source="musicbrainz",
media_id="977e6978-139d-425c-bb98-6b0c62d1e45e",
title="晴天",
)
media_chain = Mock()
media_chain.recognize_media.return_value = info
media_chain.scrape_music_metadata.return_value = (True, "已刮削 1 个音频文件")
with patch("app.api.endpoints.media.MediaChain", return_value=media_chain):
result = scrape(
fileitem=fileitem,
storage="local",
media_source="musicbrainz",
media_id="977e6978-139d-425c-bb98-6b0c62d1e45e",
type_name=MediaType.MUSIC,
music_type="recording",
_=Mock(),
)
assert result.success is True
media_chain.recognize_media.assert_called_once_with(
media_source="musicbrainz",
mediaid="977e6978-139d-425c-bb98-6b0c62d1e45e",
mtype=MediaType.MUSIC,
music_type="recording",
)
media_chain.scrape_music_metadata.assert_called_once_with(
fileitem=fileitem,
mediainfo=info,
overwrite=True,
media_source="musicbrainz",
)
def test_scrape_music_without_source_keeps_automatic_recognition() -> None:
"""未选择音乐源时刮削入口应传递空来源,让底层比较全部识别源。"""
fileitem = FileItem(storage="local", path="/music/晴天.flac", type="file")
media_chain = Mock()
media_chain.scrape_music_metadata.return_value = (True, "已刮削 1 个音频文件")
with patch("app.api.endpoints.media.MediaChain", return_value=media_chain):
result = scrape(
fileitem=fileitem,
storage="local",
type_name=MediaType.MUSIC,
_=Mock(),
)
assert result.success is True
media_chain.scrape_music_metadata.assert_called_once_with(
fileitem=fileitem,
mediainfo=None,
overwrite=True,
source=None,
)
def test_scrape_music_album_forwards_album_namespace() -> None:
"""手动专辑刮削必须把 Release Group ID 标记为 album。"""
fileitem = FileItem(storage="local", path="/music/叶惠美", type="dir")
info = MusicInfo(
media_source="musicbrainz",
media_id="977e6978-139d-425c-bb98-6b0c62d1e45e",
music_type="album",
title="叶惠美",
)
media_chain = Mock()
media_chain.recognize_media.return_value = info
media_chain.scrape_music_metadata.return_value = (True, "已刮削专辑")
with patch("app.api.endpoints.media.MediaChain", return_value=media_chain):
result = scrape(
fileitem=fileitem,
storage="local",
media_source="musicbrainz",
media_id="977e6978-139d-425c-bb98-6b0c62d1e45e",
type_name=MediaType.MUSIC,
music_type="album",
_=Mock(),
)
assert result.success is True
assert media_chain.recognize_media.call_args.kwargs["music_type"] == "album"
def test_scrape_music_accepts_douban_recording_composite_id() -> None:
"""豆瓣音乐曲目 ID 使用“专辑ID:曲序”时应通过入口校验。"""
fileitem = FileItem(storage="local", path="/music/晴天.flac", type="file")
info = MusicInfo(
media_source="doubanmusic",
media_id="1401853:3",
music_type="recording",
title="晴天",
)
media_chain = Mock()
media_chain.recognize_media.return_value = info
media_chain.scrape_music_metadata.return_value = (True, "已刮削 1 个音频文件")
with patch("app.api.endpoints.media.MediaChain", return_value=media_chain):
result = scrape(
fileitem=fileitem,
storage="local",
media_source="doubanmusic",
media_id="1401853:3",
type_name=MediaType.MUSIC,
music_type="recording",
_=Mock(),
)
assert result.success is True
assert media_chain.recognize_media.call_args.kwargs["mediaid"] == "1401853:3"