mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
fix: enable incomplete file suffix for downloaders
This commit is contained in:
@@ -153,6 +153,20 @@ class Qbittorrent:
|
|||||||
logger.error(f"同步下载Cookie出错:{str(err)}")
|
logger.error(f"同步下载Cookie出错:{str(err)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __enable_incomplete_file_suffix(qbt: Client) -> None:
|
||||||
|
"""
|
||||||
|
开启未完成文件后缀,避免监控流程提前整理仍在下载的媒体文件。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
preferences = qbt.app_preferences() or {}
|
||||||
|
if isinstance(preferences, dict) and preferences.get("incomplete_files_ext") is True:
|
||||||
|
return
|
||||||
|
qbt.app_set_preferences({"incomplete_files_ext": True})
|
||||||
|
logger.info("已开启 qbittorrent 未完成文件追加 .!qB 后缀")
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(f"开启 qbittorrent 未完成文件后缀失败:{str(err)}")
|
||||||
|
|
||||||
def is_inactive(self) -> bool:
|
def is_inactive(self) -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否需要重连
|
判断是否需要重连
|
||||||
@@ -198,6 +212,7 @@ class Qbittorrent:
|
|||||||
stack_trace = "".join(traceback.format_exception(None, e, e.__traceback__))[:2000]
|
stack_trace = "".join(traceback.format_exception(None, e, e.__traceback__))[:2000]
|
||||||
logger.error(f"qbittorrent 登录失败:{str(e)}\n{stack_trace}")
|
logger.error(f"qbittorrent 登录失败:{str(e)}\n{stack_trace}")
|
||||||
return None
|
return None
|
||||||
|
self.__enable_incomplete_file_suffix(qbt)
|
||||||
return qbt
|
return qbt
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"qbittorrent 连接出错:{str(err)}")
|
logger.error(f"qbittorrent 连接出错:{str(err)}")
|
||||||
|
|||||||
@@ -41,6 +41,25 @@ class Transmission:
|
|||||||
self._password = password
|
self._password = password
|
||||||
self.trc = self.__login_transmission()
|
self.trc = self.__login_transmission()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __enable_incomplete_file_suffix(trt: Client) -> None:
|
||||||
|
"""
|
||||||
|
开启未完成文件后缀,避免监控流程提前整理仍在下载的媒体文件。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = trt.get_session()
|
||||||
|
getter = getattr(session, "get", None)
|
||||||
|
if callable(getter):
|
||||||
|
rename_partial_files = getter("rename-partial-files")
|
||||||
|
else:
|
||||||
|
rename_partial_files = getattr(session, "rename_partial_files", None)
|
||||||
|
if rename_partial_files is True:
|
||||||
|
return
|
||||||
|
trt.set_session(rename_partial_files=True)
|
||||||
|
logger.info("已开启 transmission 未完成文件追加 .part 后缀")
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(f"开启 transmission 未完成文件后缀失败:{str(err)}")
|
||||||
|
|
||||||
def __login_transmission(self) -> Optional[Client]:
|
def __login_transmission(self) -> Optional[Client]:
|
||||||
"""
|
"""
|
||||||
连接transmission
|
连接transmission
|
||||||
@@ -57,6 +76,7 @@ class Transmission:
|
|||||||
username=self._username,
|
username=self._username,
|
||||||
password=self._password,
|
password=self._password,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
self.__enable_incomplete_file_suffix(trt)
|
||||||
return trt
|
return trt
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"transmission 连接出错:{str(err)}")
|
logger.error(f"transmission 连接出错:{str(err)}")
|
||||||
|
|||||||
@@ -208,6 +208,32 @@ class TestQbittorrentCompat(unittest.TestCase):
|
|||||||
{"Authorization": "Bearer secret-token"},
|
{"Authorization": "Bearer secret-token"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_login_enables_incomplete_file_suffix(self):
|
||||||
|
"""
|
||||||
|
登录成功后应开启未完成文件后缀,避免下载中的媒体文件被提前整理。
|
||||||
|
"""
|
||||||
|
fake_client = MagicMock()
|
||||||
|
fake_client.app_preferences.return_value = {"incomplete_files_ext": False}
|
||||||
|
|
||||||
|
with patch.object(qbittorrent_module.qbittorrentapi, "Client", return_value=fake_client):
|
||||||
|
downloader = Qbittorrent(host="http://127.0.0.1", port=8080, username="admin", password="adminadmin")
|
||||||
|
|
||||||
|
self.assertIs(downloader.qbc, fake_client)
|
||||||
|
fake_client.app_set_preferences.assert_called_once_with({"incomplete_files_ext": True})
|
||||||
|
|
||||||
|
def test_login_skips_incomplete_file_suffix_when_already_enabled(self):
|
||||||
|
"""
|
||||||
|
远端已开启未完成文件后缀时不重复写入全局偏好。
|
||||||
|
"""
|
||||||
|
fake_client = MagicMock()
|
||||||
|
fake_client.app_preferences.return_value = {"incomplete_files_ext": True}
|
||||||
|
|
||||||
|
with patch.object(qbittorrent_module.qbittorrentapi, "Client", return_value=fake_client):
|
||||||
|
downloader = Qbittorrent(host="http://127.0.0.1", port=8080, username="admin", password="adminadmin")
|
||||||
|
|
||||||
|
self.assertIs(downloader.qbc, fake_client)
|
||||||
|
fake_client.app_set_preferences.assert_not_called()
|
||||||
|
|
||||||
def test_add_torrent_accepts_structured_success_response(self):
|
def test_add_torrent_accepts_structured_success_response(self):
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.torrents_add.return_value = {
|
fake_client.torrents_add.return_value = {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
def _load_transmission_client_module():
|
||||||
|
"""
|
||||||
|
使用轻量桩加载 Transmission 客户端封装,避免测试依赖完整应用启动。
|
||||||
|
"""
|
||||||
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
app_module = types.ModuleType("app")
|
||||||
|
app_module.__path__ = []
|
||||||
|
log_module = types.ModuleType("app.log")
|
||||||
|
utils_module = types.ModuleType("app.utils")
|
||||||
|
utils_module.__path__ = []
|
||||||
|
url_module = types.ModuleType("app.utils.url")
|
||||||
|
transmission_rpc_module = types.ModuleType("transmission_rpc")
|
||||||
|
transmission_rpc_session_module = types.ModuleType("transmission_rpc.session")
|
||||||
|
|
||||||
|
class _Logger:
|
||||||
|
"""
|
||||||
|
测试日志桩,仅提供被客户端封装调用的方法。
|
||||||
|
"""
|
||||||
|
def info(self, *_args, **_kwargs):
|
||||||
|
"""
|
||||||
|
忽略信息日志。
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def warning(self, *_args, **_kwargs):
|
||||||
|
"""
|
||||||
|
忽略警告日志。
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def error(self, *_args, **_kwargs):
|
||||||
|
"""
|
||||||
|
忽略错误日志。
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _UrlUtils:
|
||||||
|
"""
|
||||||
|
测试 URL 工具桩,满足按 URL 配置下载器时的解析接口。
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def parse_url_params(url):
|
||||||
|
"""
|
||||||
|
返回固定的 Transmission 连接参数。
|
||||||
|
"""
|
||||||
|
return "http", url, 9091, ""
|
||||||
|
|
||||||
|
log_module.logger = _Logger()
|
||||||
|
url_module.UrlUtils = _UrlUtils
|
||||||
|
transmission_rpc_module.Client = object
|
||||||
|
transmission_rpc_module.Torrent = object
|
||||||
|
transmission_rpc_module.File = object
|
||||||
|
transmission_rpc_session_module.SessionStats = object
|
||||||
|
transmission_rpc_session_module.Session = object
|
||||||
|
|
||||||
|
app_module.log = log_module
|
||||||
|
app_module.utils = utils_module
|
||||||
|
utils_module.url = url_module
|
||||||
|
|
||||||
|
stub_modules = {
|
||||||
|
"app": app_module,
|
||||||
|
"app.log": log_module,
|
||||||
|
"app.utils": utils_module,
|
||||||
|
"app.utils.url": url_module,
|
||||||
|
"transmission_rpc": transmission_rpc_module,
|
||||||
|
"transmission_rpc.session": transmission_rpc_session_module,
|
||||||
|
}
|
||||||
|
|
||||||
|
transmission_path = repo_root / "app" / "modules" / "transmission" / "transmission.py"
|
||||||
|
transmission_spec = importlib.util.spec_from_file_location(
|
||||||
|
"app.modules.transmission.transmission",
|
||||||
|
transmission_path,
|
||||||
|
)
|
||||||
|
transmission_module = importlib.util.module_from_spec(transmission_spec)
|
||||||
|
assert transmission_spec and transmission_spec.loader
|
||||||
|
|
||||||
|
with patch.dict(sys.modules, stub_modules):
|
||||||
|
transmission_spec.loader.exec_module(transmission_module)
|
||||||
|
|
||||||
|
return transmission_module
|
||||||
|
|
||||||
|
|
||||||
|
transmission_module = _load_transmission_client_module()
|
||||||
|
Transmission = transmission_module.Transmission
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransmissionCompat(unittest.TestCase):
|
||||||
|
def test_login_enables_incomplete_file_suffix(self):
|
||||||
|
"""
|
||||||
|
登录成功后应开启未完成文件后缀,避免下载中的媒体文件被提前整理。
|
||||||
|
"""
|
||||||
|
fake_client = MagicMock()
|
||||||
|
fake_client.get_session.return_value = {"rename-partial-files": False}
|
||||||
|
|
||||||
|
with patch.object(transmission_module.transmission_rpc, "Client", return_value=fake_client):
|
||||||
|
downloader = Transmission(host="127.0.0.1", port=9091)
|
||||||
|
|
||||||
|
self.assertIs(downloader.trc, fake_client)
|
||||||
|
fake_client.set_session.assert_called_once_with(rename_partial_files=True)
|
||||||
|
|
||||||
|
def test_login_skips_incomplete_file_suffix_when_already_enabled(self):
|
||||||
|
"""
|
||||||
|
远端已开启未完成文件后缀时不重复写入全局会话配置。
|
||||||
|
"""
|
||||||
|
fake_client = MagicMock()
|
||||||
|
fake_client.get_session.return_value = types.SimpleNamespace(rename_partial_files=True)
|
||||||
|
|
||||||
|
with patch.object(transmission_module.transmission_rpc, "Client", return_value=fake_client):
|
||||||
|
downloader = Transmission(host="127.0.0.1", port=9091)
|
||||||
|
|
||||||
|
self.assertIs(downloader.trc, fake_client)
|
||||||
|
fake_client.set_session.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user