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

859 lines
29 KiB
Python
Raw Permalink 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.
"""
验证下载器路径映射、远程存储适配与任务状态查询。
"""
import sys
import types
from enum import Enum
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
def _load_downloader_base():
"""
隔离宿主服务依赖,加载真实下载器路径映射实现。
"""
repo_root = Path(__file__).resolve().parents[1]
app_module = types.ModuleType("app")
app_module.__path__ = []
helper_module = types.ModuleType("app.helper")
helper_module.__path__ = []
runtime_module = types.ModuleType("app.runtime")
runtime_module.__path__ = []
runtime_extensions_module = types.ModuleType("app.runtime.extensions")
runtime_extensions_module.__path__ = []
service_module = types.ModuleType("app.runtime.extensions.service")
log_module = types.ModuleType("app.runtime.log")
schemas_module = types.ModuleType("app.schemas")
schemas_module.__path__ = []
schema_message_module = types.ModuleType("app.schemas.message")
schema_system_module = types.ModuleType("app.schemas.system")
schema_types_module = types.ModuleType("app.schemas.types")
utils_module = types.ModuleType("app.utils")
utils_module.__path__ = []
mixins_module = types.ModuleType("app.runtime.reload")
class StorageSchema(Enum):
"""
提供路径映射识别所需的存储类型。
"""
# 本地文件存储
Local = "local"
# 通过 Rclone 挂载的存储
Rclone = "rclone"
# 通过 SMB 访问的共享存储
SMB = "smb"
class _ConfigReloadMixin:
"""
隔离配置热重载的运行时依赖。
"""
pass
class _Logger:
"""
隔离模块基类加载时使用的无副作用日志桩。
"""
def error(self, *_args, **_kwargs):
"""
忽略测试范围外的错误日志输出。
"""
pass
class _ServiceConfigHelper:
"""
使用空配置隔离下载器之外的服务初始化。
"""
@staticmethod
def get_downloader_configs():
"""
不从真实配置加载下载器。
"""
return []
@staticmethod
def get_notification_configs():
"""
不从真实配置加载通知渠道。
"""
return []
@staticmethod
def get_mediaserver_configs():
"""
不从真实配置加载媒体服务器。
"""
return []
schema_types_module.StorageSchema = StorageSchema
schema_types_module.ModuleType = Enum("ModuleType", {"Downloader": "downloader"})
schema_types_module.DownloaderType = Enum("DownloaderType", {"Qbittorrent": "Qbittorrent"})
schema_types_module.MediaServerType = Enum("MediaServerType", {"Emby": "Emby"})
schema_types_module.NotificationChannel = Enum("NotificationChannel", {"Telegram": "telegram"})
schema_types_module.OtherModulesType = Enum("OtherModulesType", {"Subtitle": "subtitle"})
schema_types_module.MediaRecognizeType = Enum(
"MediaRecognizeType", {"TheMovieDb": "themoviedb"}
)
schema_types_module.SystemConfigKey = Enum(
"SystemConfigKey",
{
"Downloaders": "Downloaders",
"Notifications": "Notifications",
"MediaServers": "MediaServers",
},
)
service_module.ServiceConfigHelper = _ServiceConfigHelper
log_module.logger = _Logger()
mixins_module.ConfigReloadMixin = _ConfigReloadMixin
schema_message_module.Message = object
schema_system_module.NotificationConf = object
schema_system_module.MediaServerConf = object
schema_system_module.DownloaderConf = object
app_module.helper = helper_module
app_module.runtime = runtime_module
app_module.schemas = schemas_module
app_module.utils = utils_module
helper_module.service = service_module
runtime_module.extensions = runtime_extensions_module
runtime_module.log = log_module
runtime_module.reload = mixins_module
runtime_extensions_module.service_config = service_module
schemas_module.message = schema_message_module
schemas_module.system = schema_system_module
schemas_module.types = schema_types_module
utils_module.mixins = mixins_module
stub_modules = {
"app": app_module,
"app.helper": helper_module,
"app.runtime": runtime_module,
"app.runtime.extensions": runtime_extensions_module,
"app.runtime.extensions.service": service_module,
"app.runtime.log": log_module,
"app.schemas": schemas_module,
"app.schemas.message": schema_message_module,
"app.schemas.system": schema_system_module,
"app.schemas.types": schema_types_module,
"app.utils": utils_module,
"app.runtime.reload": mixins_module,
}
module_path = repo_root / "app" / "modules" / "__init__.py"
module_spec = __import__("importlib.util").util.spec_from_file_location(
"_test_downloader_base_module",
module_path,
)
module = __import__("importlib.util").util.module_from_spec(module_spec)
assert module_spec and module_spec.loader
with patch.dict(sys.modules, stub_modules):
module_spec.loader.exec_module(module)
return module._DownloaderBase
def _load_transmission_module():
"""
隔离下载器客户端与宿主依赖,加载真实 Transmission 模块。
"""
repo_root = Path(__file__).resolve().parents[1]
app_module = types.ModuleType("app")
app_module.__path__ = []
core_module = types.ModuleType("app.core")
core_module.__path__ = []
domain_module = types.ModuleType("app.domain")
domain_module.__path__ = []
foundation_module = types.ModuleType("app.foundation")
foundation_module.__path__ = []
torrent_rules_module = types.ModuleType("app.domain.torrent")
size_tools_module = types.ModuleType("app.foundation.size")
temporal_tools_module = types.ModuleType("app.foundation.temporal")
runtime_module = types.ModuleType("app.runtime")
runtime_module.__path__ = []
cache_module = types.ModuleType("app.runtime.cache")
runtime_settings_module = types.ModuleType("app.runtime.settings")
base_module = types.ModuleType("app.modules._base")
base_module.__path__ = []
base_downloader_module = types.ModuleType("app.modules._base.downloader")
modules_module = types.ModuleType("app.modules")
modules_module.__path__ = []
transmission_package_module = types.ModuleType("app.modules.transmission")
transmission_package_module.__path__ = []
transmission_client_module = types.ModuleType("app.modules.transmission.transmission")
schemas_module = types.ModuleType("app.schemas")
schemas_module.__path__ = []
schema_dashboard_module = types.ModuleType("app.schemas.dashboard")
schema_transfer_module = types.ModuleType("app.schemas.transfer")
schema_types_module = types.ModuleType("app.schemas.types")
config_module = types.ModuleType("app.runtime.config")
metainfo_module = types.ModuleType("app.domain.metainfo")
log_module = types.ModuleType("app.runtime.log")
transmission_rpc_module = types.ModuleType("transmission_rpc")
torrentool_module = types.ModuleType("torrentool")
torrentool_module.__path__ = []
torrentool_torrent_module = types.ModuleType("torrentool.torrent")
class _ModuleBase:
"""
隔离模块注册及生命周期管理。
"""
pass
class _DownloaderBase:
"""
保留下载器模块的泛型基类入口。
"""
def __class_getitem__(cls, _item):
"""
让隔离基类接受生产代码的客户端类型参数。
"""
return cls
class _DownloaderModuleBase(_ModuleBase, _DownloaderBase):
"""
隔离测试用的下载器模块基类桩,与 app.modules._base 行为对齐。
"""
def _get_torrent_info(self, content):
"""
与真实基类一致的种子信息读取,磁力链接不解析。
"""
torrent_content = content
if isinstance(content, Path):
torrent_content = content.read_bytes() if content.exists() else None
torrent_info = None
if torrent_content and not torrent_rules_module.is_magnet_link(torrent_content):
torrent_info = torrentool_torrent_module.Torrent.from_string(
torrent_content
)
return torrent_info, torrent_content
@staticmethod
def _normalize_query_status(status):
"""
与真实基类一致的查询状态归一,返回隔离测试枚举。
"""
status_value = getattr(status, "value", status)
status_text = str(status_value or "").strip().lower()
if not status_text or status_text in {"all", "全部"}:
return TorrentQueryStatus.ALL
if status_text in {"transfer", "transferring"}:
return TorrentQueryStatus.TRANSFER
if status_text in {"downloading"}:
return TorrentQueryStatus.DOWNLOADING
if status_text in {"complete", "completed", "seeding", "完成", "已完成"}:
return TorrentQueryStatus.COMPLETED
if status_text in {"pause", "paused", "暂停", "已暂停"}:
return TorrentQueryStatus.PAUSED
return TorrentQueryStatus.ALL
class _TransferTorrent:
"""
保留整理任务构造时传入的字段,供断言检查。
"""
def __init__(self, **kwargs):
"""
保存模块实际返回的整理任务字段。
"""
self.__dict__.update(kwargs)
class _DownloadingTorrent:
"""
保留下载中任务构造时传入的字段,供断言检查。
"""
def __init__(self, **kwargs):
"""
保存模块实际返回的下载中任务字段。
"""
self.__dict__.update(kwargs)
class _DownloaderTorrent:
"""
保留统一下载任务构造时传入的字段,供断言检查。
"""
def __init__(self, **kwargs):
"""
保存模块实际返回的统一下载任务字段。
"""
self.__dict__.update(kwargs)
class _DownloaderFile:
"""
隔离加载 Transmission 时提供下载器文件 DTO 的最小契约。
"""
@classmethod
def model_validate(cls, value):
"""
按生产 DTO 入口把 SDK 对象投影为测试快照。
"""
if isinstance(value, cls):
return value
payload = value if isinstance(value, dict) else vars(value)
instance = cls()
instance.__dict__.update(payload)
return instance
class TorrentStatus(Enum):
"""
提供下载器历史查询入口使用的状态。
"""
# 等待或正在整理的任务
TRANSFER = "transfer"
# 正在下载的任务
DOWNLOADING = "downloading"
class TorrentQueryStatus(Enum):
"""
提供统一下载器查询入口使用的状态。
"""
# 所有状态的任务
ALL = "all"
# 等待或正在整理的任务
TRANSFER = "transfer"
# 正在下载的任务
DOWNLOADING = "downloading"
# 已经完成下载的任务
COMPLETED = "completed"
# 暂停下载的任务
PAUSED = "paused"
class DownloadTaskState(Enum):
"""
提供统一下载任务返回值使用的状态。
"""
# 正在下载的任务
DOWNLOADING = "downloading"
# 暂停下载的任务
PAUSED = "paused"
# 已经完成下载的任务
COMPLETED = "completed"
class _Logger:
"""
隔离 Transmission 测试的运行日志输出。
"""
def debug(self, *_args, **_kwargs):
"""
忽略测试范围外的调试日志。
"""
pass
def info(self, *_args, **_kwargs):
"""
忽略测试范围外的普通日志。
"""
pass
def error(self, *_args, **_kwargs):
"""
忽略测试范围外的错误日志。
"""
pass
class _MetaInfo:
"""
提供下载任务展示所需的最小媒体元数据。
"""
def __init__(self, name):
"""
保留种子名称并将未解析的媒体字段置空。
"""
self.name = name
self.year = None
self.season_episode = ""
self.episode_list = []
def _is_magnet_link(value):
"""
按生产领域规则识别测试磁力链接。
"""
return isinstance(value, str) and value.startswith("magnet:")
def _format_size(value):
"""
返回隔离测试需要的简化容量文本。
"""
return str(value)
def _format_duration(value):
"""
返回隔离测试需要的简化时长文本。
"""
return str(value)
class _FileCache:
"""
隔离真实文件缓存,避免读取运行时状态。
"""
def get(self, *_args, **_kwargs):
"""
固定返回缓存未命中。
"""
return None
transmission_client_module.Transmission = object
cache_module.FileCache = _FileCache
schema_transfer_module.TransferTorrent = _TransferTorrent
schema_transfer_module.DownloadingTorrent = _DownloadingTorrent
schema_transfer_module.DownloaderTorrent = _DownloaderTorrent
schema_transfer_module.DownloaderFile = _DownloaderFile
schema_dashboard_module.DownloaderInfo = object
schema_types_module.TorrentStatus = TorrentStatus
schema_types_module.TorrentQueryStatus = TorrentQueryStatus
schema_types_module.DownloadTaskState = DownloadTaskState
schema_types_module.ModuleType = Enum("ModuleType", {"Downloader": "downloader"})
schema_types_module.DownloaderType = Enum(
"DownloaderType", {"Transmission": "Transmission"}
)
config_module.settings = SimpleNamespace(TORRENT_TAG="moviepilot-tag")
runtime_settings_module.get_runtime_setting = lambda key, default=None: getattr(
config_module.settings, key, default
)
metainfo_module.MetaInfo = _MetaInfo
log_module.logger = _Logger()
modules_module._ModuleBase = _ModuleBase
modules_module._DownloaderBase = _DownloaderBase
modules_module._base = base_module
base_module.downloader = base_downloader_module
base_downloader_module._DownloaderModuleBase = _DownloaderModuleBase
torrent_rules_module.is_magnet_link = _is_magnet_link
size_tools_module.format_compact_size = _format_size
temporal_tools_module.format_duration = _format_duration
transmission_rpc_module.File = object
torrentool_torrent_module.Torrent = SimpleNamespace(
from_string=lambda _content: SimpleNamespace(name="test", total_size=1)
)
app_module.core = core_module
app_module.domain = domain_module
app_module.foundation = foundation_module
app_module.modules = modules_module
app_module.runtime = runtime_module
app_module.schemas = schemas_module
domain_module.torrent = torrent_rules_module
foundation_module.size = size_tools_module
foundation_module.temporal = temporal_tools_module
core_module.cache = cache_module
core_module.config = config_module
core_module.metainfo = metainfo_module
runtime_module.cache = cache_module
runtime_module.config = config_module
runtime_module.log = log_module
runtime_module.settings = runtime_settings_module
modules_module.transmission = transmission_package_module
transmission_package_module.transmission = transmission_client_module
schemas_module.dashboard = schema_dashboard_module
schemas_module.transfer = schema_transfer_module
schemas_module.types = schema_types_module
torrentool_module.torrent = torrentool_torrent_module
stub_modules = {
"app": app_module,
"app.core": core_module,
"app.domain": domain_module,
"app.domain.torrent": torrent_rules_module,
"app.foundation": foundation_module,
"app.foundation.size": size_tools_module,
"app.foundation.temporal": temporal_tools_module,
"app.runtime.cache": cache_module,
"app.runtime.config": config_module,
"app.runtime": runtime_module,
"app.domain.metainfo": metainfo_module,
"app.runtime.log": log_module,
"app.runtime.settings": runtime_settings_module,
"app.modules": modules_module,
"app.modules._base": base_module,
"app.modules._base.downloader": base_downloader_module,
"app.modules.transmission": transmission_package_module,
"app.modules.transmission.transmission": transmission_client_module,
"app.schemas": schemas_module,
"app.schemas.dashboard": schema_dashboard_module,
"app.schemas.transfer": schema_transfer_module,
"app.schemas.types": schema_types_module,
"transmission_rpc": transmission_rpc_module,
"torrentool": torrentool_module,
"torrentool.torrent": torrentool_torrent_module,
}
module_path = repo_root / "app" / "modules" / "transmission" / "__init__.py"
module_spec = __import__("importlib.util").util.spec_from_file_location(
"_test_transmission_module",
module_path,
)
module = __import__("importlib.util").util.module_from_spec(module_spec)
assert module_spec and module_spec.loader
with patch.dict(sys.modules, stub_modules):
module_spec.loader.exec_module(module)
return module.TransmissionModule, TorrentStatus, module
DownloaderBase = _load_downloader_base()
TransmissionModule, TransmissionTorrentStatus, transmission_module = _load_transmission_module()
def _build_base(path_mapping):
"""
构造只依赖显式路径映射的下载器基类。
"""
downloader = DownloaderBase.__new__(DownloaderBase)
downloader.get_config = MagicMock(
return_value=SimpleNamespace(path_mapping=path_mapping)
)
return downloader
def _build_transmission_module(server):
"""
注入离线下载器及固定路径映射,避免初始化真实服务。
"""
module = TransmissionModule.__new__(TransmissionModule)
module.get_instances = MagicMock(return_value={"tr": server})
module.get_instance = MagicMock(return_value=server)
module.normalize_return_path = MagicMock(
side_effect=lambda path, _downloader: str(path).replace(
"/mnt/raid5/home_lt999lt", "/media", 1
)
)
return module
def test_normalize_path_maps_moviepilot_path_to_downloader_path():
"""
MoviePilot 访问路径应转换为下载器容器内路径。
"""
downloader = _build_base([("/media", "/mnt/raid5/home_lt999lt")])
result = downloader.normalize_path(Path("/media/video/downloads/movie"), "tr")
assert result == "/mnt/raid5/home_lt999lt/video/downloads/movie"
def test_normalize_return_path_maps_downloader_path_back_to_moviepilot_path():
"""
下载器容器内路径应转换回 MoviePilot 可访问路径。
"""
downloader = _build_base([("/media", "/mnt/raid5/home_lt999lt")])
result = downloader.normalize_return_path(
Path("/mnt/raid5/home_lt999lt/video/downloads/TV/Show.mkv"), "tr"
)
assert result == "/media/video/downloads/TV/Show.mkv"
def test_path_mapping_matches_complete_path_segment_only():
"""
路径映射只应命中完整路径段,避免误伤相似前缀。
"""
downloader = _build_base([("/media", "/mnt/media")])
result = downloader.normalize_return_path(Path("/mnt/media2/Show.mkv"), "tr")
assert result == "/mnt/media2/Show.mkv"
def test_blank_path_mapping_entry_is_ignored():
"""
空路径映射项应被忽略,继续使用后续有效配置。
"""
downloader = _build_base(
[("", "/downloads"), ("/media2", ""), ("/media", "/mnt/media")]
)
result = downloader.normalize_return_path(Path("/mnt/media/Show.mkv"), "tr")
assert result == "/media/Show.mkv"
def test_normalize_path_strips_storage_prefix_after_mapping():
"""
带存储类型前缀的路径映射后应返回下载器原生路径。
"""
downloader = _build_base([("local:/media", "/downloads")])
result = downloader.normalize_path(Path("local:/media/movie"), "qb")
assert result == "/downloads/movie"
def test_smb_mapping_round_trip_preserves_storage_identity():
"""
SMB 共享路径下发为下载器原生路径,返回时必须恢复存储身份。
"""
downloader = _build_base([("smb:/incoming", "/downloads")])
outbound = downloader.normalize_path(Path("smb:/incoming/Movie/movie.mkv"), "qb")
inbound = downloader.normalize_return_path(Path(outbound), "qb")
assert outbound == "/downloads/Movie/movie.mkv"
assert inbound == "smb:/incoming/Movie/movie.mkv"
assert downloader.normalize_return_path(Path("/downloads2/movie.mkv"), "qb") == "/downloads2/movie.mkv"
def test_return_path_keeps_local_mapping_compatible():
"""
旧 local 前缀映射仍返回普通本地路径。
"""
downloader = _build_base([("local:/media", "/downloads")])
assert downloader.normalize_return_path(Path("/downloads/movie.mkv"), "qb") == "/media/movie.mkv"
def test_completed_torrents_return_moviepilot_accessible_path():
"""
Transmission 已完成任务返回的路径字段均应为 MoviePilot 可访问路径。
"""
server = MagicMock()
server.get_completed_torrents.return_value = [
SimpleNamespace(
name="Show.S01E01.mkv",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/TV",
hashString="hash-tr",
labels=[],
progress=100,
status="seeding",
)
]
module = _build_transmission_module(server)
torrents = module.list_torrents(status=TransmissionTorrentStatus.TRANSFER)
assert torrents[0].path == Path("/media/video/downloads/TV/Show.S01E01.mkv")
assert torrents[0].save_path == "/media/video/downloads/TV"
assert torrents[0].content_path == "/media/video/downloads/TV/Show.S01E01.mkv"
@pytest.mark.parametrize(
("progress_fields", "expected_progress"),
[
({"percent_done": 0}, 0),
({"percent_done": 0.42}, 42),
({"percent_done": 1}, 100),
({"percent_done": 42}, 42),
({"percent_done": 100}, 100),
({"progress": 42, "percent_done": 1}, 42),
({"progress": 0, "percent_done": 1}, 0),
({"progress": None, "percent_done": 1}, 100),
({}, 0),
],
)
def test_transmission_progress_uses_percentage_contract(progress_fields, expected_progress):
"""
原始进度须转换为百分比,已有 progress(含零值)须优先保留。
"""
server = MagicMock()
server.get_completed_torrents.return_value = [
SimpleNamespace(
name="Movie",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
hashString="hash-tr",
labels=[],
status="seeding",
**progress_fields,
)
]
module = _build_transmission_module(server)
torrents = module.list_torrents(status=TransmissionTorrentStatus.TRANSFER)
assert torrents[0].progress == expected_progress
def test_hash_lookup_return_moviepilot_accessible_path():
"""
Transmission 按 Hash 查询时返回的路径字段均应完成路径映射。
"""
server = MagicMock()
server.get_torrents.return_value = (
[
SimpleNamespace(
name="Movie",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
hashString="hash-tr",
total_size=1024,
labels=[],
progress=100,
status="seeding",
)
],
False,
)
module = _build_transmission_module(server)
torrents = module.list_torrents(hashs=["hash-tr"], downloader="tr")
assert torrents[0].path == Path("/media/video/downloads/movie/Movie")
assert torrents[0].save_path == "/media/video/downloads/movie"
assert torrents[0].content_path == "/media/video/downloads/movie/Movie"
def test_list_torrents_normalizes_parsed_year_to_string(monkeypatch):
"""
种子名称解析出的整数年份应按下载任务契约转换为字符串。
"""
server = MagicMock()
server.get_torrents.return_value = (
[
SimpleNamespace(
name="The.Fifth.Republic.2005.1080p.WEB-DL.HEVC.AAC",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
hashString="hash-with-year",
total_size=1024,
labels=[],
progress=100,
status="seeding",
)
],
False,
)
monkeypatch.setattr(
transmission_module,
"MetaInfo",
lambda _name: SimpleNamespace(
name="The Fifth Republic",
year=2005,
season_episode="",
),
)
module = _build_transmission_module(server)
torrents = module.list_torrents(include_all_tags=True)
assert torrents[0].year == "2005"
def test_list_torrents_ignores_missing_transmission_limit_fields():
"""
Transmission 任务缺少做种限制字段时不应中断列表查询。
"""
class _TorrentWithMissingLimitFields:
"""
模拟 transmission-rpc 属性访问缺失字段时抛出 KeyError 的任务对象。
"""
name = "Movie"
download_dir = "/mnt/raid5/home_lt999lt/video/downloads/movie"
hashString = "hash-missing-limit"
total_size = 1024
labels = []
progress = 100
status = "seeding"
@property
def seed_ratio_limit(self):
"""
模拟 seedRatioLimit 原始字段缺失。
"""
raise KeyError("seedRatioLimit")
@property
def seed_idle_limit(self):
"""
模拟 seedIdleLimit 原始字段缺失。
"""
raise KeyError("seedIdleLimit")
server = MagicMock()
server.get_torrents.return_value = (
[_TorrentWithMissingLimitFields()],
False,
)
module = _build_transmission_module(server)
torrents = module.list_torrents()
assert torrents[0].hash == "hash-missing-limit"
assert torrents[0].ratio_limit is None
assert torrents[0].seeding_time_limit is None
def test_all_torrents_include_completed_and_downloading_states():
"""
Transmission 默认列表应同时包含已完成和下载中的任务状态。
"""
server = MagicMock()
server.get_torrents.return_value = (
[
SimpleNamespace(
name="Completed",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
hashString="hash-completed",
total_size=1024,
labels=[],
progress=100,
status="seed pending",
),
SimpleNamespace(
name="Downloading",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
hashString="hash-downloading",
total_size=2048,
labels=[],
progress=50,
status="downloading",
rate_download=1024,
rate_upload=0,
left_until_done=1024,
),
],
False,
)
module = _build_transmission_module(server)
torrents = module.list_torrents()
assert ["completed", "downloading"] == [torrent.state for torrent in torrents]
assert ["hash-completed", "hash-downloading"] == [
torrent.hash for torrent in torrents
]
server.get_torrents.assert_called_once_with(tags="moviepilot-tag")
def test_include_all_tags_removes_builtin_tag_filter():
"""
查询全部标签任务时不应附加 MoviePilot 内置标签过滤。
"""
server = MagicMock()
server.get_torrents.return_value = (
[
SimpleNamespace(
name="External",
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
hashString="hash-external",
total_size=1024,
labels=["external"],
progress=100,
status="seeding",
)
],
False,
)
module = _build_transmission_module(server)
torrents = module.list_torrents(include_all_tags=True)
assert ["hash-external"] == [torrent.hash for torrent in torrents]
server.get_torrents.assert_called_once_with(tags=None)