From 58bb4961af8a8255325f68306f99c0d6ee3dc0f7 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 23 Sep 2026 18:13:39 +0800 Subject: [PATCH] fix: bound downloader connection retries for v3.0.8 --- .github/workflows/build-v3.yml | 7 +++++-- app/modules/qbittorrent/qbittorrent.py | 13 +++++++++++-- app/modules/transmission/transmission.py | 7 ++++--- docs/architecture-overview.md | 2 +- docs/refactor/optimization-checklist.md | 2 +- .../fixtures/architecture/dependency-baseline.json | 5 +++-- tests/test_qbittorrent_compat.py | 7 ++++++- tests/test_system_nettest.py | 3 ++- tests/test_transmission_compat.py | 7 ++++++- version.py | 4 ++-- 10 files changed, 41 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-v3.yml b/.github/workflows/build-v3.yml index 91775aab2..714e51b66 100644 --- a/.github/workflows/build-v3.yml +++ b/.github/workflows/build-v3.yml @@ -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}$ ]] diff --git a/app/modules/qbittorrent/qbittorrent.py b/app/modules/qbittorrent/qbittorrent.py index fc67a5c5c..97abf13a8 100644 --- a/app/modules/qbittorrent/qbittorrent.py +++ b/app/modules/qbittorrent/qbittorrent.py @@ -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() diff --git a/app/modules/transmission/transmission.py b/app/modules/transmission/transmission.py index cae4d065e..0404c3ca5 100755 --- a/app/modules/transmission/transmission.py +++ b/app/modules/transmission/transmission.py @@ -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: diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 07731b751..ab498f43c 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -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) | diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md index 2bd9153b0..a9d211c6d 100644 --- a/docs/refactor/optimization-checklist.md +++ b/docs/refactor/optimization-checklist.md @@ -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 等基线均为零 | diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index b2da95d2b..dc3df4267 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -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", diff --git a/tests/test_qbittorrent_compat.py b/tests/test_qbittorrent_compat.py index 17034d6c8..d8eea1bc0 100644 --- a/tests/test_qbittorrent_compat.py +++ b/tests/test_qbittorrent_compat.py @@ -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(): diff --git a/tests/test_system_nettest.py b/tests/test_system_nettest.py index 16d22441f..be2b9b809 100644 --- a/tests/test_system_nettest.py +++ b/tests/test_system_nettest.py @@ -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), diff --git a/tests/test_transmission_compat.py b/tests/test_transmission_compat.py index 4f6e9c3d8..d2282871f 100644 --- a/tests/test_transmission_compat.py +++ b/tests/test_transmission_compat.py @@ -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(): diff --git a/version.py b/version.py index 9d5a937a5..77f52f717 100644 --- a/version.py +++ b/version.py @@ -1,2 +1,2 @@ -APP_VERSION = 'v3.0.7' -FRONTEND_VERSION = 'v3.0.7' +APP_VERSION = 'v3.0.8' +FRONTEND_VERSION = 'v3.0.8'