From 0115c414597ebd6dfc8c5cd28c0031698b483591 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 9 Oct 2026 21:33:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(workflow):=20=E4=BF=AE=E5=A4=8D=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=B8=8B=E8=BD=BD=E7=9A=84=E5=AA=92=E4=BD=93=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E7=B1=BB=E5=9E=8B=20#6931?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/workflow/actions/add_download.py | 83 ++++++++++++++++++++++++++-- tests/test_workflow_actions.py | 40 +++++++++++++- 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/app/workflow/actions/add_download.py b/app/workflow/actions/add_download.py index 18e0df94f..09a827800 100644 --- a/app/workflow/actions/add_download.py +++ b/app/workflow/actions/add_download.py @@ -1,12 +1,20 @@ +from dataclasses import fields from typing import Optional from pydantic import Field from app.chain.download import DownloadChain from app.chain.media import MediaChain +from app.domain.context import Context as DownloadContext +from app.domain.context import MediaInfo as DownloadMediaInfo +from app.domain.context import MusicInfo as DownloadMusicInfo +from app.domain.context import TorrentInfo as DownloadTorrentInfo +from app.domain.meta.metabase import MetaBase +from app.domain.meta.metamusic import MetaMusic from app.domain.metainfo import MetaInfo from app.runtime.log import logger from app.runtime.stop import runtime_stop_state +from app.schemas.context import Context as WorkflowTorrentContext from app.schemas.types import MediaType from app.schemas.workflow import ActionContext, ActionParams, DownloadTask from app.workflow.actions import BaseAction @@ -34,6 +42,7 @@ class AddDownloadAction(BaseAction): } def __init__(self, action_id: str): + """初始化下载结果集合和动作错误状态。""" super().__init__(action_id) self._added_downloads = [] self._has_error = False @@ -44,11 +53,12 @@ class AddDownloadAction(BaseAction): @property def success(self) -> bool: + """返回本次动作是否未遇到下载失败。""" return not self._has_error def execute(self, workflow_id: int, params: dict, context: ActionContext) -> ActionContext: """ - 将上下文中的torrents添加到下载任务中 + 将工作流资源还原为下载链所需的领域上下文并添加下载任务。 """ params = AddDownloadParams(**params) _started = False @@ -71,10 +81,11 @@ class AddDownloadAction(BaseAction): self._has_error = True logger.warning(f"{t.torrent_info.title} 未识别到媒体信息,无法下载") continue + download_context = self._to_domain_context(t) if params.only_lack: - exists_info = DownloadChain().media_exists(t.media_info) + exists_info = DownloadChain().media_exists(download_context.media_info) if exists_info: - if t.media_info.type == MediaType.MOVIE: + if download_context.media_info.type == MediaType.MOVIE: # 电影 logger.warning(f"{t.torrent_info.title} 媒体库中已存在,跳过") continue @@ -99,7 +110,7 @@ class AddDownloadAction(BaseAction): continue _started = True - did = DownloadChain().download_single(context=t, + did = DownloadChain().download_single(context=download_context, downloader=params.downloader, save_path=params.save_path, label=params.labels) @@ -118,3 +129,67 @@ class AddDownloadAction(BaseAction): self.job_done(f"已添加 {len(self._added_downloads)} 个下载任务") return context + + @staticmethod + def _to_domain_context(context: WorkflowTorrentContext) -> DownloadContext: + """把工作流传输模型转换为下载链使用的领域上下文和媒体类型。""" + if context.media_info is None or context.torrent_info is None: + raise ValueError("工作流下载上下文缺少媒体或种子信息") + + media_info = context.media_info + if isinstance(media_info, (DownloadMediaInfo, DownloadMusicInfo)): + domain_media_info = media_info + else: + media_data = media_info.model_dump() + if media_data.get("type") == MediaType.MUSIC.value: + domain_media_info = DownloadMusicInfo.from_dict(media_data) + else: + domain_media_info = DownloadMediaInfo() + domain_media_info.from_dict(media_data) + + torrent_info = context.torrent_info + if isinstance(torrent_info, DownloadTorrentInfo): + domain_torrent_info = torrent_info + else: + domain_torrent_info = DownloadTorrentInfo() + domain_torrent_info.from_dict(torrent_info.model_dump()) + + meta_info = context.meta_info + if isinstance(meta_info, MetaBase): + domain_meta_info = meta_info + elif meta_info is None: + domain_meta_info = None + else: + meta_data = meta_info.model_dump() + meta_type = meta_data.get("type") + if meta_type == MediaType.MUSIC.value: + domain_meta_info = MetaMusic.from_dict(meta_data) + else: + domain_meta_info = MetaInfo( + title=meta_data.get("org_string") or meta_data.get("title") or torrent_info.title or "", + subtitle=meta_data.get("subtitle"), + mtype=MediaType(meta_type) if meta_type else None, + ) + for item in fields(domain_meta_info): + if item.name not in meta_data: + continue + value = meta_data[item.name] + if item.name == "type" and value: + value = MediaType(value) + setattr(domain_meta_info, item.name, value) + if meta_data.get("name"): + domain_meta_info.name = meta_data["name"] + + return DownloadContext( + meta_info=domain_meta_info, + media_info=domain_media_info, + torrent_info=domain_torrent_info, + resource_source=context.resource_source or "unknown", + match_source=context.match_source or "unknown", + candidate_recognized=bool(context.candidate_recognized), + media_info_is_target=bool(context.media_info_is_target), + match_status=context.match_status, + match_reason=context.match_reason, + confirmed_full_coverage=bool(context.confirmed_full_coverage), + music_track_keys=list(context.music_track_keys) if context.music_track_keys else None, + ) diff --git a/tests/test_workflow_actions.py b/tests/test_workflow_actions.py index 41afd38c1..13c9b0807 100644 --- a/tests/test_workflow_actions.py +++ b/tests/test_workflow_actions.py @@ -252,8 +252,10 @@ def test_workflow_manager_list_actions_exposes_contract(): def test_add_download_only_lack_handles_schema_metainfo(monkeypatch): - """添加下载动作开启仅下载缺失资源时应支持 Schema MetaInfo 的 season_list 属性并正确过滤已存在剧集。""" + """仅下载缺失资源时应兼容工作流 MetaInfo 并向下载链传递领域上下文。""" import app.workflow.actions.add_download as add_download_module + from app.domain.context import MediaInfo as DomainMediaInfo + from app.domain.meta.metabase import MetaBase from app.schemas.context import ( Context as SchemaContext, ) @@ -272,11 +274,21 @@ def test_add_download_only_lack_handles_schema_metainfo(monkeypatch): downloaded = [] class FakeDownloadChain: + """模拟媒体库检查和下载提交,并验证工作流对象已转换为领域类型。""" + def media_exists(self, mediainfo): + """返回已存在剧集,并检查传入媒体使用领域类型。""" + assert isinstance(mediainfo, DomainMediaInfo) + assert mediainfo.type is MediaType.TV # 模拟媒体库已存在第 1 季的第 1、2 集 return SimpleNamespace(seasons={1: [1, 2]}) def download_single(self, context=None, **kwargs): + """记录提交的种子名称,并验证下载链拿到领域上下文。""" + assert isinstance(context, Context) + assert isinstance(context.media_info, DomainMediaInfo) + assert isinstance(context.meta_info, MetaBase) + assert isinstance(context.torrent_info, TorrentInfo) downloaded.append(context.torrent_info.title) return f"hash-{context.torrent_info.title}" @@ -325,3 +337,29 @@ def test_add_download_only_lack_handles_schema_metainfo(monkeypatch): assert downloaded == ["Show S01E03", "Show Ep04"] assert len(result.downloads) == 2 assert [d.download_id for d in result.downloads] == ["hash-Show S01E03", "hash-Show Ep04"] + + +def test_add_download_converts_workflow_music_context_to_domain_types(): + """添加下载动作应将音乐工作流模型恢复为下载链使用的领域对象。""" + from app.domain.context import MusicInfo as DomainMusicInfo + from app.domain.meta.metamusic import MetaMusic + from app.schemas.context import Context as SchemaContext + from app.schemas.context import TorrentInfo as SchemaTorrentInfo + from app.schemas.music import MusicInfo as SchemaMusicInfo + from app.schemas.music import MusicMeta + from app.schemas.types import MediaType + from app.workflow.actions.add_download import AddDownloadAction + + context = SchemaContext( + meta_info=MusicMeta(title="Track", artists=["Artist"]), + media_info=SchemaMusicInfo(title="Track", artists=["Artist"]), + torrent_info=SchemaTorrentInfo(title="Track", site=1), + ) + + result = AddDownloadAction._to_domain_context(context) + + assert isinstance(result, Context) + assert isinstance(result.meta_info, MetaMusic) + assert isinstance(result.media_info, DomainMusicInfo) + assert result.media_info.type is MediaType.MUSIC + assert isinstance(result.torrent_info, TorrentInfo)