fix(qbittorrent): verify temporary tag cleanup

This commit is contained in:
jxxghp
2026-09-02 12:46:30 +08:00
parent c700fb9987
commit a329507137
2 changed files with 83 additions and 1 deletions

View File

@@ -15,6 +15,10 @@ from app.domain import torrent as torrent_rules
from app.foundation import url as url_tools
_TORRENT_TAG_CLEANUP_RETRY_TIMES = 10
_TORRENT_TAG_CLEANUP_RETRY_INTERVAL = 1
class Qbittorrent:
"""
qbittorrent下载器
@@ -305,6 +309,10 @@ class Qbittorrent:
def delete_torrents_tag(self, ids: Optional[Union[str, list]], tag: Union[str, list]) -> bool:
"""
从指定种子移除标签,并删除全局标签定义
qBittorrent 5.x 添加任务后可能暂时无法按响应中的种子 ID 查询任务,
移除接口此时会静默成功,因此需读取任务标签确认清理结果。
:param ids: 种子Hash列表;未知种子Hash时传入None,仅删除全局标签定义
:param tag: 标签内容
:return: 是否删除成功
@@ -313,7 +321,41 @@ class Qbittorrent:
return False
try:
if ids:
self.qbc.torrents_remove_tags(torrent_hashes=ids, tags=tag)
expected_hashes = (
{torrent_hash.strip().lower() for torrent_hash in ids.split('|') if torrent_hash.strip()}
if isinstance(ids, str)
else {str(torrent_hash).strip().lower() for torrent_hash in ids if torrent_hash}
)
expected_tags = (
{tag_name.strip() for tag_name in tag.split(',') if tag_name.strip()}
if isinstance(tag, str)
else {str(tag_name).strip() for tag_name in tag if tag_name}
)
for attempt in range(_TORRENT_TAG_CLEANUP_RETRY_TIMES):
self.qbc.torrents_remove_tags(torrent_hashes=ids, tags=tag)
torrents = self.qbc.torrents_info(torrent_hashes=ids) or []
visible_hashes = {
str(torrent.get("hash") or "").strip().lower()
for torrent in torrents
}
tags_removed = all(
not expected_tags.intersection(
{
torrent_tag.strip()
for torrent_tag in str(torrent.get("tags") or "").split(',')
if torrent_tag.strip()
}
)
for torrent in torrents
)
if expected_hashes.issubset(visible_hashes) and tags_removed:
break
if attempt < _TORRENT_TAG_CLEANUP_RETRY_TIMES - 1:
time.sleep(_TORRENT_TAG_CLEANUP_RETRY_INTERVAL)
else:
self.qbc.torrents_delete_tags(tags=tag)
logger.warning(f"种子 {ids} 的临时Tag未能在重试期限内确认删除:{tag}")
return False
self.qbc.torrents_delete_tags(tags=tag)
return True
except Exception as err:

View File

@@ -733,6 +733,7 @@ def test_download_removes_temporary_tag_from_existing_torrent():
def test_delete_torrents_tag_uses_supported_qbittorrent_api_arguments():
"""删除标签时应分别调用任务移除接口和全局标签删除接口。"""
fake_client = MagicMock()
fake_client.torrents_info.return_value = [{"hash": "abc123", "tags": "moviepilot-tag"}]
downloader = Qbittorrent.__new__(Qbittorrent)
downloader.qbc = fake_client
@@ -741,6 +742,45 @@ def test_delete_torrents_tag_uses_supported_qbittorrent_api_arguments():
torrent_hashes="abc123",
tags="tmp-tag-01",
)
fake_client.torrents_info.assert_called_once_with(torrent_hashes="abc123")
fake_client.torrents_delete_tags.assert_called_once_with(tags="tmp-tag-01")
def test_delete_torrents_tag_retries_until_new_torrent_tag_is_removed():
"""新添加任务尚未可见或标签状态滞后时应重试并验证删除结果。"""
fake_client = MagicMock()
fake_client.torrents_info.side_effect = [
[],
[{"hash": "abc123", "tags": "moviepilot-tag,tmp-tag-01"}],
[{"hash": "abc123", "tags": "moviepilot-tag"}],
]
downloader = Qbittorrent.__new__(Qbittorrent)
downloader.qbc = fake_client
with patch.object(qbittorrent_module.time, "sleep") as sleep:
cleaned = downloader.delete_torrents_tag("abc123", "tmp-tag-01")
assert cleaned
assert fake_client.torrents_remove_tags.call_count == 3
assert fake_client.torrents_info.call_count == 3
assert sleep.call_args_list == [call(1), call(1)]
fake_client.torrents_delete_tags.assert_called_once_with(tags="tmp-tag-01")
def test_delete_torrents_tag_reports_unverified_cleanup():
"""重试后仍无法读取目标任务时不得把标签清理误报为成功。"""
fake_client = MagicMock()
fake_client.torrents_info.return_value = []
downloader = Qbittorrent.__new__(Qbittorrent)
downloader.qbc = fake_client
with patch.object(qbittorrent_module.time, "sleep") as sleep:
cleaned = downloader.delete_torrents_tag("abc123", "tmp-tag-01")
assert not cleaned
assert fake_client.torrents_remove_tags.call_count == 10
assert fake_client.torrents_info.call_count == 10
assert sleep.call_count == 9
fake_client.torrents_delete_tags.assert_called_once_with(tags="tmp-tag-01")