mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
fix(transfer): warn on unmatched custom target directory (#7159)
This commit is contained in:
@@ -539,12 +539,15 @@ class FileManagerModule(_ModuleBase):
|
||||
"""
|
||||
解析整理策略并生成零写副作用的冻结计划。
|
||||
|
||||
目录配置优先于显式目标路径;未匹配配置时保留自定义路径能力并告警,
|
||||
未提供显式路径时业务拒绝,不自动使用存储根目录。
|
||||
|
||||
:param fileitem: 文件信息
|
||||
:param meta: 预识别的元数据
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param target_directory: 目标目录配置
|
||||
:param target_storage: 目标存储
|
||||
:param target_path: 目标路径
|
||||
:param target_path: 调用方显式指定的目标根路径;未匹配目录配置时使用
|
||||
:param transfer_type: 转移模式
|
||||
:param scrape: 是否刮削元数据
|
||||
:param library_type_folder: 是否按媒体类型创建目录
|
||||
@@ -582,6 +585,7 @@ class FileManagerModule(_ModuleBase):
|
||||
need_type_folder=library_type_folder,
|
||||
need_category_folder=library_category_folder)
|
||||
elif target_path:
|
||||
explicit_target_path = target_path
|
||||
need_scrape = scrape or False
|
||||
need_rename = True
|
||||
need_notify = False
|
||||
@@ -589,6 +593,13 @@ class FileManagerModule(_ModuleBase):
|
||||
target_path = handler.get_dest_path(mediainfo=mediainfo, target_path=target_path,
|
||||
need_type_folder=library_type_folder,
|
||||
need_category_folder=library_category_folder)
|
||||
logger.warning(
|
||||
"未匹配到媒体库目录配置,使用调用方显式指定的目标路径:"
|
||||
f"【{target_storage or fileitem.storage or 'local'}】{explicit_target_path},"
|
||||
f"计算后的目标路径:{target_path},源路径:"
|
||||
f"【{fileitem.storage or 'local'}】{fileitem.path};"
|
||||
"如需使用存储目录配置的媒体库路径,请将目标路径设为自动匹配"
|
||||
)
|
||||
else:
|
||||
# 目录匹配失败按业务拒绝结算,避免进入异常重试和堆栈日志路径。
|
||||
logger.error(
|
||||
|
||||
@@ -37,6 +37,15 @@ Discord 仅指定 `userid` 的定向通知使用私聊;无法私聊时返回
|
||||
|
||||
### 文件整理与恢复
|
||||
|
||||
自动选择目的目录时,整理请求应将 `target_path` 留空,由源存储、源目录、媒体类型和分类
|
||||
匹配启用的存储目录配置;未匹配且没有显式目标路径时,整理会以“未找到有效的媒体库目录”拒绝。
|
||||
`downloader` 为空不代表使用默认目标路径,也不会自动选择存储根目录。
|
||||
|
||||
显式传入 `target_path` 时,若未匹配目录配置,则保留自定义目标路径,并按请求的类型、分类
|
||||
子目录开关拼接路径;例如 `/media` 可得到 `/media/电影/华语电影`,不会自动补入 `links`。
|
||||
该分支在预览和实际规划时均输出 WARNING,包含源存储与路径、传入的目标根路径及计算后的目标路径。
|
||||
需要使用已配置的库路径时,请选择自动匹配,并在执行前通过整理预览核对目标位置。
|
||||
|
||||
整理成功历史保存 `episode_group`;批量复用历史时仅恢复所有记录共同的剧集组,
|
||||
不同剧集组应分别提交。目录整理的叶子文件同样遵守冻结的覆盖策略,
|
||||
`overwrite_skipped` 表示策略拒绝,不能当作文件已导入。
|
||||
|
||||
130
tests/test_transfer_target_directory.py
Normal file
130
tests/test_transfer_target_directory.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
整理目录未匹配时的自定义路径告警与自动匹配拒绝边界。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.transfer.execution import TransferPlanningRejectedError
|
||||
from app.domain.context import MediaInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.modules.filemanager.module import FileManagerModule
|
||||
from app.schemas.category import ClassificationResult, ClassificationSelection
|
||||
from app.schemas.file import FileItem
|
||||
from app.schemas.system import TransferDirectoryConf
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
# 源文件、识别信息和可观察边界共同组成一次隔离规划上下文。
|
||||
TransferContext = tuple[FileItem, MetaBase, MediaInfo, Mock, Mock]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def transfer_context(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TransferContext:
|
||||
"""
|
||||
提供隔离源文件与分类快照,记录告警并禁止规划产生存储写入。
|
||||
"""
|
||||
source = tmp_path / "downloads" / "Movie.2021.mkv"
|
||||
source.parent.mkdir()
|
||||
source.write_bytes(b"video")
|
||||
fileitem = FileItem(
|
||||
storage="local", path=source.as_posix(), type="file",
|
||||
name=source.name, basename=source.stem, extension="mkv", size=5,
|
||||
)
|
||||
media = MediaInfo(
|
||||
type=MediaType.MOVIE, title="乌海", year="2021",
|
||||
classification=ClassificationResult(effective=ClassificationSelection(
|
||||
category_path=["华语电影"], source="automatic",
|
||||
)),
|
||||
)
|
||||
warning = Mock()
|
||||
monkeypatch.setattr("app.modules.filemanager.module.logger.warning", warning)
|
||||
return fileitem, MetaInfo(source.name), media, Mock(), warning
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_storage", [None, "smb"])
|
||||
@pytest.mark.parametrize("preview", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("type_folder", "category_folder", "relative_path"),
|
||||
[(True, True, "电影/华语电影"), (False, True, "华语电影"), (False, False, "")],
|
||||
)
|
||||
def test_unmatched_explicit_target_warns_and_preserves_requested_path(
|
||||
transfer_context: TransferContext, tmp_path: Path, target_storage: Optional[str],
|
||||
preview: bool, type_folder: bool, category_folder: bool, relative_path: str,
|
||||
) -> None:
|
||||
"""
|
||||
未匹配配置时保留自定义路径合同,并在预览和实际规划中明确告警。
|
||||
"""
|
||||
fileitem, meta, media, source_oper, warning = transfer_context
|
||||
explicit_root = tmp_path / "media"
|
||||
expected_root = explicit_root / relative_path
|
||||
module = FileManagerModule()
|
||||
|
||||
checkpoint = module.plan_transfer(
|
||||
fileitem=fileitem, meta=meta, mediainfo=media,
|
||||
target_storage=target_storage, target_path=explicit_root,
|
||||
transfer_type="copy", library_type_folder=type_folder,
|
||||
library_category_folder=category_folder, source_oper=source_oper,
|
||||
preview=preview,
|
||||
)
|
||||
|
||||
assert checkpoint.root_target_path == expected_root.as_posix()
|
||||
assert Path(checkpoint.final_target_path).is_relative_to(expected_root)
|
||||
assert checkpoint.target_storage == (target_storage or "local")
|
||||
warning.assert_called_once()
|
||||
message = warning.call_args.args[0]
|
||||
assert "未匹配到媒体库目录配置" in message
|
||||
assert f"【{target_storage or 'local'}】{explicit_root}" in message
|
||||
assert f"计算后的目标路径:{expected_root}" in message
|
||||
assert f"源路径:【local】{fileitem.path}" in message
|
||||
assert "自动匹配" in message
|
||||
assert source_oper.mock_calls == []
|
||||
assert not explicit_root.exists()
|
||||
|
||||
|
||||
def test_matched_directory_keeps_library_path_without_custom_target_warning(
|
||||
transfer_context: TransferContext, tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
命中目录配置时应保留 links 层级,并且不能误报自定义路径告警。
|
||||
"""
|
||||
fileitem, meta, media, source_oper, warning = transfer_context
|
||||
library_root = tmp_path / "media" / "links" / "电影"
|
||||
directory = TransferDirectoryConf(
|
||||
name="电影", library_path=library_root.as_posix(),
|
||||
library_storage="local", transfer_type="copy", media_type="电影",
|
||||
library_type_folder=False, library_category_folder=True,
|
||||
)
|
||||
|
||||
checkpoint = FileManagerModule().plan_transfer(
|
||||
fileitem=fileitem, meta=meta, mediainfo=media,
|
||||
target_directory=directory, target_path=tmp_path / "media",
|
||||
source_oper=source_oper,
|
||||
)
|
||||
|
||||
assert checkpoint.root_target_path == (library_root / "华语电影").as_posix()
|
||||
warning.assert_not_called()
|
||||
assert source_oper.mock_calls == []
|
||||
assert not library_root.exists()
|
||||
|
||||
|
||||
def test_unmatched_automatic_target_rejects_without_storage_root_fallback(
|
||||
transfer_context: TransferContext,
|
||||
) -> None:
|
||||
"""
|
||||
没有显式目标路径时必须业务拒绝,不能自行生成存储根下的整理计划。
|
||||
"""
|
||||
fileitem, meta, media, source_oper, warning = transfer_context
|
||||
|
||||
with pytest.raises(TransferPlanningRejectedError, match="未找到有效的媒体库目录"):
|
||||
FileManagerModule().plan_transfer(
|
||||
fileitem=fileitem, meta=meta, mediainfo=media,
|
||||
transfer_type="copy", library_type_folder=True,
|
||||
library_category_folder=True, source_oper=source_oper,
|
||||
)
|
||||
|
||||
warning.assert_not_called()
|
||||
assert source_oper.mock_calls == []
|
||||
Reference in New Issue
Block a user