fix(transfer): 种子未下载完成时不回写已整理标签 (#6106)

This commit is contained in:
freeman
2026-07-13 12:05:01 +08:00
committed by GitHub
parent 2a49495e27
commit 4f2935c85e
3 changed files with 231 additions and 5 deletions

View File

@@ -1240,14 +1240,44 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
history_exists: bool = True,
):
"""
当同一种子的任务都已结束时,回写下载器已整理标签。
当同一种子的任务都已结束且种子已完成下载时,回写下载器已整理标签。
"""
if (
history_exists
and download_hash
and self.jobview.is_torrent_done(download_hash)
not history_exists
or not download_hash
or not self.jobview.is_torrent_done(download_hash)
):
self.transfer_completed(hashs=download_hash, downloader=downloader)
return
# 作业视图只包含已登记的整理任务;多集种子部分文件先下载完成时,
# 剩余文件尚未产生任务,此时打已整理标签会使下载器轮询永久跳过
# 剩余文件(#6009因此必须确认种子已整体下载完成。
if not self.__is_torrent_download_completed(download_hash, downloader):
logger.debug(
f"种子 {download_hash} 尚未下载完成或状态未知,暂不设置已整理标签"
)
return
if not self.jobview.is_torrent_done(download_hash):
logger.debug(
f"种子 {download_hash} 存在新登记的整理任务,暂不设置已整理标签"
)
return
self.transfer_completed(hashs=download_hash, downloader=downloader)
def __is_torrent_download_completed(
self, download_hash: str, downloader: Optional[str]
) -> bool:
"""
检查种子在下载器中是否已完成下载;查询不到或查询失败时视为未完成,
留待下载器定时轮询兜底,避免误打已整理标签。
"""
try:
torrents = self.list_torrents(hashs=download_hash, downloader=downloader)
if not torrents:
return False
return all((torrent.progress or 0) >= 100 for torrent in torrents)
except Exception as e:
logger.error(f"检查种子 {download_hash} 下载进度失败:{e}")
return False
def __send_metadata_scrape_event(
self, task: TransferTask, transferinfo: TransferInfo

View File

@@ -579,6 +579,7 @@ class TransferJobManagerTest(unittest.TestCase):
completed.append((hashs, downloader))
chain.transfer_completed = fake_transfer_completed
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
(fileitem, False)
]
@@ -621,6 +622,7 @@ class TransferJobManagerTest(unittest.TestCase):
completed.append((hashs, downloader))
chain.transfer_completed = fake_transfer_completed
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
(fileitem, False)
]
@@ -664,6 +666,7 @@ class TransferJobManagerTest(unittest.TestCase):
completed.append((hashs, downloader))
chain.transfer_completed = fake_transfer_completed
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
task = make_task(1)
task.downloader = "qbittorrent"
task.download_hash = "abc123"

View File

