fix: bound downloader connection retries for v3.0.8

This commit is contained in:
jxxghp
2026-09-23 18:13:39 +08:00
parent c68288e897
commit 58bb4961af
10 changed files with 41 additions and 16 deletions

View File

@@ -67,8 +67,11 @@ jobs:
run: |
plugins_revision=$(git ls-remote https://github.com/jxxghp/MoviePilot-Plugins.git refs/heads/main | awk '{print $1}')
resources_revision=$(git ls-remote https://github.com/jxxghp/MoviePilot-Resources.git refs/heads/main | awk '{print $1}')
frontend_digest=$(gh api "repos/jxxghp/MoviePilot-Frontend/releases/tags/${FRONTEND_VERSION}" \
--jq '.assets[] | select(.name == "dist.zip") | .digest')
frontend_release_id=$(gh api "repos/jxxghp/MoviePilot-Frontend/releases/tags/${FRONTEND_VERSION}" \
--jq '.id')
[[ "$frontend_release_id" =~ ^[0-9]+$ ]]
frontend_digest=$(gh api "repos/jxxghp/MoviePilot-Frontend/releases/${frontend_release_id}/assets" \
--jq '.[] | select(.name == "dist.zip") | .digest')
[[ "$plugins_revision" =~ ^[0-9a-f]{40}$ ]]
[[ "$resources_revision" =~ ^[0-9a-f]{40}$ ]]

View File

@@ -17,6 +17,8 @@ from app.foundation import url as url_tools
_TORRENT_TAG_CLEANUP_RETRY_TIMES = 10
_TORRENT_TAG_CLEANUP_RETRY_INTERVAL = 1
_DOWNLOADER_CONNECT_TIMEOUT_SECONDS = 3
_DOWNLOADER_READ_TIMEOUT_SECONDS = 60
class Qbittorrent:
@@ -192,7 +194,7 @@ class Qbittorrent:
def __login_qbittorrent(self) -> Optional[Client]:
"""
连接qbittorrent
连接 qbittorrent;缩短建连等待,关闭 HTTP 重试,并采用地址中显式指定的协议。
:return: qbittorrent对象
"""
if not self._host or not self._port:
@@ -207,7 +209,14 @@ class Qbittorrent:
EXTRA_HEADERS={"Authorization": f"Bearer {self._apikey}"}
if self.__use_api_key_auth() else None,
VERIFY_WEBUI_CERTIFICATE=False,
REQUESTS_ARGS={'timeout': (15, 60)})
REQUESTS_ARGS={
"timeout": (
_DOWNLOADER_CONNECT_TIMEOUT_SECONDS,
_DOWNLOADER_READ_TIMEOUT_SECONDS,
)
},
HTTPADAPTER_ARGS={"max_retries": 0},
FORCE_SCHEME_FROM_HOST=True)
try:
if self.__use_api_key_auth():
qbt.app_version()

View File

@@ -1,4 +1,4 @@
from typing import Optional, Union, Tuple, List
from typing import Any, List, Optional, Tuple, Union, cast
import transmission_rpc
from transmission_rpc import Client, Torrent, File
@@ -65,7 +65,7 @@ class Transmission:
def __login_transmission(self) -> Optional[Client]:
"""
连接transmission
连接 Transmission;缩短 TCP 建连等待,同时保留原有 HTTP 响应读取时限。
:return: transmission对象
"""
if not self._host or not self._port:
@@ -78,7 +78,8 @@ class Transmission:
port=self._port,
username=self._username,
password=self._password,
timeout=60)
# transmission-rpc supports Requests timeout tuples at runtime.
timeout=cast(Any, (3, 60)))
self.__sync_incomplete_file_suffix(trt, enabled=bool(self._rename_partial_files))
return trt
except Exception as err:

View File

@@ -761,7 +761,7 @@ flowchart LR
| 指标 | 当前值 |
|---|---:|
| Python 模块 | 1044 |
| 内部导入边 | 8,881 |
| 内部导入边 | 8,882 |
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
| Direct egress | 53(债务已清零,53 条精确 containment) |

View File

