mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-11 00:25:36 +08:00
fix(transfer): 种子未下载完成时不回写已整理标签 (#6106)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
193
tests/test_transfer_mark_torrent_completed.py
Normal file
193
tests/test_transfer_mark_torrent_completed.py
Normal 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"}]
|
||||
Reference in New Issue
Block a user