mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-04 14:24:58 +08:00
fix(download): treat blank save path as default directory
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
InfinityPacer
parent
dd90883c5d
commit
338687bee8
@@ -839,6 +839,21 @@ def _validate_download_save_path_value(
|
||||
raise ValueError("保存路径不在允许的下载目录范围内")
|
||||
|
||||
|
||||
def specified_download_save_path(save_path: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
返回调用方真正指定的下载保存目录,空白值视为未指定。
|
||||
|
||||
旧版订阅编辑界面会把清空的保存目录持久化为空字符串,它表示使用默认下载目录,
|
||||
不能当作非法路径交给 validate_download_save_path 拒绝。
|
||||
|
||||
:param save_path: 订阅、事件或接口传入的保存目录
|
||||
:return: 原样保留的非空保存目录;空值或纯空白返回 None
|
||||
"""
|
||||
if save_path is None or not str(save_path).strip():
|
||||
return None
|
||||
return save_path
|
||||
|
||||
|
||||
def validate_download_save_path(save_path: str) -> str:
|
||||
"""
|
||||
校验用户传入的下载保存目录,/download/paths 暴露的下载目录配置是允许写入的公共合同。
|
||||
|
||||
@@ -9,7 +9,10 @@ from typing import Any, Callable, Dict, Optional, Set, Tuple, Union, cast
|
||||
from urllib.parse import urlencode, urljoin, urlparse
|
||||
|
||||
from app.application.configuration import get_chain_runtime_config_snapshot
|
||||
from app.application.directory import validate_download_save_path
|
||||
from app.application.directory import (
|
||||
specified_download_save_path,
|
||||
validate_download_save_path,
|
||||
)
|
||||
from app.application.download.admission import SubscriptionDownloadGovernance
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
|
||||
from app.application.torrent.download import TorrentHelper
|
||||
@@ -293,6 +296,8 @@ class _DownloadResourceOwner(_DownloadOwnerBase):
|
||||
username: Optional[str],
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""应用资源下载事件覆盖,并校验事件返回的下载目录。"""
|
||||
# 空白保存目录表示使用默认下载目录,事件订阅方也只会看到 None。
|
||||
save_path = specified_download_save_path(save_path)
|
||||
meta = context.meta_info
|
||||
media = context.media_info
|
||||
event_data = ResourceDownloadEventData(
|
||||
@@ -319,7 +324,9 @@ class _DownloadResourceOwner(_DownloadOwnerBase):
|
||||
)
|
||||
return save_path, "下载被事件取消"
|
||||
if event_data.options and "save_path" in event_data.options:
|
||||
save_path = cast(Optional[str], event_data.options.get("save_path"))
|
||||
save_path = specified_download_save_path(
|
||||
cast(Optional[str], event_data.options.get("save_path"))
|
||||
)
|
||||
if save_path is None:
|
||||
return None, None
|
||||
try:
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.application.configuration import get_chain_runtime_config_snapshot
|
||||
from app.application.directory import (
|
||||
DirectoryHelper,
|
||||
build_media_download_path,
|
||||
specified_download_save_path,
|
||||
validate_download_save_path,
|
||||
)
|
||||
from app.application.torrent.download import TorrentHelper
|
||||
@@ -321,6 +322,7 @@ class DownloadSubtitleOwner(_DownloadOwnerBase):
|
||||
根据媒体信息解析下载目录。
|
||||
"""
|
||||
storage = 'local'
|
||||
save_path = specified_download_save_path(save_path)
|
||||
if save_path is not None:
|
||||
try:
|
||||
validated_save_path = validate_download_save_path(save_path)
|
||||
|
||||
@@ -577,6 +577,69 @@ def test_download_single_supplements_category_before_download_event(monkeypatch)
|
||||
assert context.media_info.tmdb_id == 12345
|
||||
|
||||
|
||||
@pytest.mark.parametrize("save_path", ["", " "])
|
||||
def test_resource_download_event_treats_blank_save_path_as_default(monkeypatch, save_path):
|
||||
"""旧订阅持久化的空白保存目录表示使用默认目录,不能被路径校验拒绝。"""
|
||||
captured = {}
|
||||
|
||||
def keep_event(_event_type, event_data):
|
||||
"""记录事件订阅方看到的保存目录。"""
|
||||
captured["save_path"] = event_data.options["save_path"]
|
||||
return SimpleNamespace(event_data=event_data)
|
||||
|
||||
monkeypatch.setattr(eventmanager, "send_event", keep_event)
|
||||
context = Context(
|
||||
meta_info=MetaInfo("测试剧集 S01E01"),
|
||||
media_info=MediaInfo(type=MediaType.TV, title="测试剧集"),
|
||||
torrent_info=TorrentInfo(title="测试剧集 S01E01"),
|
||||
)
|
||||
|
||||
result = DownloadChain._apply_resource_download_event(
|
||||
context, None, None, "Subscribe|{}", None, save_path, None, None,
|
||||
)
|
||||
|
||||
assert result == (None, None)
|
||||
assert captured["save_path"] is None
|
||||
|
||||
|
||||
def test_resource_download_event_treats_blank_override_as_default(monkeypatch):
|
||||
"""事件把保存目录改成空白时同样回到默认目录。"""
|
||||
|
||||
def blank_override(_event_type, event_data):
|
||||
"""模拟插件清空保存目录。"""
|
||||
event_data.options["save_path"] = " "
|
||||
return SimpleNamespace(event_data=event_data)
|
||||
|
||||
monkeypatch.setattr(eventmanager, "send_event", blank_override)
|
||||
context = Context(
|
||||
meta_info=MetaInfo("测试剧集 S01E01"),
|
||||
media_info=MediaInfo(type=MediaType.TV, title="测试剧集"),
|
||||
torrent_info=TorrentInfo(title="测试剧集 S01E01"),
|
||||
)
|
||||
|
||||
result = DownloadChain._apply_resource_download_event(
|
||||
context, None, None, None, None, "/downloads/tv", None, None,
|
||||
)
|
||||
|
||||
assert result == (None, None)
|
||||
|
||||
|
||||
def test_resource_download_event_still_rejects_invalid_save_path(monkeypatch):
|
||||
"""非空白的非法保存目录仍按白名单拒绝。"""
|
||||
monkeypatch.setattr(eventmanager, "send_event", lambda *_args: None)
|
||||
context = Context(
|
||||
meta_info=MetaInfo("测试剧集 S01E01"),
|
||||
media_info=MediaInfo(type=MediaType.TV, title="测试剧集"),
|
||||
torrent_info=TorrentInfo(title="测试剧集 S01E01"),
|
||||
)
|
||||
|
||||
result = DownloadChain._apply_resource_download_event(
|
||||
context, None, None, None, None, "relative/path", None, None,
|
||||
)
|
||||
|
||||
assert result == ("relative/path", "保存路径必须是绝对路径")
|
||||
|
||||
|
||||
def test_download_single_persists_custom_words_snapshot(monkeypatch):
|
||||
"""下载成功登记历史时,应把传入的订阅识别词原样存入快照,供整理时原样复现识别。"""
|
||||
captured = {}
|
||||
|
||||
@@ -4,7 +4,11 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.directory import DirectoryHelper, validate_download_save_path
|
||||
from app.application.directory import (
|
||||
DirectoryHelper,
|
||||
specified_download_save_path,
|
||||
validate_download_save_path,
|
||||
)
|
||||
from app.chain.download.subtitle import DownloadSubtitleOwner
|
||||
from app.domain.context import MediaInfo
|
||||
from app.schemas.system import TransferDirectoryConf
|
||||
@@ -139,3 +143,18 @@ def test_resolve_media_download_dir_surfaces_unknown_alias_error():
|
||||
with patch.object(DirectoryHelper, "get_download_dirs", return_value=dirs):
|
||||
result = DownloadSubtitleOwner._resolve_media_download_dir(media, save_path="不存在的库")
|
||||
assert result == (None, None, "未找到名为「不存在的库」的下载目录")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("value", "expected"), [(None, None), ("", None), (" ", None), ("剧集库", "剧集库")])
|
||||
def test_specified_download_save_path_treats_blank_as_unspecified(value, expected):
|
||||
"""空白保存目录视为未指定,非空值原样交给后续校验。"""
|
||||
assert specified_download_save_path(value) == expected
|
||||
|
||||
|
||||
def test_resolve_media_download_dir_uses_default_dir_for_blank_save_path():
|
||||
"""空字符串保存目录走媒体默认下载目录,而不是报“保存路径不能为空”。"""
|
||||
default_dir = TransferDirectoryConf(name="剧集库", priority=1, storage="local", download_path="/downloads/tv")
|
||||
media = MediaInfo(type=MediaType.TV, title="测试剧集")
|
||||
with patch.object(DirectoryHelper, "get_dir", return_value=default_dir):
|
||||
result = DownloadSubtitleOwner._resolve_media_download_dir(media, save_path="")
|
||||
assert result == ("local", Path("/downloads/tv"), "")
|
||||
|
||||
Reference in New Issue
Block a user