@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
from types import SimpleNamespace
from app.chain.transfer import JobManager, TransferChain
from app.core.config import settings
from app.schemas import FileItem, TransferTask
from app.schemas.types import MediaType
class _FakeMeta:
"""构造最小可用的剧集元数据。"""
def __init__(self, episode: int, season: int = 1):
"""初始化剧集编号相关字段。"""
self.name = "Test Show"
self.title = f"Test Show S{season:02d}E{episode:02d}"
self.year = "2026"
self.type = MediaType.TV
self.begin_season = season
self.end_season = None
self.total_season = 1
self.begin_episode = episode
self.end_episode = None
self.total_episode = 1
self.episode_list = [episode]
self.season_episode = f"S{season:02d}E{episode:02d}"
self.part = None
@property
def season(self):
"""返回季字符串。"""
return f"S{self.begin_season:02d}"
@property
def episode(self):
"""返回集字符串。"""
return f"E{self.begin_episode:02d}"
def to_dict(self):
"""返回元数据字典。"""
return {
"title": self.title,
"name": self.name,
"year": self.year,
"type": self.type.value,
"begin_season": self.begin_season,
"end_season": self.end_season,
"total_season": self.total_season,
"begin_episode": self.begin_episode,
"end_episode": self.end_episode,
"total_episode": self.total_episode,
"season_episode": self.season_episode,
"episode_list": self.episode_list,
"part": self.part,
}
def _make_chain() -> TransferChain:
"""构造跳过初始化的 TransferChain仅带作业视图。"""
chain = object.__new__(TransferChain)
chain.jobview = JobManager()
chain._media_exts = settings.RMT_MEDIAEXT
chain._subtitle_exts = settings.RMT_SUBEXT
chain._audio_exts = settings.RMT_AUDIOEXT
chain._allowed_exts = (
chain._media_exts + chain._audio_exts + chain._subtitle_exts
)
chain._success_target_files = {}
chain._scrape_batches = {}
return chain
def _make_task(episode: int, download_hash: str, downloader: str) -> TransferTask:
"""构造带下载器信息的整理任务。"""
name = f"Test.Show.S01E{episode:02d}.mkv"
task = TransferTask(
fileitem=FileItem(
storage="local",
path=f"/downloads/Test Show/{name}",
type="file",
name=name,
basename=name.removesuffix(".mkv"),
extension="mkv",
size=1024,
),
meta=_FakeMeta(episode),
)
task.download_hash = download_hash
task.downloader = downloader
return task
def _mark(chain: TransferChain, download_hash: str, downloader: str):
"""调用私有的打标签方法。"""
chain._TransferChain__mark_torrent_completed_if_done(download_hash, downloader)
def _finish_task(chain: TransferChain, task: TransferTask):
"""将任务登记并流转到完成状态。"""
assert chain.jobview.add_task(task)
chain.jobview.running_task(task)
chain.jobview.finish_task(task)
def test_mark_skips_tag_when_torrent_still_downloading():
"""种子未下载完成时(多集种子先完成单集),不得设置已整理标签(#6009"""
chain = _make_chain()
completed = []
chain.transfer_completed = lambda **kwargs: completed.append(kwargs)
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=52.3)]
_finish_task(chain, _make_task(1, "hash1", "qbittorrent"))
_mark(chain, "hash1", "qbittorrent")
assert completed == []
def test_mark_tags_when_torrent_completed():
"""种子已下载完成且任务全部结束时,正常设置已整理标签。"""
chain = _make_chain()
completed = []
chain.transfer_completed = lambda **kwargs: completed.append(kwargs)
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
_finish_task(chain, _make_task(1, "hash1", "qbittorrent"))
_mark(chain, "hash1", "qbittorrent")
assert completed == [{"hashs": "hash1", "downloader": "qbittorrent"}]
def test_mark_short_circuits_downloader_query_when_jobview_not_done():
"""作业视图还有未结束任务时,不应产生任何下载器查询。"""
chain = _make_chain()
queries = []
chain.transfer_completed = lambda **kwargs: None
chain.list_torrents = lambda **kwargs: queries.append(kwargs) or []
task = _make_task(1, "hash1", "qbittorrent")
assert chain.jobview.add_task(task)
chain.jobview.running_task(task)
_mark(chain, "hash1", "qbittorrent")
assert queries == []
def test_mark_skips_tag_when_torrent_not_found():
"""下载器中查不到种子时不打标签,留待定时轮询兜底。"""
chain = _make_chain()
completed = []
chain.transfer_completed = lambda **kwargs: completed.append(kwargs)
chain.list_torrents = lambda **kwargs: []
_finish_task(chain, _make_task(1, "hash1", "qbittorrent"))
_mark(chain, "hash1", "qbittorrent")
assert completed == []
def test_mark_skips_tag_when_list_torrents_raises():
"""查询下载器异常时不打标签且不向上抛出。"""
chain = _make_chain()
completed = []
chain.transfer_completed = lambda **kwargs: completed.append(kwargs)
def _raise(**_kwargs):
raise RuntimeError("downloader unreachable")
chain.list_torrents = _raise
_finish_task(chain, _make_task(1, "hash1", "qbittorrent"))
_mark(chain, "hash1", "qbittorrent")
assert completed == []
def test_multi_episode_torrent_tags_only_after_last_episode():
"""#6009 回归E01 先整理完不打标签,种子整体下载完成后才打标签。"""
chain = _make_chain()
completed = []
chain.transfer_completed = lambda **kwargs: completed.append(kwargs)
progress = {"value": 60}
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=progress["value"])]
# E01 下载完成并整理,此时种子整体仍在下载
_finish_task(chain, _make_task(1, "hash1", "qbittorrent"))
_mark(chain, "hash1", "qbittorrent")
assert completed == []
# E02 下载完成并整理,种子整体到 100%
progress["value"] = 100
_finish_task(chain, _make_task(2, "hash1", "qbittorrent"))
_mark(chain, "hash1", "qbittorrent")
assert completed == [{"hashs": "hash1", "downloader": "qbittorrent"}]