fix(qbittorrent): clean temporary tags on all download paths

This commit is contained in:
jxxghp
2026-08-30 16:56:35 +08:00
parent 26366c65a6
commit 3e9c2dcf42
2 changed files with 117 additions and 89 deletions

View File

@@ -105,6 +105,8 @@ class QbittorrentModule(_DownloaderModuleBase[Qbittorrent]):
:param label: 标签
:param downloader: 下载器
:return: 下载器名称、种子Hash、种子文件布局、错误原因
添加流程结束时统一清理随机临时标签,覆盖添加失败、任务查询失败和后续处理异常等路径。
"""
if not content:
return None, None, None, "下载内容为空"
@@ -133,98 +135,106 @@ class QbittorrentModule(_DownloaderModuleBase[Qbittorrent]):
tags = [tag]
# 如果要选择文件则先暂停
is_paused = True if episodes else False
# 添加任务
state, added_torrent_ids = server.add_torrent(
content=content,
download_dir=self.normalize_path(download_dir, downloader),
is_paused=is_paused,
tag=tags,
cookie=cookie,
category=category,
ignore_category_check=False
)
torrent_hash = None
temporary_tag_cleaned = False
try:
# 添加任务
state, added_torrent_ids = server.add_torrent(
content=content,
download_dir=self.normalize_path(download_dir, downloader),
is_paused=is_paused,
tag=tags,
cookie=cookie,
category=category,
ignore_category_check=False
)
if not state:
# 查询所有下载器的种子
torrents, error = server.get_torrents()
if error:
return None, None, None, "无法连接qbittorrent下载器"
if torrents:
try:
for torrent in torrents:
# 名称与大小相等则认为是同一个种子
if torrent.get("name") == getattr(torrent_from_file, 'name', '') \
and torrent.get("total_size") == getattr(torrent_from_file, 'total_size', 0):
torrent_hash = torrent.get("hash")
server.delete_torrents_tag(torrent_hash, tag)
torrent_tags = [str(tag).strip() for tag in (torrent.get("tags") or "").split(',')]
logger.warn(f"下载器中已存在该种子任务:{torrent_hash} - {torrent.get('name')}")
# 给种子打上标签
if "已整理" in torrent_tags:
server.remove_torrents_tag(ids=torrent_hash, tag=['已整理'])
if get_runtime_setting('TORRENT_TAG') and get_runtime_setting('TORRENT_TAG') not in torrent_tags:
logger.info(f"给种子 {torrent_hash} 打上标签:{get_runtime_setting('TORRENT_TAG')}")
server.set_torrents_tag(ids=torrent_hash, tags=[get_runtime_setting('TORRENT_TAG')])
# 获取种子内容布局: `Original: 原始, Subfolder: 创建子文件夹, NoSubfolder: 不创建子文件夹`
torrent_layout = server.get_content_layout()
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"下载任务已存在"
finally:
torrents.clear()
del torrents
return None, None, None, f"添加种子任务失败:{content}"
else:
# 获取种子Hash
torrent_hash = next(iter(added_torrent_ids), None)
if torrent_hash:
server.delete_torrents_tag(torrent_hash, tag)
else:
torrent_hash = server.get_torrent_id_by_tag(tags=tag)
if not torrent_hash:
return None, None, None, f"下载任务添加成功,但获取Qbittorrent任务信息失败:{content}"
else:
# 获取种子内容布局: `Original: 原始, Subfolder: 创建子文件夹, NoSubfolder: 不创建子文件夹`
torrent_layout = server.get_content_layout()
if is_paused:
# 种子文件
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, "获取种子文件失败,下载任务可能在暂停状态"
# 不需要的文件ID
file_ids = []
# 需要的集清单
sucess_epidised = []
if not state:
# 查询所有下载器的种子
torrents, error = server.get_torrents()
if error:
return None, None, None, "无法连接qbittorrent下载器"
if torrents:
try:
for torrent_file in torrent_files:
file_id = torrent_file.get("id")
file_name = torrent_file.get("name")
meta_info = MetaInfo(file_name)
if not meta_info.episode_list \
or not set(meta_info.episode_list).issubset(episodes):
file_ids.append(file_id)
else:
sucess_epidised = list(set(sucess_epidised).union(set(meta_info.episode_list)))
for torrent in torrents:
# 名称与大小相等则认为是同一个种子
if torrent.get("name") == getattr(torrent_from_file, 'name', '') \
and torrent.get("total_size") == getattr(torrent_from_file, 'total_size', 0):
torrent_hash = torrent.get("hash")
temporary_tag_cleaned = server.delete_torrents_tag(torrent_hash, tag)
torrent_tags = [str(tag).strip() for tag in (torrent.get("tags") or "").split(',')]
logger.warn(f"下载器中已存在该种子任务:{torrent_hash} - {torrent.get('name')}")
# 给种子打上标签
if "已整理" in torrent_tags:
server.remove_torrents_tag(ids=torrent_hash, tag=['已整理'])
if get_runtime_setting('TORRENT_TAG') and get_runtime_setting('TORRENT_TAG') not in torrent_tags:
logger.info(f"给种子 {torrent_hash} 打上标签:{get_runtime_setting('TORRENT_TAG')}")
server.set_torrents_tag(ids=torrent_hash, tags=[get_runtime_setting('TORRENT_TAG')])
# 获取种子内容布局: `Original: 原始, Subfolder: 创建子文件夹, NoSubfolder: 不创建子文件夹`
torrent_layout = server.get_content_layout()
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"下载任务已存在"
finally:
torrent_files.clear()
del torrent_files
if sucess_epidised and file_ids:
# 选择文件
server.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0)
# 开始任务
if server.is_force_resume():
# 强制继续
server.torrents_set_force_start(torrent_hash)
else:
server.start_torrents(torrent_hash)
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"添加下载成功,已选择集数:{sucess_epidised}"
torrents.clear()
del torrents
return None, None, None, f"添加种子任务失败:{content}"
else:
# 获取种子Hash
torrent_hash = next(iter(added_torrent_ids), None)
if not torrent_hash:
torrent_hash = server.get_torrent_id_by_tag(tags=tag)
if torrent_hash:
temporary_tag_cleaned = server.delete_torrents_tag(torrent_hash, tag)
if not torrent_hash:
return None, None, None, f"下载任务添加成功,但获取Qbittorrent任务信息失败:{content}"
else:
if server.is_force_resume():
server.torrents_set_force_start(torrent_hash)
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, "添加下载成功"
# 获取种子内容布局: `Original: 原始, Subfolder: 创建子文件夹, NoSubfolder: 不创建子文件夹`
torrent_layout = server.get_content_layout()
if is_paused:
# 种子文件
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, "获取种子文件失败,下载任务可能在暂停状态"
# 不需要的文件ID
file_ids = []
# 需要的集清单
sucess_epidised = []
try:
for torrent_file in torrent_files:
file_id = torrent_file.get("id")
file_name = torrent_file.get("name")
meta_info = MetaInfo(file_name)
if not meta_info.episode_list \
or not set(meta_info.episode_list).issubset(episodes):
file_ids.append(file_id)
else:
sucess_epidised = list(set(sucess_epidised).union(set(meta_info.episode_list)))
finally:
torrent_files.clear()
del torrent_files
if sucess_epidised and file_ids:
# 选择文件
server.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0)
# 开始任务
if server.is_force_resume():
# 强制继续
server.torrents_set_force_start(torrent_hash)
else:
server.start_torrents(torrent_hash)
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"添加下载成功,已选择集数:{sucess_epidised}"
else:
if server.is_force_resume():
server.torrents_set_force_start(torrent_hash)
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, "添加下载成功"
finally:
# qBittorrent 可能在添加或查询链路中途失败,仍需清理已注册的临时标签。
if not temporary_tag_cleaned:
server.delete_torrents_tag(torrent_hash, tag)
def list_torrents(self, status: TorrentStatus = None,
hashs: Union[list, str] = None,

View File

@@ -681,10 +681,28 @@ def test_download_falls_back_to_tag_lookup_when_added_ids_missing():
)
assert result == ("qb", "def456", "Original", "添加下载成功")
fake_server.delete_torrents_tag.assert_not_called()
fake_server.delete_torrents_tag.assert_called_once_with("def456", "tmp-tag-01")
fake_server.get_torrent_id_by_tag.assert_called_once_with(tags="tmp-tag-01")
def test_download_cleans_temporary_tag_when_addition_lookup_fails():
"""添加失败且无法查询下载器时仍应清理临时标签。"""
fake_server = MagicMock()
fake_server.add_torrent.return_value = (False, [])
fake_server.get_torrents.return_value = ([], True)
module = _build_module(fake_server)
result = module.download(
content="magnet:?xt=urn:btih:789",
download_dir=Path("/downloads"),
cookie="",
downloader="qb",
)
assert result == (None, None, None, "无法连接qbittorrent下载器")
fake_server.delete_torrents_tag.assert_called_once_with(None, "tmp-tag-01")
def test_download_removes_temporary_tag_from_existing_torrent():
"""重复添加任务时应从已存在的种子中删除本次临时标签。"""
fake_server = MagicMock()