Files
MoviePilot/app/workflow/actions/fetch_torrents.py
jxxghp 6d4ce83837 fix(types): clear host mypy debt and prevent typing regressions
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。
按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩,
CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。

修复空值、异步和供应方类型边界,保持插件与 API 兼容合同,
并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
2026-10-21 22:03:18 +08:00

136 lines
5.2 KiB
Python

"""
从站点搜索候选资源并写入工作流上下文。
"""
import random
import time
from typing import List, Optional
from pydantic import Field
from app.chain.media import MediaChain
from app.chain.search.facade import SearchChain
from app.domain.context import Context as SearchContext
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.context import Context as WorkflowContext
from app.schemas.types import MediaType
from app.schemas.workflow import ActionContext, ActionParams
from app.workflow.actions import BaseAction
class FetchTorrentsParams(ActionParams):
"""
获取站点资源参数
"""
search_type: Optional[str] = Field(default="keyword", description="搜索类型")
name: Optional[str] = Field(default=None, description="资源名称")
year: Optional[str] = Field(default=None, description="年份")
type: Optional[str] = Field(default=None, description="资源类型 (电影/电视剧)")
season: Optional[int] = Field(default=None, description="季度")
sites: Optional[List[int]] = Field(default=[], description="站点列表")
match_media: Optional[bool] = Field(default=False, description="匹配媒体信息")
class FetchTorrentsAction(BaseAction):
"""
搜索站点资源
"""
contract = {
"inputs": [{"name": "medias", "label": "媒体", "kind": "list"}],
"outputs": [{"name": "torrents", "label": "资源", "kind": "list"}],
}
def __init__(self, action_id: str) -> None:
"""
为当前动作建立独立的搜索结果集合。
"""
super().__init__(action_id)
self._torrents: list[WorkflowContext] = []
name = "搜索站点资源"
description = "搜索站点种子资源列表"
data = FetchTorrentsParams().model_dump()
@property
def success(self) -> bool:
"""
以动作完成状态报告执行结果。
"""
return self.done
def execute(self, workflow_id: int, params: dict[str, object], context: ActionContext) -> ActionContext:
"""
搜索站点,获取资源列表
"""
action_params = FetchTorrentsParams.model_validate(params)
searchchain = SearchChain()
if action_params.search_type == "keyword":
# 按关键字搜索
torrents = searchchain.search_by_title(
title=action_params.name or "",
sites=action_params.sites,
)
for torrent in torrents:
if runtime_stop_state.is_workflow_stopped(workflow_id):
break
meta_info = torrent.meta_info
if action_params.year and (not meta_info or meta_info.year != action_params.year):
continue
if action_params.type and torrent.media_info and torrent.media_info.type != MediaType(action_params.type):
continue
if action_params.season is not None and (
not meta_info or meta_info.begin_season != action_params.season
):
continue
# 识别媒体信息
if action_params.match_media:
if not meta_info:
continue
torrent.media_info = MediaChain().recognize_by_meta(
meta_info,
obtain_images=False,
)
if not torrent.media_info:
logger.warning(f"{torrent.torrent_info.title if torrent.torrent_info else ''} 未识别到媒体信息")
continue
self._torrents.append(self._to_workflow_context(torrent))
else:
# 搜索媒体列表
for media in context.medias or []:
if runtime_stop_state.is_workflow_stopped(workflow_id):
break
if not media.media_source or not media.media_id or not media.type:
logger.warning("媒体身份或类型不完整,跳过资源搜索")
continue
torrents = searchchain.search_by_id(
media_source=media.media_source,
media_id=media.media_id,
mtype=MediaType(media.type),
sites=action_params.sites,
)
for torrent in torrents:
self._torrents.append(self._to_workflow_context(torrent))
# 随机休眠 5-30秒
sleep_time = random.randint(5, 30)
logger.info(f"随机休眠 {sleep_time} 秒 ...")
time.sleep(sleep_time)
if self._torrents:
if context.torrents is None:
context.torrents = []
context.torrents.extend(self._torrents)
logger.info(f"共搜索到 {len(self._torrents)} 条资源")
self.job_done(f"搜索到 {len(self._torrents)} 个资源")
return context
@staticmethod
def _to_workflow_context(context: SearchContext) -> WorkflowContext:
"""
把领域搜索结果投影为工作流传输上下文。
"""
return WorkflowContext.model_validate(context.to_dict())