mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
fix(transfer): 种子未下载完成时不回写已整理标签 (#6106)
This commit is contained in:
+35
-5
@@ -1240,14 +1240,44 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
history_exists: bool = True,
|
history_exists: bool = True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
当同一种子的任务都已结束时,回写下载器已整理标签。
|
当同一种子的任务都已结束且种子已完成下载时,回写下载器已整理标签。
|
||||||
"""
|
"""
|
||||||
if (
|
if (
|
||||||
history_exists
|
not history_exists
|
||||||
and download_hash
|
or not download_hash
|
||||||
and self.jobview.is_torrent_done(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(
|
def __send_metadata_scrape_event(
|
||||||
self, task: TransferTask, transferinfo: TransferInfo
|
self, task: TransferTask, transferinfo: TransferInfo
|
||||||
|
|||||||
@@ -579,6 +579,7 @@ class TransferJobManagerTest(unittest.TestCase):
|
|||||||
completed.append((hashs, downloader))
|
completed.append((hashs, downloader))
|
||||||
|
|
||||||
chain.transfer_completed = fake_transfer_completed
|
chain.transfer_completed = fake_transfer_completed
|
||||||
|
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
|
||||||
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
|
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
|
||||||
(fileitem, False)
|
(fileitem, False)
|
||||||
]
|
]
|
||||||
@@ -621,6 +622,7 @@ class TransferJobManagerTest(unittest.TestCase):
|
|||||||
completed.append((hashs, downloader))
|
completed.append((hashs, downloader))
|
||||||
|
|
||||||
chain.transfer_completed = fake_transfer_completed
|
chain.transfer_completed = fake_transfer_completed
|
||||||
|
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
|
||||||
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
|
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
|
||||||
(fileitem, False)
|
(fileitem, False)
|
||||||
]
|
]
|
||||||
@@ -664,6 +666,7 @@ class TransferJobManagerTest(unittest.TestCase):
|
|||||||
completed.append((hashs, downloader))
|
completed.append((hashs, downloader))
|
||||||
|
|
||||||
chain.transfer_completed = fake_transfer_completed
|
chain.transfer_completed = fake_transfer_completed
|
||||||
|
chain.list_torrents = lambda **kwargs: [SimpleNamespace(progress=100)]
|
||||||
task = make_task(1)
|
task = make_task(1)
|
||||||
task.downloader = "qbittorrent"
|
task.downloader = "qbittorrent"
|
||||||
task.download_hash = "abc123"
|
task.download_hash = "abc123"
|
||||||
|
|||||||
@@ -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