Files
MoviePilot/docs/rules/09-external-response.md
jxxghp b82bbcb420 fix(webhook)!: guard unknown movie paths in transfer history
Recover Jellyfin deletion paths from webhook payloads or bounded server-scoped caches. Avoid querying deleted items and preserve TV season/series lookup behavior.

BREAKING CHANGE: Movie and unspecified-type history queries without a destination now return no records unless match_all_versions=True is explicitly requested.

Refs #6988
2026-10-13 23:48:10 +08:00

7.0 KiB
Raw Blame History

09 — External APIs, Protocols, and Responses

HTTP Client Conventions

Rule: Host-authored ordinary outbound HTTP must go through RequestUtils from app/adapters/network/http.py. This transport rule does not authorize Application or Chain to import the concrete Adapter; they own/use a Port and receive its implementation from startup. Plugins import the curated facade from app.sdk.network.

Direct requests/httpx clients, SDK transports, product streaming protocols, contained vendor code, diagnostics and control-plane access are governed by the exact direct_egress facts and manual policy. An approved fingerprint is containment for that exact source/binding/use identity only; owner: "$source" names that source module and does not authorize another caller or operation. New SDK roots must be added to the registry and receive explicit policy review. Ordinary direct HTTP, RequestUtils Session bridges and Application-owned DNS I/O remain temporary_debt with an empty target state. Runtime wildcard imports from registered egress roots are forbidden.

RequestUtils handles:

  • Proxy configuration (from settings.PROXY_*)
  • Timeouts
  • SSL verification settings
  • User-Agent headers
  • Retry logic
# Adapter implementation example; Application/Chain consume an injected Port instead.
from app.adapters.network.http import RequestUtils

res = RequestUtils(
    ua=settings.USER_AGENT,
    proxies=settings.PROXY,
    timeout=30,
).get_res(url="https://api.example.com/data")

if res and res.status_code == 200:
    data = res.json()

Response Format — REST API

All REST API responses use Pydantic schema models from app/schemas/. Do not return raw dict objects from endpoints.

Standard Response Patterns

# Success with data
from app.schemas.response import Response

return Response(success=True, message="", data=result)

# Success without data
return Response(success=True, message="操作成功")

# Error
return Response(success=False, message="错误原因描述")

List Responses

For paginated lists, follow the pattern of existing endpoint files. Check app/api/endpoints/ for examples matching the resource domain.

Error Responses (Endpoint Layer Only)

In endpoints, raise HTTPException for request-level errors:

from fastapi import HTTPException

raise HTTPException(status_code=404, detail="Resource not found")
raise HTTPException(status_code=403, detail="Permission denied")

Do not raise HTTPException in chain or module code. Chains and modules return None or domain-level error objects on failure; the endpoint translates that into an HTTP response.


Error Handling by Layer

Layer On external API failure
Module Log the error, return None or (False, "error message") tuple
Chain Log the error, return None or an appropriate domain object with failure indication
Endpoint Translate None or failure result into a Response(success=False, ...) or HTTPException
# Module layer
def test(self) -> Optional[Tuple[bool, str]]:
    """测试模块连通性"""
    try:
        ok = self.client.ping()
        return (True, "连接成功") if ok else (False, "连接失败")
    except Exception as err:
        logger.error(f"测试连通性失败:{str(err)}")
        return (False, str(err))

MCP Protocol

MoviePilot exposes an MCP (Model Context Protocol) interface for AI agent integration.

  • Transport: HTTP, JSON-RPC 2.0
  • Base path: /api/v1/mcp
  • Protocol versions supported: 2025-11-25, 2025-06-18, 2024-11-05

Authentication

Header: X-API-KEY: <api_key>
Query:  ?apikey=<api_key>

Supported Methods

Method Description
initialize Initialize session, negotiate protocol version and capabilities
notifications/initialized Client confirmation of initialization
tools/list List all available tools
tools/call Invoke a specific tool
ping Connection liveness check

Error Codes

Code Message Meaning
-32700 Parse error Malformed JSON
-32600 Invalid Request Invalid JSON-RPC request structure
-32601 Method not found Unknown method
-32602 Invalid params Parameter validation failure
-32002 Session not found Session does not exist or has expired
-32003 Not initialized Session has not completed initialization
-32603 Internal error Server-side error

Tool Response Format

MCP tools return structured content. Errors must use the JSON-RPC error object format, not HTTP status codes.


Notification and Messaging

Internal notifications use the Notification schema and the event system:

from app.schemas import Notification
from app.schemas.types import NotificationType, MessageChannel
from app.runtime.events import eventmanager
from app.schemas.types import EventType

eventmanager.send_event(
    EventType.NoticeMessage,
    {
        "channel": MessageChannel.Telegram,
        "type": NotificationType.Download,
        "title": "下载成功",
        "text": f"{media_name} 已添加到下载队列",
        "image": poster_url,
    }
)

Do not call message channel modules directly from chain code. Use the event bus to decouple senders from channels.


Media Metadata API Conventions

When calling TMDB, TheTVDB, Douban, or Bangumi via the module layer:

  • Always check the module return for None before using the result — modules return None when the backend is not configured or the request fails.
  • Cache responses using FileCache / AsyncFileCache where the result is stable and repeated requests would be expensive.
  • Return domain objects (MediaInfo, TmdbEpisode, MediaPerson, etc.) from modules, never raw API response dicts.

Webhook Handling

Webhook payloads arrive at app/api/endpoints/webhook.py and are dispatched via eventmanager.send_event(EventType.WebhookMessage, ...). Processing logic lives in the chain layer (app/chain/webhook.py).

Do not add webhook-specific business logic directly in the endpoint. The endpoint parses the payload and fires the event; the chain handles the response.

Jellyfin 的条目删除后无法通过播放接口反查路径。适配器优先使用 webhook 的 Path; 非删除事件查到的路径会按服务器和条目 ID 缓存在进程内(最多 10000 条、有效期 30 天), 删除事件消费缓存,不请求已删除条目的路径或图片。重启、过期或从未观察过的条目仍可能 没有路径,此时告警并保留 item_path=None,消费方不得据此推断同片其他副本。

插件兼容接口 TransferHistoryOper.get_by 及底层 TransferHistory.list_by 对电影和 未指定类型的查询要求非空 dest,空路径默认返回空列表。确实需要按身份或标题年份查询 全部版本时,调用方须显式传入 match_all_versions=True;路径未知的删除事件不得使用 该参数。电视剧整剧、整季及单集路径精确匹配继续沿用原有查询语义。

Last Updated: 2026-08-14