Files
MoviePilot/app/schemas/event.py
T

1168 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from collections.abc import Mapping as _Mapping
from enum import Enum as _Enum
from pathlib import Path
from typing import Annotated as _Annotated
from typing import Any, Callable, Dict, Iterable, List, Optional, Set
from typing import Union as _Union
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic import BeforeValidator as _BeforeValidator
from app.schemas.common import JsonData
from app.schemas.context import Context as _ContextSnapshotBase
from app.schemas.context import MediaInfo as _MediaInfoSnapshot
from app.schemas.context import MetaInfo as _MetaInfoSnapshot
from app.schemas.file import FileItem
from app.schemas.media import OptionalMediaIdentityMixin, RequiredMediaIdentityMixin
from app.schemas.music import MusicInfo as _MusicInfoSnapshot
from app.schemas.music import MusicMeta as _MusicMetaSnapshot
from app.schemas.subscribe import Subscribe as _SubscribeSnapshot
from app.schemas.transfer import TransferInfo
from app.schemas.types import MediaSource, MediaType, NotificationChannel
JsonValue = JsonData
"""事件扩展字段允许的 JSON 值类型。"""
def _coerce_event_snapshot(value: Any) -> Any:
"""把旧运行时对象转换为只用于契约校验的可读取快照。"""
if isinstance(value, BaseModel):
value = value.model_dump(mode="python")
elif hasattr(value, "to_dict") and callable(value.to_dict):
value = value.to_dict()
elif isinstance(value, _Enum):
return value.value
elif isinstance(value, Path):
return value.as_posix()
if isinstance(value, _Mapping):
return {str(key): _coerce_event_snapshot(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set, frozenset)):
return [_coerce_event_snapshot(item) for item in value]
return value
EventJsonValue = _Annotated[JsonValue, _BeforeValidator(_coerce_event_snapshot)]
MediaSnapshot = _Annotated[
_Union[_MusicInfoSnapshot, _MediaInfoSnapshot],
_BeforeValidator(_coerce_event_snapshot),
]
MetaSnapshot = _Annotated[
_Union[_MusicMetaSnapshot, _MetaInfoSnapshot],
_BeforeValidator(_coerce_event_snapshot),
]
class ContextSnapshot(_ContextSnapshotBase):
"""资源上下文的稳定事件快照,不替换链路中的运行时 Context。"""
allowed_episodes: Optional[Set[int]] = None
model_config = ConfigDict(
extra="allow",
from_attributes=True,
arbitrary_types_allowed=True,
)
@model_validator(mode="before") # type: ignore[misc]
@classmethod
def coerce_runtime_context(cls, value: Any) -> Any:
"""允许旧 Context/dataclass 进入校验,同时保持原对象继续投递。"""
return _coerce_event_snapshot(value)
class FileContextSnapshot(BaseModel): # type: ignore[misc]
"""音乐批次中单个文件的元数据上下文快照。"""
path: str
meta: Optional[MetaSnapshot] = None
mediainfo: Optional[MediaSnapshot] = None
model_config = ConfigDict(extra="allow", from_attributes=True)
@model_validator(mode="before") # type: ignore[misc]
@classmethod
def coerce_runtime_context(cls, value: Any) -> Any:
"""把旧文件上下文对象转换成可验证的字典。"""
return _coerce_event_snapshot(value)
class Event(BaseModel):
"""
事件模型
"""
event_type: str = Field(..., description="事件类型")
event_data: Optional[dict] = Field(default={}, description="事件数据")
priority: Optional[int] = Field(0, description="事件优先级")
class BaseEventData(BaseModel):
"""
事件数据的基类,所有具体事件数据类应继承自此类
"""
model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True)
class ExtensibleEventData(BaseEventData):
"""允许第三方插件附加字段的类型化事件载荷基类。"""
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class EmptyEventData(ExtensibleEventData):
"""当前没有固定字段、但仍需进入 typed registry 的事件载荷。"""
class PluginReloadEventData(ExtensibleEventData):
"""插件重载广播事件载荷。"""
plugin_id: str = Field(description="重载的插件 ID")
class PluginActionEventData(ExtensibleEventData):
"""插件命令动作载荷;具体动作参数由目标插件扩展。"""
plugin_id: Optional[str] = Field(default=None, description="目标插件 ID")
action: Optional[str] = Field(default=None, description="插件动作名称")
channel: Optional[str] = Field(default=None, description="消息渠道")
source: Optional[str] = Field(default=None, description="消息来源")
user: Optional[Any] = Field(default=None, description="发起用户")
class PluginTriggeredEventData(ExtensibleEventData):
"""插件主动发布的跨插件事件载荷。"""
plugin_id: str = Field(description="发布事件的插件 ID")
event_name: str = Field(description="插件定义的稳定事件名")
data: Any = Field(default=None, description="插件定义的事件数据")
class CommandExecuteEventData(ExtensibleEventData):
"""斜杠命令执行事件载荷。"""
cmd: str = Field(description="包含参数的完整命令文本")
user: Optional[Any] = Field(default=None, description="发起用户")
channel: Optional[str] = Field(default=None, description="消息渠道")
source: Optional[str] = Field(default=None, description="消息来源")
processing_status: Optional[Any] = Field(default=None, description="交互处理状态")
class SiteEventData(ExtensibleEventData):
"""站点新增、更新、删除或数据刷新事件载荷。"""
site_id: Optional[int | str] = Field(default=None, description="站点 ID 或通配符")
domain: Optional[str] = Field(default=None, description="站点域名")
name: Optional[str] = Field(default=None, description="站点名称")
site_url: Optional[str] = Field(default=None, description="站点地址")
class HistoryDeletedEventData(ExtensibleEventData):
"""历史记录删除事件的兼容载荷。"""
history_id: Optional[int] = Field(default=None, description="历史记录 ID")
src: Optional[str] = Field(default=None, description="关联源路径")
class DownloadFileDeletedEventData(ExtensibleEventData):
"""下载源文件删除事件载荷。"""
src: Optional[str] = Field(default=None, description="已删除的下载源路径")
hash: Optional[str] = Field(default=None, description="下载任务 hash")
class DownloadDeletedEventData(ExtensibleEventData):
"""下载任务删除事件载荷。"""
hash: str = Field(description="下载任务 hash")
torrents: List[Dict[str, Any]] = Field(default_factory=list, description="删除前任务快照")
class UserMessageEventData(ExtensibleEventData):
"""未被宿主命令或交互消费的用户文本消息载荷。"""
text: str = Field(description="用户文本")
userid: Optional[Any] = Field(default=None, description="用户 ID")
channel: Optional[str] = Field(default=None, description="消息渠道")
source: Optional[str] = Field(default=None, description="消息来源")
chat_id: Optional[Any] = Field(default=None, description="会话 ID")
reply_to_message_id: Optional[Any] = Field(default=None, description="回复消息 ID")
class NoticeMessageEventData(ExtensibleEventData):
"""宿主向消息模块发送的通知事件载荷。"""
type: Optional[Any] = Field(default=None, description="兼容消息类型")
title: Optional[str] = Field(default=None, description="消息标题")
text: Optional[str] = Field(default=None, description="消息正文")
userid: Optional[Any] = Field(default=None, description="目标用户 ID")
channel: Optional[Any] = Field(default=None, description="目标消息渠道")
source: Optional[str] = Field(default=None, description="消息来源")
class SubscribeCompleteEventData(ExtensibleEventData):
"""订阅完成广播事件载荷。"""
subscribe_id: int = Field(description="已完成订阅 ID")
subscribe_info: Dict[str, Any] = Field(default_factory=dict, description="订阅快照")
mediainfo: Dict[str, Any] = Field(default_factory=dict, description="媒体信息快照")
class SystemErrorEventData(ExtensibleEventData):
"""事件、模块、插件或调度器错误的宿主诊断载荷。"""
type: str = Field(description="错误来源类别")
error: str = Field(description="错误摘要")
traceback: Optional[str] = Field(default=None, description="错误堆栈")
class MetadataScrapeEventData(ExtensibleEventData):
"""媒体文件元数据刮削事件载荷。"""
fileitem: FileItem = Field(description="待刮削目录或文件项")
file_list: List[str] = Field(default_factory=list, description="待刮削文件清单")
meta: Any = Field(default=None, description="文件名解析对象")
mediainfo: Any = Field(default=None, description="媒体信息对象")
overwrite: bool = Field(default=False, description="是否覆盖已有元数据")
file_contexts: List[Any] = Field(default_factory=list, description="逐文件上下文")
class MessageActionEventData(ExtensibleEventData):
"""定向插件消息交互动作载荷。"""
plugin_id: Optional[str] = Field(default=None, description="目标插件 ID")
text: Optional[str] = Field(default=None, description="兼容动作文本")
input_text: Optional[str] = Field(default=None, description="用户输入文本")
userid: Optional[Any] = Field(default=None, description="用户 ID")
channel: Optional[str] = Field(default=None, description="消息渠道")
source: Optional[str] = Field(default=None, description="消息来源")
input_session_id: Optional[str] = Field(default=None, description="输入会话 ID")
payload: Any = Field(default=None, description="插件自定义交互数据")
class WorkflowExecuteEventData(ExtensibleEventData):
"""请求执行指定工作流的事件载荷。"""
workflow_id: int = Field(description="工作流 ID")
class NameRecognizeEventData(ExtensibleEventData):
"""影视名称辅助识别的输入和插件回写字段。"""
title: str = Field(description="待识别标题")
name: Optional[str] = Field(default=None, description="插件识别后的名称")
year: Optional[Any] = Field(default=None, description="年份")
season: Optional[Any] = Field(default=None, description="季号")
episode: Optional[Any] = Field(default=None, description="集号")
class MusicNameRecognizeEventData(ExtensibleEventData):
"""音乐名称辅助识别的输入和插件回写字段。"""
title: str = Field(description="待识别曲名")
artist: Optional[str] = Field(default=None, description="艺术家")
album: Optional[str] = Field(default=None, description="专辑")
year: Optional[Any] = Field(default=None, description="年份")
duration: Optional[Any] = Field(default=None, description="时长")
name: Optional[str] = Field(default=None, description="插件识别后的曲名")
class MediaRecognizeEventData(ExtensibleEventData):
"""影视媒体身份补充识别的输入和插件回写字段。"""
title: Optional[str] = Field(default=None, description="待识别标题")
year: Optional[Any] = Field(default=None, description="年份")
season: Optional[Any] = Field(default=None, description="季号")
type: Optional[Any] = Field(default=None, description="媒体类型")
media_source: Optional[Any] = Field(default=None, description="媒体数据源")
media_id: Optional[Any] = Field(default=None, description="数据源原生 ID")
mediainfo: Optional[Dict[str, Any]] = Field(default=None, description="插件回写媒体信息")
class MusicMediaRecognizeEventData(MediaRecognizeEventData):
"""音乐媒体身份补充识别的输入和插件回写字段。"""
artists: List[str] = Field(default_factory=list, description="艺术家列表")
album: Optional[str] = Field(default=None, description="专辑")
isrc: Optional[str] = Field(default=None, description="ISRC")
music_type: Optional[str] = Field(default=None, description="音乐实体类型")
class ConfigChangeEventData(BaseEventData):
"""
ConfigChange 事件的数据模型
"""
key: set[str] = Field(..., description="配置项的键(集合类型)")
value: Optional[Any] = Field(default=None, description="配置项的新值")
change_type: str = Field(
default="update", description="配置项的变更类型,如 'add', 'update', 'delete'"
)
@field_validator("key", mode="before")
@classmethod
def convert_to_set(cls, v):
"""将输入的 str、list、dict.keys() 等转为 set"""
if v is None:
return set()
elif isinstance(v, str):
return {v}
elif isinstance(v, dict):
return set(str(k) for k in v.keys())
elif isinstance(v, (list, tuple)):
return set(str(item) for item in v)
elif isinstance(v, set):
return set(str(item) for item in v)
elif isinstance(v, Iterable):
return set(str(item) for item in v)
else:
return {str(v)}
class ChainEventData(BaseEventData):
"""
链式事件数据的基类,所有具体事件数据类应继承自此类
"""
model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True)
class ResourceSelectionContractData(ChainEventData):
"""ResourceSelection 的稳定输入/输出契约。"""
contexts: List[ContextSnapshot] = Field(default_factory=list)
downloader: Optional[str] = None
origin: Optional[str] = None
updated: bool = False
updated_contexts: Optional[List[ContextSnapshot]] = None
source: Optional[str] = "未知拦截源"
class ResourceSelectionInputContractData(ChainEventData):
"""ResourceSelection 事件输入字段。"""
contexts: List[ContextSnapshot] = Field(default_factory=list)
downloader: Optional[str] = None
origin: Optional[str] = None
class ResourceSelectionOutputContractData(ChainEventData):
"""ResourceSelection 事件插件回写字段。"""
updated: bool = False
updated_contexts: Optional[List[ContextSnapshot]] = None
source: Optional[str] = "未知拦截源"
class ResourceDownloadContractData(ChainEventData):
"""ResourceDownload 的稳定输入/输出契约。"""
context: Optional[ContextSnapshot] = None
episodes: Optional[Set[int]] = None
channel: Optional[NotificationChannel] = None
origin: Optional[str] = None
downloader: Optional[str] = None
options: Optional[Dict[str, EventJsonValue]] = None
cancel: bool = False
source: str = "未知拦截源"
reason: str = ""
class ResourceDownloadInputContractData(ChainEventData):
"""ResourceDownload 事件输入字段。"""
context: Optional[ContextSnapshot] = None
episodes: Optional[Set[int]] = None
channel: Optional[NotificationChannel] = None
origin: Optional[str] = None
downloader: Optional[str] = None
options: Optional[Dict[str, EventJsonValue]] = None
class ResourceDownloadOutputContractData(ChainEventData):
"""ResourceDownload 事件插件回写字段。"""
cancel: bool = False
source: str = "未知拦截源"
reason: str = ""
class DownloadAddedContractData(BaseEventData):
"""DownloadAdded 的稳定下载上下文契约。"""
hash: str
context: ContextSnapshot
username: Optional[str] = None
downloader: Optional[str] = None
episodes: List[int] = Field(default_factory=list)
source: Optional[str] = None
idempotency_key: Optional[str] = None
class TransferResultContractData(BaseEventData):
"""整理结果事件的稳定媒体与元数据契约。"""
fileitem: Optional[FileItem] = None
meta: Optional[MetaSnapshot] = None
mediainfo: Optional[MediaSnapshot] = None
transferinfo: Optional[TransferInfo] = None
downloader: Optional[str] = None
download_hash: Optional[str] = None
transfer_history_id: Optional[int] = None
idempotency_key: Optional[str] = None
class MetadataScrapeContractData(ExtensibleEventData):
"""MetadataScrape 的稳定媒体刮削契约。"""
fileitem: FileItem
file_list: List[str] = Field(default_factory=list)
meta: Optional[MetaSnapshot] = None
mediainfo: Optional[MediaSnapshot] = None
overwrite: bool = False
file_contexts: List[FileContextSnapshot] = Field(default_factory=list)
class SubscribeCompletionCheckContractData(ChainEventData):
"""订阅完成判定的稳定订阅、媒体和元数据契约。"""
subscribe: Optional[_SubscribeSnapshot] = None
mediainfo: Optional[MediaSnapshot] = None
meta: Optional[MetaSnapshot] = None
cancel: bool = False
source: str = "未知来源"
reason: str = ""
class SubscribeCompletionCheckInputContractData(ChainEventData):
"""SubscribeCompletionCheck 事件输入字段。"""
subscribe: Optional[_SubscribeSnapshot] = None
mediainfo: Optional[MediaSnapshot] = None
meta: Optional[MetaSnapshot] = None
class SubscribeCompletionCheckOutputContractData(ChainEventData):
"""SubscribeCompletionCheck 事件插件回写字段。"""
cancel: bool = False
source: str = "未知来源"
reason: str = ""
class PluginDataResetEventData(ChainEventData):
"""
PluginDataReset 事件的数据模型。
在主程序清空某个插件配置或插件数据前发出,插件可在数据被删除前完成
自有状态补偿。事件处理器只应处理 plugin_id 与自身匹配的事件。
"""
plugin_id: str = Field(..., description="即将被重置的插件 ID")
reset_config: bool = Field(default=False, description="是否即将重置插件配置")
reset_data: bool = Field(default=False, description="是否即将重置插件数据")
class AgentLLMProviderEventData(ChainEventData):
"""
Agent LLM 供应商选择事件数据。
事件发出方会带入当前系统配置作为默认值;插件可覆盖 provider、base_url、
api_key、model、user_agent、use_proxy 等字段,并通过 selected_provider_id 标记本次选择,方便
后续用量事件精确回写到同一个配额条目。
"""
provider: Optional[str] = Field(default=None, description="LLM provider ID")
base_url: Optional[str] = Field(default=None, description="API Base URL")
api_key: Optional[str] = Field(default=None, description="API Key")
model: Optional[str] = Field(default=None, description="模型名称")
base_url_preset: Optional[str] = Field(default=None, description="Base URL 预设ID")
user_agent: Optional[str] = Field(default=None, description="OpenAI兼容接口User-Agent")
use_proxy: Optional[bool] = Field(default=None, description="是否使用系统代理")
thinking_level: Optional[str] = Field(default=None, description="思考模式级别")
api_protocol: Optional[str] = Field(default=None, description="OpenAI兼容接口API协议:auto/chat_completions/responses")
web_search_mode: Optional[str] = Field(default=None, description="联网搜索模式:local/builtin/auto/disabled")
selected_provider_id: Optional[str] = Field(default=None, description="插件侧供应商ID")
selected_provider_name: Optional[str] = Field(default=None, description="插件侧供应商名称")
source: Optional[str] = Field(default=None, description="选择来源")
metadata: Dict[str, Any] = Field(default_factory=dict, description="扩展元数据")
class AgentTokensUsageEventData(BaseEventData):
"""
Agent Tokens 用量广播事件数据。
用量事件不携带 API Key,只携带选择事件返回的 selected_provider_id 以及
聚合后的 token 统计,避免把密钥扩散给广播订阅者。
"""
session_id: str = Field(..., description="Agent 会话ID")
selected_provider_id: Optional[str] = Field(default=None, description="插件侧供应商ID")
selected_provider_name: Optional[str] = Field(default=None, description="插件侧供应商名称")
provider: Optional[str] = Field(default=None, description="实际 LLM provider ID")
base_url: Optional[str] = Field(default=None, description="API Base URL")
model: Optional[str] = Field(default=None, description="模型名称")
input_tokens: int = Field(default=0, description="输入 tokens")
output_tokens: int = Field(default=0, description="输出 tokens")
total_tokens: int = Field(default=0, description="总 tokens")
cache_read_input_tokens: int = Field(default=0, description="从提示词缓存读取的输入 tokens")
cache_write_input_tokens: int = Field(default=0, description="写入提示词缓存的输入 tokens")
uncached_input_tokens: int = Field(default=0, description="未命中缓存的输入 tokens")
cache_hit_ratio: Optional[float] = Field(default=None, description="提示词缓存命中率")
cache_usage_available: bool = Field(default=False, description="供应商是否返回缓存用量明细")
model_call_count: int = Field(default=0, description="模型调用次数")
success: bool = Field(default=False, description="Agent 执行是否成功")
error: Optional[str] = Field(default=None, description="失败原因")
started_at: Optional[str] = Field(default=None, description="开始时间")
finished_at: Optional[str] = Field(default=None, description="结束时间")
source: str = Field(default="agent", description="事件来源")
metadata: Dict[str, Any] = Field(default_factory=dict, description="扩展元数据")
class AuthCredentials(ChainEventData):
"""
AuthVerification 事件的数据模型
Attributes:
username (Optional[str]): 用户名,适用于 "password" grant_type
password (Optional[str]): 用户密码,适用于 "password" grant_type
mfa_code (Optional[str]): 一次性密码,目前仅适用于 "password" 认证类型
code (Optional[str]): 授权码,适用于 "authorization_code" grant_type
grant_type (str): 认证类型,如 "password", "authorization_code", "client_credentials"
# scope (List[str]): 权限范围,如 ["read", "write"]
token (Optional[str]): 认证令牌
channel (Optional[str]): 认证渠道
service (Optional[str]): 服务名称
"""
# 输入参数
username: Optional[str] = Field(
None, description="用户名,适用于 'password' 认证类型"
)
password: Optional[str] = Field(
None, description="用户密码,适用于 'password' 认证类型"
)
mfa_code: Optional[str] = Field(
None, description="一次性密码,目前仅适用于 'password' 认证类型"
)
code: Optional[str] = Field(
None, description="授权码,适用于 'authorization_code' 认证类型"
)
grant_type: str = Field(
...,
description="认证类型,如 'password', 'authorization_code', 'client_credentials'",
)
# scope: List[str] = Field(default_factory=list, description="权限范围,如 ['read', 'write']")
# 输出参数
# grant_type 为 authorization_code 时,输出参数包括 username、token、channel、service
token: Optional[str] = Field(default=None, description="认证令牌")
channel: Optional[str] = Field(default=None, description="认证渠道")
service: Optional[str] = Field(default=None, description="服务名称")
@model_validator(mode="before")
@classmethod
def check_fields_based_on_grant_type(cls, values): # noqa
grant_type = values.get("grant_type")
if not grant_type:
values["grant_type"] = "password"
grant_type = "password"
if grant_type == "password":
if not values.get("username") or not values.get("password"):
raise ValueError(
"username and password are required for grant_type 'password'"
)
elif grant_type == "authorization_code":
if not values.get("code"):
raise ValueError("code is required for grant_type 'authorization_code'")
return values
class AuthInterceptCredentials(ChainEventData):
"""
AuthIntercept 事件的数据模型
Attributes:
# 输入参数
username (str): 用户名
channel (str): 认证渠道
service (str): 服务名称
token (str): 认证令牌
status (str): 认证状态,"triggered" 和 "completed" 两个状态
# 输出参数
source (str): 拦截源,默认值为 "未知拦截源"
cancel (bool): 是否取消认证,默认值为 False
"""
# 输入参数
username: Optional[str] = Field(..., description="用户名")
channel: str = Field(..., description="认证渠道")
service: str = Field(..., description="服务名称")
status: str = Field(
...,
description="认证状态, 包含 'triggered' 表示认证触发,'completed' 表示认证成功",
)
token: Optional[str] = Field(default=None, description="认证令牌")
# 输出参数
source: str = Field(default="未知拦截源", description="拦截源")
cancel: bool = Field(default=False, description="是否取消认证")
class CommandRegisterEventData(ChainEventData):
"""
CommandRegister 事件的数据模型
Attributes:
# 输入参数
commands (dict): 菜单命令
origin (str): 事件源,可以是 Chain 或具体的模块名称
service (str): 服务名称
# 输出参数
source (str): 拦截源,默认值为 "未知拦截源"
cancel (bool): 是否取消认证,默认值为 False
"""
# 输入参数
commands: Dict[str, dict] = Field(..., description="菜单命令")
origin: str = Field(..., description="事件源")
service: Optional[str] = Field(..., description="服务名称")
# 输出参数
cancel: bool = Field(default=False, description="是否取消注册")
source: str = Field(default="未知拦截源", description="拦截源")
class TransferRenameBuildEventData(ChainEventData):
"""
TransferRenameBuild 事件的数据模型
在 ``transhandler.get_rename_path`` 渲染文件名之前发出,给插件一次往
``rename_dict`` 写字段的机会。典型用法是通过 ffprobe 或外部接口探测源文件,
把分辨率、视频/音频编码、HDR 等字段写入 ``rename_dict``,主程序下一步渲染时
就能直接用到这些字段,不需要插件事后再渲染一次去覆盖结果。
与 ``TransferRenameEventData`` 的分工:
- 本事件负责"往 ``rename_dict`` 里写字段",没有输出参数;
- ``TransferRename`` 在渲染之后触发,负责对已渲染好的字符串再做改写(大小写、
词替换、模板覆盖等),由智能重命名一类插件使用。
使用约定:
- 只往 ``rename_dict`` 写字段,不要在这里改写已经渲染好的字符串;
- ``source_path`` / ``source_item`` 为空时(如重命名预览场景),需要源文件
才能工作的插件请直接 return;
- ``rename_dict`` 中以双下划线开头的键(``__meta__`` / ``__mediainfo__`` 等)
存放的是原始对象引用,只读使用,不要修改这些对象本身。
Attributes:
template_string (str): Jinja2 模板字符串
rename_dict (Dict[str, Any]): 渲染上下文,可直接修改
source_path (Optional[str]): 源文件路径,即待整理的文件路径
source_item (Optional[FileItem]): 源文件信息,即待整理的文件信息
"""
template_string: str = Field(..., description="模板字符串")
rename_dict: Dict[str, Any] = Field(..., description="渲染上下文")
source_path: Optional[str] = Field(
None, description="源文件路径,即待整理的文件路径"
)
source_item: Optional[FileItem] = Field(
None, description="源文件信息,即待整理的文件信息"
)
class TransferRenameEventData(ChainEventData):
"""
TransferRename 事件的数据模型
Attributes:
# 输入参数
template_string (str): Jinja2 模板字符串
rename_dict (dict): 渲染上下文
render_str (str): 渲染生成的字符串
path (Optional[Path]): 当前文件的目标路径
source_path (Optional[str]): 源文件路径,即待整理的文件路径
source_item (Optional[FileItem]): 源文件信息,即待整理的文件信息
# 输出参数
updated (bool): 是否已更新,默认值为 False
updated_str (str): 更新后的字符串
source (str): 拦截源,默认值为 "未知拦截源"
"""
# 输入参数
template_string: str = Field(..., description="模板字符串")
rename_dict: Dict[str, Any] = Field(..., description="渲染上下文")
path: Optional[Path] = Field(None, description="文件的目标路径")
render_str: str = Field(..., description="渲染生成的字符串")
source_path: Optional[str] = Field(
None, description="源文件路径,即待整理的文件路径"
)
source_item: Optional[FileItem] = Field(
None, description="源文件信息,即待整理的文件信息"
)
# 输出参数
updated: bool = Field(default=False, description="是否已更新")
updated_str: Optional[str] = Field(default=None, description="更新后的字符串")
source: Optional[str] = Field(default="未知拦截源", description="拦截源")
class ResourceSelectionEventData(BaseModel):
"""
ResourceSelection 事件的数据模型
Attributes:
# 输入参数
contexts (List[Context]): 当前待选择的资源上下文列表
source (str): 事件源,指示事件的触发来源
# 输出参数
updated (bool): 是否已更新,默认值为 False
updated_contexts (Optional[List[Context]]): 已更新的资源上下文列表,默认值为 None
source (str): 更新源,默认值为 "未知更新源"
"""
# 输入参数
contexts: Any = Field(None, description="待选择的资源上下文列表")
downloader: Optional[str] = Field(None, description="下载器")
origin: Optional[str] = Field(None, description="来源")
# 输出参数
updated: bool = Field(default=False, description="是否已更新")
updated_contexts: Optional[List[Any]] = Field(
default=None, description="已更新的资源上下文列表"
)
source: Optional[str] = Field(default="未知拦截源", description="拦截源")
class ResourceDownloadEventData(ChainEventData):
"""
ResourceDownload 事件的数据模型
Attributes:
# 输入参数
context (Context): 当前资源上下文
episodes (Set[int]): 需要下载的集数
channel (NotificationChannel): 通知渠道
origin (str): 来源(消息通知、Subscribe、Manual等)
downloader (str): 下载器
options (dict): 其他参数
# 输出参数
cancel (bool): 是否取消下载,默认值为 False
source (str): 拦截源,默认值为 "未知拦截源"
reason (str): 拦截原因,描述拦截的具体原因
"""
# 输入参数
context: Any = Field(None, description="当前资源上下文")
episodes: Optional[Set[int]] = Field(None, description="需要下载的集数")
channel: Optional[NotificationChannel] = Field(None, description="通知渠道")
origin: Optional[str] = Field(None, description="来源")
downloader: Optional[str] = Field(None, description="下载器")
options: Optional[dict] = Field(default={}, description="其他参数")
# 输出参数
cancel: bool = Field(default=False, description="是否取消下载")
source: str = Field(default="未知拦截源", description="拦截源")
reason: str = Field(default="", description="拦截原因")
class TransferInterceptEventData(ChainEventData):
"""
TransferIntercept 事件的数据模型
Attributes:
# 输入参数
fileitem (FileItem): 源文件
meta (Any): 元数据
target_storage (str): 目标存储
target_path (Path): 目标路径
transfer_type (str): 整理方式(copy、move、link、softlink等)
options (dict): 其他参数
# 输出参数
cancel (bool): 是否取消下载,默认值为 False
source (str): 拦截源,默认值为 "未知拦截源"
reason (str): 拦截原因,描述拦截的具体原因
"""
# 输入参数
fileitem: FileItem = Field(..., description="源文件")
meta: Optional[Any] = Field(default=None, description="元数据")
mediainfo: Any = Field(..., description="媒体信息")
target_storage: str = Field(..., description="目标存储")
target_path: Path = Field(..., description="目标路径")
transfer_type: str = Field(..., description="整理方式")
options: Optional[dict] = Field(default=None, description="其他参数")
# 输出参数
cancel: bool = Field(default=False, description="是否取消整理")
source: str = Field(default="未知拦截源", description="拦截源")
reason: str = Field(default="", description="拦截原因")
class TransferOverwriteCheckEventData(ChainEventData):
"""
TransferOverwriteCheck 事件的数据模型
在覆盖模式判断(如按文件大小覆盖)执行之前触发,允许插件提供源文件与
目标文件的真实大小(例如本地 .strm 文件指向的网盘原始文件大小),或者
直接给出覆盖决策。
Attributes:
# 输入参数
fileitem (FileItem): 源文件
target_item (FileItem): 目标文件(已存在)
target_storage (str): 目标存储
target_path (Path): 目标文件路径
overwrite_mode (str): 覆盖模式(always、size、never、latest
transfer_type (str): 整理方式
options (dict): 其他参数
# 输出参数
source_size (Optional[int]): 由插件提供的源文件真实大小,覆盖
fileitem.size 用于 size 模式比较;为 None 时表示不修改
target_size (Optional[int]): 由插件提供的目标文件真实大小,覆盖
target_item.size 用于 size 模式比较;为 None 时表示不修改
overwrite (Optional[bool]): 由插件直接给出的覆盖决策,非 None 时
将完全跳过 MoviePilot 内置的 size/never/latest 等比较逻辑
source (str): 处理来源
reason (str): 处理原因,描述插件做出决策或修改的原因
"""
# 输入参数
fileitem: FileItem = Field(..., description="源文件")
target_item: FileItem = Field(..., description="目标已存在文件")
target_storage: str = Field(..., description="目标存储")
target_path: Path = Field(..., description="目标文件路径")
overwrite_mode: str = Field(..., description="覆盖模式")
transfer_type: str = Field(..., description="整理方式")
options: Optional[dict] = Field(default=None, description="其他参数")
# 输出参数
source_size: Optional[int] = Field(
default=None, description="插件提供的源文件真实大小"
)
target_size: Optional[int] = Field(
default=None, description="插件提供的目标文件真实大小"
)
overwrite: Optional[bool] = Field(
default=None, description="插件直接给出的覆盖决策"
)
source: str = Field(default="未知处理源", description="处理来源")
reason: str = Field(default="", description="处理原因")
class DiscoverMediaSource(BaseModel):
"""
探索媒体数据源的基类。
``mediaid_prefix`` 是既有插件与前端标签使用的稳定标识;
``media_source`` 是新的规范媒体来源,可以是内置常量或插件扩展成员。模型同时
输出两者,并在输入时互相补齐,以兼容尚未升级的已安装插件。
"""
name: str = Field(..., description="数据源名称")
media_source: MediaSource = Field(..., description="内置或插件扩展媒体来源")
mediaid_prefix: str = Field(..., description="兼容插件使用的媒体ID前缀")
api_path: str = Field(..., description="媒体数据源API地址")
filter_params: Optional[Dict[str, JsonData]] = Field(
default=None, description="过滤参数"
)
filter_ui: Optional[List[Dict[str, JsonData]]] = Field(default=[], description="过滤参数UI配置")
depends: Optional[Dict[str, list[str]]] = Field(
default=None, description="UI依赖关系字典"
)
@model_validator(mode="before")
@classmethod
def normalize_media_identity(cls, value: Any) -> Any:
"""在旧前缀与规范媒体来源之间双向补齐发现源身份。"""
if not isinstance(value, dict):
return value
normalized = dict(value)
media_source = normalized.get("media_source")
mediaid_prefix = normalized.get("mediaid_prefix")
if media_source and not mediaid_prefix:
normalized["mediaid_prefix"] = str(media_source)
elif mediaid_prefix and not media_source:
normalized["media_source"] = cls._media_source_from_prefix(
str(mediaid_prefix)
)
return normalized
@staticmethod
def _media_source_from_prefix(mediaid_prefix: str) -> MediaSource:
"""将旧插件前缀映射为内置或插件扩展媒体来源。"""
aliases = {
"mangguo": MediaSource.MangoTV,
"tencentvideo": MediaSource.TencentVideo,
}
if mediaid_prefix in aliases:
return aliases[mediaid_prefix]
return MediaSource(mediaid_prefix)
class MediaSourceInfo(BaseModel):
"""
媒体数据源注册描述。
插件通过该描述声明来源的展示名称和支持的媒体类型;识别、搜索和刮削的
实际实现仍由插件模块方法提供,宿主只负责把来源传递到统一媒体链路。
"""
name: str = Field(..., description="数据源展示名称")
media_source: MediaSource = Field(..., description="规范媒体来源标识")
media_types: List[MediaType] = Field(
default_factory=lambda: [MediaType.MOVIE, MediaType.TV],
description="支持的媒体类型",
)
class DiscoverSourceEventData(ChainEventData):
"""
DiscoverSource 事件的数据模型
Attributes:
# 输出参数
extra_sources (List[DiscoverMediaSource]): 额外媒体数据源
"""
# 输出参数
extra_sources: List[DiscoverMediaSource] = Field(
default_factory=list, description="额外媒体数据源"
)
class RecommendMediaSource(BaseModel):
"""
推荐媒体数据源的基类
"""
name: str = Field(..., description="数据源名称")
api_path: str = Field(..., description="媒体数据源API地址")
type: str = Field(..., description="类型")
class RecommendSourceEventData(ChainEventData):
"""
RecommendSource 事件的数据模型
Attributes:
# 输出参数
extra_sources (List[RecommendMediaSource]): 额外媒体数据源
"""
# 输出参数
extra_sources: List[RecommendMediaSource] = Field(
default_factory=list, description="额外媒体数据源"
)
class MediaRecognizeConvertEventData(RequiredMediaIdentityMixin, ChainEventData):
"""
MediaRecognizeConvert 事件的数据模型
Attributes:
# 输入参数
media_source (MediaSource): 输入内置或插件扩展媒体来源
media_id (str): 数据源原生 ID
target_media_source (MediaSource): 需要转换到的内置或插件扩展媒体来源
# 输出参数
media_dict (dict): TheMovieDb/豆瓣的媒体数据
"""
# 输入参数
media_source: MediaSource = Field(..., description="媒体来源")
media_id: str = Field(..., description="数据源原生 ID")
target_media_source: MediaSource = Field(..., description="目标媒体来源")
# 输出参数
media_dict: dict = Field(
default_factory=dict, description="转换后的媒体信息"
)
class StorageOperSelectionEventData(ChainEventData):
"""
StorageOperSelect 事件的数据模型
Attributes:
# 输入参数
storage (str): 存储类型
# 输出参数
storage_oper (Callable): 存储操作对象
"""
# 输入参数
storage: Optional[str] = Field(default=None, description="存储类型")
# 输出参数
storage_oper: Optional[Callable] = Field(default=None, description="存储操作对象")
class SubscribeEpisodesRefreshEventData(OptionalMediaIdentityMixin, ChainEventData):
"""
SubscribeEpisodesRefresh 事件的数据模型
主程序在推算订阅某季总集数时发出,携带主程序本次识别到的 TMDB 当前季总集数;
外部可据自身策略向上覆盖 total_episode(如待定集数),低于 current_total_episode 的覆盖值会被主程序钳制。
Attributes:
# 输入参数
media_source (Optional[MediaSource]): 媒体来源
media_id (Optional[str]): 数据源原生 ID
season (Optional[int]): 季号
mediainfo (Any): 媒体信息
current_total_episode (int): 主程序本次识别到的 TMDB 当前季总集数
subscribe_id (Optional[int]): 订阅 ID;订阅创建场景下尚未入库,为空
scene (Optional[str]): 触发场景,create/refresh/precheck
# 输出参数
updated (bool): 外部是否覆盖了总集数,默认 False
total_episode (Optional[int]): 覆盖后的总集数,仅在 updated=True 时生效;低于 current_total_episode 时由主程序钳制
source (str): 覆盖来源
reason (str): 覆盖原因
"""
# 输入参数
media_source: Optional[MediaSource] = Field(default=None, description="媒体数据源")
media_id: Optional[str] = Field(default=None, description="数据源原生 ID")
season: Optional[int] = Field(default=None, description="季号")
mediainfo: Any = Field(default=None, description="媒体信息")
current_total_episode: int = Field(default=0, description="主程序本次识别到的 TMDB 当前季总集数")
subscribe_id: Optional[int] = Field(default=None, description="订阅 ID;创建场景为空")
scene: Optional[str] = Field(default=None, description="触发场景:create/refresh/precheck")
# 输出参数
updated: bool = Field(default=False, description="外部是否覆盖了总集数")
total_episode: Optional[int] = Field(default=None, description="覆盖后的总集数;低于主程序本次识别到的 TMDB 当前季总集数时由主程序钳制")
source: str = Field(default="未知来源", description="覆盖来源")
reason: str = Field(default="", description="覆盖原因")
class SubscribeModifiedEventData(BaseEventData):
"""
SubscribeModified 广播事件数据。
主程序在订阅字段被普通更新、状态入口、重置或 Agent 更新后发出。payload
继续保持 dict 形态,scene 用于表达操作场景,fields 表达最终快照里的真实字段差异。
"""
subscribe_id: int = Field(description="订阅 ID")
old_subscribe_info: Dict[str, Any] = Field(default_factory=dict, description="更新前订阅快照")
subscribe_info: Dict[str, Any] = Field(default_factory=dict, description="更新后订阅快照")
scene: str = Field(default="update", description="触发场景:update/status/reset/agent_update")
fields: List[str] = Field(default_factory=list, description="真实变更字段")
idempotency_key: Optional[str] = Field(default=None, description="宿主生成的幂等键")
@model_validator(mode="after")
def compute_fields(self):
self.fields = self._diff_fields(self.old_subscribe_info, self.subscribe_info)
return self
@staticmethod
def _diff_fields(old_info: Dict[str, Any], new_info: Dict[str, Any]) -> List[str]:
"""
按 old/new 快照并集计算真实字段差异;缺失 key 按 None 参与比较。
"""
old_info = old_info or {}
new_info = new_info or {}
keys = set(old_info) | set(new_info)
return sorted(key for key in keys if old_info.get(key) != new_info.get(key))
def to_dict(self) -> Dict[str, Any]:
"""
输出公开事件 payload,避免内部属性被未来扩展意外暴露。
"""
payload = {
"subscribe_id": self.subscribe_id,
"old_subscribe_info": self.old_subscribe_info,
"subscribe_info": self.subscribe_info,
"scene": self.scene,
"fields": list(self.fields),
}
if self.idempotency_key:
payload["idempotency_key"] = self.idempotency_key
return payload
class SubscribeAddedEventData(BaseEventData):
"""SubscribeAdded 广播事件的可恢复公开 payload。"""
subscribe_id: int = Field(description="订阅 ID")
username: Optional[str] = Field(default=None, description="发起订阅的用户")
mediainfo: Dict[str, Any] = Field(default_factory=dict, description="媒体信息快照")
idempotency_key: Optional[str] = Field(default=None, description="宿主生成的幂等键")
class SubscribeDeletedEventData(BaseEventData):
"""SubscribeDeleted 广播事件的可恢复公开 payload。"""
subscribe_id: int = Field(description="订阅 ID")
subscribe_info: Dict[str, Any] = Field(default_factory=dict, description="删除前订阅快照")
idempotency_key: Optional[str] = Field(default=None, description="宿主生成的幂等键")
class DownloadAddedEventData(BaseEventData):
"""DownloadAdded 广播事件的插件兼容 payload。"""
hash: str = Field(description="下载任务 hash")
context: Any = Field(description="下载上下文对象")
username: Optional[str] = Field(default=None, description="发起下载的用户")
downloader: Optional[str] = Field(default=None, description="下载器名称")
episodes: List[int] = Field(default_factory=list, description="下载剧集列表")
source: Optional[str] = Field(default=None, description="下载来源")
idempotency_key: Optional[str] = Field(default=None, description="宿主生成的幂等键")
class TransferResultEventData(BaseEventData):
"""TransferComplete/Failed 共用的插件兼容 payload。"""
fileitem: Optional[FileItem] = Field(default=None, description="源文件项")
meta: Any = Field(default=None, description="文件名解析对象")
mediainfo: Any = Field(default=None, description="媒体信息对象")
transferinfo: Optional[TransferInfo] = Field(default=None, description="整理结果")
downloader: Optional[str] = Field(default=None, description="下载器名称")
download_hash: Optional[str] = Field(default=None, description="下载任务 hash")
transfer_history_id: Optional[int] = Field(default=None, description="整理历史 ID")
idempotency_key: Optional[str] = Field(default=None, description="宿主生成的幂等键")
class SubscribeCompletionCheckEventData(ChainEventData):
"""
SubscribeCompletionCheck 事件的数据模型
在订阅被自动判定完成、即将收口(写历史并删除)之前发出,允许外部据完结策略否决本次完成
Attributes:
# 输入参数
subscribe (Any): 订阅对象
mediainfo (Any): 媒体信息
meta (Any): 元数据
# 输出参数
cancel (bool): 是否否决本次完成,默认 False
source (str): 否决来源
reason (str): 否决原因
"""
# 输入参数
subscribe: Any = Field(default=None, description="订阅对象")
mediainfo: Any = Field(default=None, description="媒体信息")
meta: Any = Field(default=None, description="元数据")
# 输出参数
cancel: bool = Field(default=False, description="是否否决本次完成")
source: str = Field(default="未知来源", description="否决来源")
reason: str = Field(default="", description="否决原因")