@@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
| 指标 | 当前值 | 解释 |
|---|---:|---|
| 宿主 Python 模块 / 内部依赖边 | 1045 / 8,881 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档,分类、下载资源归类、订阅搜索、整理恢复、Agent 计划、工具视觉、终端生命周期与终端作用域模块的受控依赖 |
| 宿主 Python 模块 / 内部依赖边 | 1045 / 8,882 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档,分类、下载资源归类、订阅搜索、整理恢复、Agent 计划、工具视觉、终端生命周期与终端作用域模块的受控依赖 |
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |

View File

@@ -1084,8 +1084,8 @@
"runtime_only": true
}
},
"edge_count": 8881,
"edge_sha256": "700858f0eb08a886c206c9be9685964b01f0e3412bed7b1c1fa84fa343231a14",
"edge_count": 8882,
"edge_sha256": "fd866881390126c9f4a68c671c9641e27223e23a1b6c242c15042cb3c7d1a087",
"edges": [
"app -> app.foundation",
"app -> app.foundation.environment",
@@ -3040,6 +3040,7 @@
"app.api.endpoints.system -> app.application.configuration",
"app.api.endpoints.system -> app.application.database",
"app.api.endpoints.system -> app.application.image",
"app.api.endpoints.system -> app.application.mediaserver",
"app.api.endpoints.system -> app.application.messaging",
"app.api.endpoints.system -> app.application.messaging.message",
"app.api.endpoints.system -> app.application.network",

View File

@@ -319,13 +319,18 @@ def test_login_uses_api_key_header_without_auth_login():
fake_client = MagicMock()
fake_client.app_version.return_value = "v5.2.0"
with patch.object(qbittorrent_module.qbittorrentapi, "Client", return_value=fake_client) as client_cls:
with patch.object(
qbittorrent_module.qbittorrentapi, "Client", return_value=fake_client
) as client_cls:
downloader = Qbittorrent(host="http://127.0.0.1", port=8080, apikey="secret-token")
assert downloader.qbc is fake_client
fake_client.auth_log_in.assert_not_called()
fake_client.app_version.assert_called_once_with()
assert client_cls.call_args.kwargs["EXTRA_HEADERS"] == {"Authorization": "Bearer secret-token"}
assert client_cls.call_args.kwargs["REQUESTS_ARGS"] == {"timeout": (3, 60)}
assert client_cls.call_args.kwargs["HTTPADAPTER_ARGS"] == {"max_retries": 0}
assert client_cls.call_args.kwargs["FORCE_SCHEME_FROM_HOST"] is True
def test_login_enables_incomplete_file_suffix_by_default():

View File

@@ -59,7 +59,8 @@ _STUB_MODULES = dict([
_stub("app.api.dependencies.auth", get_current_active_superuser=_Dummy,
get_current_active_superuser_async=_Dummy, get_current_active_user_async=_Dummy),
_stub("app.agent.llm", LLMHelper=_Dummy, LLMTestError=_DummyError, LLMTestTimeout=_DummyError),
_stub("app.application.mediaserver", MediaServerHelper=_Dummy),
_stub("app.application.mediaserver", MediaServerHelper=_Dummy,
get_mediaserver_configs=lambda: []),
_stub("app.application.messaging.message", MessageHelper=_Dummy),
_stub("app.runtime.progress", ProgressHelper=_Dummy, AsyncProgressHelper=_Dummy),
_stub("app.application.rules", RuleHelper=_Dummy),

View File

@@ -100,11 +100,16 @@ def test_login_enables_incomplete_file_suffix_by_default():
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):
with patch.object(
transmission_module.transmission_rpc,
"Client",
return_value=fake_client,
) as client_cls:
downloader = Transmission(host="127.0.0.1", port=9091)
assert downloader.trc is fake_client
fake_client.set_session.assert_called_once_with(rename_partial_files=True)
assert client_cls.call_args.kwargs["timeout"] == (3, 60)
def test_login_disables_incomplete_file_suffix_when_configured():

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v3.0.7'
FRONTEND_VERSION = 'v3.0.7'
APP_VERSION = 'v3.0.8'
FRONTEND_VERSION = 'v3.0.8'