Files
MoviePilot/tests/test_bluray.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

233 lines
8.2 KiB
Python

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
验证蓝光目录识别、转移入口与元数据刮削。
"""
from pathlib import Path
from types import SimpleNamespace
from typing import Optional
from unittest.mock import patch
import pytest
from app.application.history import TransferHistorySnapshot
from app.chain.scraping import ScrapingChain
from app.chain.storage import StorageChain
from app.chain.transfer.facade import TransferChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfoPath
from app.runtime.events import Event
from app.runtime.log import logger
from app.schemas.file import FileItem
from app.schemas.types import EventType
from tests.cases.files import bluray_files
@pytest.fixture
def bluray_context():
"""
构造独立的蓝光文件树与转移历史,供路径归并和刮削断言使用。
:return: 含根目录、路径索引与历史记录的测试上下文
"""
bluray_context = SimpleNamespace()
bluray_context.history = []
bluray_context.root = FileItem(path="/", name="", type="dir", extension="", size=0)
bluray_context.items = {bluray_context.root.path: bluray_context.root}
def __build_child(parent: FileItem, files: list[tuple[str, list | int]]):
"""
递归构造目录子项并建立路径索引,不创建真实文件。
"""
parent.children = []
for name, children in files:
sep = "" if parent.path.endswith("/") else "/"
file_item = FileItem(
path=f"{parent.path}{sep}{name}",
name=name,
extension=Path(name).suffix[1:],
basename=Path(name).stem,
type="file" if isinstance(children, int) else "dir",
size=children if isinstance(children, int) else 0,
)
parent.children.append(file_item)
bluray_context.items[file_item.path] = file_item
if isinstance(children, list):
__build_child(file_item, children)
__build_child(bluray_context.root, bluray_files)
return bluray_context
def _test_do_transfer(bluray_context):
"""
验证各级蓝光路径归并到原盘目录,普通视频保留文件路径。
"""
def __test_do_transfer(path: str):
bluray_context.history.clear()
TransferChain().do_transfer(
force=False,
background=False,
fileitem=StorageChain().get_file_item(None, Path(path)),
)
return bluray_context.history
assert [
"/FOLDER/Digimon/Digimon BluRay (2055)",
"/FOLDER/Digimon/Digimon BluRay (2099)",
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
] == __test_do_transfer("/FOLDER/Digimon")
assert ["/FOLDER/Digimon/Digimon BluRay (2055)"] == __test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)")
assert ["/FOLDER/Digimon/Digimon BluRay (2055)"] == __test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV")
assert ["/FOLDER/Digimon/Digimon BluRay (2055)"] == __test_do_transfer(
"/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM"
)
assert ["/FOLDER/Digimon/Digimon BluRay (2055)"] == __test_do_transfer(
"/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM/00001.m2ts"
)
assert ["/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"] == __test_do_transfer("/FOLDER/Digimon/Digimon (2199)")
assert ["/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"] == __test_do_transfer(
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"
)
assert ["/FOLDER/Pokemon.2029.mp4"] == __test_do_transfer("/FOLDER/Pokemon.2029.mp4")
assert [
"/FOLDER/Digimon/Digimon BluRay (2055)",
"/FOLDER/Digimon/Digimon BluRay (2099)",
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
"/FOLDER/Pokemon BluRay (2016)",
"/FOLDER/Pokemon BluRay (2021)",
"/FOLDER/Pokemon (2028)/Pokemon.2028.mkv",
"/FOLDER/Pokemon.2029.mp4",
"/FOLDER/Pokemon.2039.mp4",
"/FOLDER/Pokemon (2031)/Pokemon (2031).mp4",
] == __test_do_transfer("/")
def _test_scrape_metadata(mock_metadata_nfo):
"""
验证手动和事件刮削对蓝光原盘及普通电影生成正确数量的元数据。
"""
def __test_scrape_metadata(path: str, excepted_nfo_count: int = 1):
"""
分别验证手动和自动刮削的元数据生成次数。
:param path: 模拟文件树中的电影或目录路径
:param excepted_nfo_count: 每种刮削入口应生成的元数据数量
"""
fileitem = StorageChain().get_file_item(None, Path(path))
meta = MetaInfoPath(Path(fileitem.path))
mediainfo = MediaInfo(tmdb_info={"id": 1, "title": "Test"})
# 测试手动刮削
logger.debug(f"测试手动刮削 {path}")
mock_metadata_nfo.reset_mock()
ScrapingChain().scrape_metadata(fileitem=fileitem, meta=meta, mediainfo=mediainfo, overwrite=True)
# 确保调用了指定次数的metadata_nfo
assert mock_metadata_nfo.call_count == excepted_nfo_count
# 测试自动刮削
logger.debug(f"测试自动刮削 {path}")
mock_metadata_nfo.reset_mock()
ScrapingChain().scrape_metadata_event(
Event(
event_type=EventType.MetadataScrape,
event_data={
"meta": meta,
"mediainfo": mediainfo,
"fileitem": fileitem,
"file_list": [fileitem.path],
"overwrite": False,
},
)
)
# 调用了指定次数的metadata_nfo
assert mock_metadata_nfo.call_count == excepted_nfo_count
# 刮削原盘目录
__test_scrape_metadata("/FOLDER/Digimon/Digimon BluRay (2099)")
# 刮削电影文件
__test_scrape_metadata("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4")
# 刮削电影目录
__test_scrape_metadata("/FOLDER", excepted_nfo_count=2)
# 避免真实图片请求
@patch("app.chain.scraping.ScrapingChain.metadata_img", return_value=None)
@patch("app.chain.transfer.records.resolve_history")
@patch("app.chain.storage.StorageChain.list_files")
@patch("app.chain.storage.StorageChain.get_parent_item")
@patch("app.chain.storage.StorageChain.get_file_item")
def test_bluray_transfer_and_scraping(
mock_get_file_item,
mock_get_parent_item,
mock_list_files,
mock_resolve_history,
mock_metadata_img,
bluray_context,
) -> None:
"""
验证模拟蓝光文件树的转移与刮削过程,不访问真实文件和图片服务。
"""
def get_file_item(storage: str, path: Path):
"""
从内存文件树返回路径对应的文件项。
"""
path_posix = path.as_posix()
return bluray_context.items.get(path_posix)
def get_parent_item(fileitem: FileItem):
"""
返回内存文件树中的父目录。
"""
return get_file_item(None, Path(fileitem.path).parent)
def list_files(fileitem: FileItem, recursion: bool = False):
"""
列出模拟目录子项,递归模式按路径前缀收集所有后代。
"""
if fileitem.type != "dir":
return None
if recursion:
result = []
file_path = f"{fileitem.path}/"
for path, item in bluray_context.items.items():
if path.startswith(file_path):
result.append(item)
return result
else:
return fileitem.children
def resolve_history(
src: str,
storage: Optional[str] = None,
transfer_history_oper=None,
):
"""
记录被解析的源路径并返回成功历史,避免读取真实数据库。
"""
bluray_context.history.append(src)
return TransferHistorySnapshot(id=1, status=True)
mock_get_file_item.side_effect = get_file_item
mock_get_parent_item.side_effect = get_parent_item
mock_list_files.side_effect = list_files
mock_resolve_history.side_effect = resolve_history
_test_do_transfer(bluray_context)
with patch("app.chain.scraping.ScrapingChain.metadata_nfo", return_value=None) as mock:
_test_scrape_metadata(mock_metadata_nfo=mock)