diff --git a/backend/src/module/manager/torrent.py b/backend/src/module/manager/torrent.py index 5fc8dc9f..db825ea5 100644 --- a/backend/src/module/manager/torrent.py +++ b/backend/src/module/manager/torrent.py @@ -221,13 +221,33 @@ class TorrentManager: # 没换:规则的种子可能经订阅进了别的实例,逐个实例按各自的 # 下载目录移动 if new_instance == old_instance: - for instance_id in await self._torrent_instances(old_data): - root = settings.downloader_instance(instance_id).path - moved_path = gen_save_path(data, root) - old_paths = _save_paths(old_data, root) - if old_paths == {normalize_save_path(moved_path)}: - continue - match_list = await self.__match_torrents_list(old_data, instance_id) + # 先在所有实例上匹配种子:任一实例不可用就什么都不改, + # 避免一部分实例已移动而规则仍记着旧目录 + moves = [] + try: + for instance_id in await self._torrent_instances(old_data): + root = settings.downloader_instance(instance_id).path + moved_path = gen_save_path(data, root) + old_paths = _save_paths(old_data, root) + if old_paths == {normalize_save_path(moved_path)}: + continue + match_list = await self.__match_torrents_list( + old_data, instance_id + ) + moves.append((instance_id, moved_path, old_paths, match_list)) + except ConnectionError as e: + logger.warning( + "[Manager] Can't update rule %s: %s", old_data.rule_name, e + ) + return ResponseModel( + status_code=500, + status=False, + msg_en=f"Updating {old_data.official_title} failed: a " + "downloader is unreachable; nothing was changed, retry later.", + msg_zh=f"更新 {old_data.official_title} 失败:下载器不可用," + "规则未修改,请稍后重试。", + ) + for instance_id, moved_path, old_paths, match_list in moves: async with DownloadClient(instance_id) as client: # Move existing torrents to new location if path changed if match_list: diff --git a/backend/src/test/test_multi_downloader.py b/backend/src/test/test_multi_downloader.py index 77a8ca50..7f26bebd 100644 --- a/backend/src/test/test_multi_downloader.py +++ b/backend/src/test/test_multi_downloader.py @@ -448,6 +448,44 @@ class TestRuleOnAnotherInstance: moved = _mock("b")._torrents["hb"]["save_path"] assert moved == "/b/Bangumi/Test Anime (2024)/Season 2" + async def test_update_rule_unavailable_instance_changes_nothing_for_retry( + self, two_instances + ): + """实例 b 不可用:a 上的种子不移动、规则不改,b 恢复后重试两边一起移动。""" + from module.manager import TorrentManager + from module.models import BangumiUpdate + + bangumi_id = await self._rule_with_torrent_on_a() + hb = _completed("B", "hb", "/b/Bangumi/Test Anime (2024)/Season 1") + _mock("b")._torrents["hb"] = hb + _mock("b").auth = AsyncMock(return_value=False) # type: ignore[method-assign] + async with Database() as db: + await db.torrent.add( + Torrent(name="B", url="u-b", bangumi_id=1, downloader_id="b") + ) + old = await db.bangumi.search_id(bangumi_id) + assert old is not None + update = BangumiUpdate(**old.model_dump(exclude={"id"})) + update.season = 2 + resp = await TorrentManager(db).update_rule(bangumi_id, update) + assert resp.status is False and resp.status_code == 500 + assert _mock("a")._torrents["old"]["save_path"] == old.save_path + kept = await db.bangumi.search_id(bangumi_id) + assert kept is not None and kept.save_path == old.save_path + + # 登录失败会丢弃缓存的客户端,恢复后是新的客户端实例 + _mock("b")._torrents["hb"] = hb + update = BangumiUpdate(**old.model_dump(exclude={"id"})) + update.season = 2 + retry = await TorrentManager(db).update_rule(bangumi_id, update) + assert retry.status is True + new = await db.bangumi.search_id(bangumi_id) + + season2 = "Bangumi/Test Anime (2024)/Season 2" + assert new is not None and new.save_path == f"/a/{season2}" + assert _mock("a")._torrents["old"]["save_path"] == f"/a/{season2}" + assert _mock("b")._torrents["hb"]["save_path"] == f"/b/{season2}" + async def test_delete_rule_unavailable_instance_keeps_rule_for_retry( self, two_instances ):