From 132f27c1c6db0fc1f1acc1f20ab269977a78dc16 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 5 Jul 2026 17:15:07 +0800 Subject: [PATCH] fix: retry qbittorrent files after add --- app/modules/qbittorrent/__init__.py | 22 ++++++++++- app/modules/qbittorrent/qbittorrent.py | 25 +++++++++--- tests/test_qbittorrent_compat.py | 55 ++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index 324e9558..6da0da0c 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -37,9 +37,14 @@ _QBITTORRENT_PAUSED_STATES = { "stoppeddl", "stoppedup", } +_TORRENT_FILES_RETRY_TIMES = 5 +_TORRENT_FILES_RETRY_INTERVAL = 1 class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): + """ + qBittorrent 下载器模块,负责下载任务添加、文件选择和任务管理。 + """ def init_module(self) -> None: """ @@ -50,6 +55,9 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): @staticmethod def get_name() -> str: + """ + 获取模块名称 + """ return "Qbittorrent" @staticmethod @@ -73,7 +81,10 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): """ return 1 - def stop(self): + def stop(self) -> None: + """ + 停止模块 + """ pass def test(self) -> Optional[Tuple[bool, str]]: @@ -90,6 +101,9 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): return True, "" def init_setting(self) -> Tuple[str, Union[str, bool]]: + """ + 返回控制模块启用状态的配置项 + """ pass def scheduler_job(self) -> None: @@ -221,7 +235,11 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): else: if is_paused: # 种子文件 - torrent_files = server.get_files(torrent_hash) + torrent_files = server.get_files( + torrent_hash, + retry=_TORRENT_FILES_RETRY_TIMES, + interval=_TORRENT_FILES_RETRY_INTERVAL, + ) if not torrent_files: return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, "获取种子文件失败,下载任务可能在暂停状态" diff --git a/app/modules/qbittorrent/qbittorrent.py b/app/modules/qbittorrent/qbittorrent.py index 19766c16..cbe55c50 100644 --- a/app/modules/qbittorrent/qbittorrent.py +++ b/app/modules/qbittorrent/qbittorrent.py @@ -488,17 +488,30 @@ class Qbittorrent: logger.error(f"删除种子出错:{str(err)}") return False - def get_files(self, tid: str) -> Optional[TorrentFilesList]: + def get_files(self, tid: str, retry: int = 1, interval: float = 0) -> Optional[TorrentFilesList]: """ 获取种子文件清单 + :param tid: 种子Hash + :param retry: 最多尝试次数 + :param interval: 重试间隔,单位秒 + :return: 种子文件清单 """ if not self.qbc: return None - try: - return self.qbc.torrents_files(torrent_hash=tid) - except Exception as err: - logger.error(f"获取种子文件列表出错:{str(err)}") - return None + last_error = None + retry_times = max(retry, 1) + for index in range(retry_times): + try: + torrent_files = self.qbc.torrents_files(torrent_hash=tid) + if torrent_files: + return torrent_files + except Exception as err: + last_error = err + if index < retry_times - 1 and interval: + time.sleep(interval) + if last_error: + logger.error(f"获取种子文件列表出错:{str(last_error)}") + return None def set_files(self, **kwargs) -> bool: """ diff --git a/tests/test_qbittorrent_compat.py b/tests/test_qbittorrent_compat.py index 8a7b490f..d04dc3f9 100644 --- a/tests/test_qbittorrent_compat.py +++ b/tests/test_qbittorrent_compat.py @@ -494,6 +494,61 @@ def test_download_falls_back_to_tag_lookup_when_added_ids_missing(): fake_server.get_torrent_id_by_tag.assert_called_once_with(tags="tmp-tag-01") +def test_get_files_retries_until_qbittorrent_files_available(): + """qBittorrent 添加任务后文件列表短暂未就绪时应重试。""" + torrent_files = [{"id": 12, "name": "Show.S01E12.mkv"}] + fake_client = MagicMock() + fake_client.torrents_files.side_effect = [ + Exception("Torrent hash(es): abc123"), + torrent_files, + ] + + with patch.object(Qbittorrent, "_Qbittorrent__login_qbittorrent", return_value=fake_client): + downloader = Qbittorrent(host="http://127.0.0.1", port=8080, username="admin", password="adminadmin") + + with patch.object(qbittorrent_module.time, "sleep") as sleep: + result = downloader.get_files("abc123", retry=2, interval=1) + + assert result == torrent_files + assert fake_client.torrents_files.call_count == 2 + fake_client.torrents_files.assert_called_with(torrent_hash="abc123") + sleep.assert_called_once_with(1) + + +def test_download_episode_selection_retries_file_list_after_add(): + """按集选择下载时应等待 qBittorrent 刚添加的任务文件列表。""" + + class _EpisodeMetaInfo: + """测试用集数识别对象。""" + + def __init__(self, name): + self.episode_list = [12] if "E12" in name else [1] + + fake_server = MagicMock() + fake_server.add_torrent.return_value = (True, ["abc123"]) + fake_server.get_content_layout.return_value = "Original" + fake_server.get_files.return_value = [ + {"id": 1, "name": "Show.S01E01.mkv"}, + {"id": 12, "name": "Show.S01E12.mkv"}, + ] + fake_server.is_force_resume.return_value = False + + module = _build_module(fake_server) + with patch.object(qbittorrent_package_module, "MetaInfo", _EpisodeMetaInfo): + result = module.download( + content=b"torrent-content", + download_dir=Path("/downloads"), + cookie="", + episodes={12}, + downloader="qb", + ) + + assert result == ("qb", "abc123", "Original", "添加下载成功,已选择集数:[12]") + fake_server.get_files.assert_called_once_with("abc123", retry=5, interval=1) + fake_server.set_files.assert_called_once_with(torrent_hash="abc123", file_ids=[1], priority=0) + fake_server.start_torrents.assert_called_once_with("abc123") + + def test_set_speed_limit_allows_single_direction_limit(): """ 设置全局限速时允许只传一个方向,未传方向按不限速处理。