mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 02:05:13 +08:00
fix(api): restore successful query response semantics
This commit is contained in:
@@ -107,7 +107,7 @@ async def exists_local(
|
||||
)
|
||||
if exist:
|
||||
ret_info = {"id": exist.item_id}
|
||||
return schemas.Response(success=True if exist else False, data={"item": ret_info})
|
||||
return schemas.Response(success=True, data={"item": ret_info})
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -69,6 +69,10 @@ class TransferHistory(OptionalMediaIdentityMixin, BaseModel):
|
||||
|
||||
# ID
|
||||
id: int
|
||||
# 源存储类型
|
||||
src_storage: Optional[str] = None
|
||||
# 目标存储类型
|
||||
dest_storage: Optional[str] = None
|
||||
# 源目录
|
||||
src: Optional[str] = None
|
||||
# 目的目录
|
||||
|
||||
@@ -117,6 +117,7 @@ MoviePilot 也提供普通 REST API 给前端和自动化客户端使用。所
|
||||
- 普通 JSON REST 接口统一使用 `/api/v1`,不再提供 `/api/v2` 套壳版本。
|
||||
- 成功和失败响应都只包含 `success`、`message`、`data` 三个顶层字段;各接口只有 `data` 的模型可以变化。
|
||||
- 成功响应为 `{"success": true, "message": "", "data": <接口数据>}`。HTTP 错误保留原状态码,返回 `{"success": false, "message": <错误原因>, "data": null}`;请求参数校验错误会在 `data` 中附带结构化错误列表。
|
||||
- 查询接口未命中但请求已正常完成时仍返回 `success=true`,存在性等业务状态通过 `data` 表达。例如 `/mediaserver/exists` 未命中时返回空的 `data.item`。
|
||||
- 每个普通 JSON 端点都会在 OpenAPI 中声明具体的 `Response[DataModel]`,调用方可从 `/docs` 或 `/api/v1/openapi.json` 查询数据结构。
|
||||
- SSE、文件、图片、HTML、空响应,以及 OAuth2 登录、OpenAI、Anthropic、MCP JSON-RPC 等标准协议端点保持协议原生响应体;它们会在 OpenAPI 中显式声明对应的流、文件或协议模型。
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ Streaming search sends `{"type":"heartbeat"}` every 15 seconds without business
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/history/download` | Download history, newest first. Params: `page`, `count`. `poster` is the poster image; legacy `image` is the backdrop image. |
|
||||
| DELETE | `/api/v1/history/download` | Delete download history. Body: DownloadHistory JSON |
|
||||
| GET | `/api/v1/history/transfer` | Transfer history. Params: `title`, `page`, `count`, `status` |
|
||||
| GET | `/api/v1/history/transfer` | Transfer history, including `src_storage` and `dest_storage` for path labels. Params: `title`, `page`, `count`, `status` |
|
||||
| DELETE | `/api/v1/history/transfer` | Delete transfer history. Params: `deletesrc`, `deletedest`. Body: TransferHistory |
|
||||
| GET | `/api/v1/history/empty/transfer` | Clear all transfer history |
|
||||
|
||||
@@ -321,7 +321,7 @@ Streaming search sends `{"type":"heartbeat"}` every 15 seconds without business
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/mediaserver/play/{itemid}` | Play media online |
|
||||
| GET | `/api/v1/mediaserver/exists` | Check if media exists in the local library database. Params: `media_source` + `media_id`, or `title` discovery; optional `year`, `mtype`, `season` |
|
||||
| GET | `/api/v1/mediaserver/exists` | Check if media exists in the local library database. A completed miss is `success=true` with an empty `data.item`. Params: `media_source` + `media_id`, or `title` discovery; optional `year`, `mtype`, `season` |
|
||||
| POST | `/api/v1/mediaserver/exists_remote` | Check existing episodes (remote). Body: MediaInfo JSON |
|
||||
| POST | `/api/v1/mediaserver/notexists` | Check missing episodes (remote). Body: MediaInfo JSON |
|
||||
| GET | `/api/v1/mediaserver/latest` | Latest library items. Params: `server` (required), `count` |
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from app.schemas.event import DiscoverMediaSource
|
||||
from app.schemas.file import FileItem, StorageTransType
|
||||
from app.schemas.history import TransferHistory
|
||||
from app.schemas.mediaserver import MediaServerLibrary, MediaServerPlayItem, NotExistMediaInfo
|
||||
from app.schemas.plugin import Plugin, PluginDashboard
|
||||
from app.schemas.site import SiteStatistic, SiteUserData
|
||||
@@ -43,6 +44,15 @@ def test_stable_collections_serialize_without_changing_payload_shape():
|
||||
assert StorageTransType(transtype={"move": "移动"}).model_dump()["transtype"] == {
|
||||
"move": "移动"
|
||||
}
|
||||
history = TransferHistory(
|
||||
id=1,
|
||||
src_storage="local",
|
||||
dest_storage="alist",
|
||||
src="/downloads/demo.mkv",
|
||||
dest="/media/demo.mkv",
|
||||
).model_dump()
|
||||
assert history["src_storage"] == "local"
|
||||
assert history["dest_storage"] == "alist"
|
||||
assert episode.model_dump()["crew"][0]["job"] == "Writer"
|
||||
assert episode.model_dump()["guest_stars"][0]["character"] == "Guest"
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import schemas
|
||||
from app.api.endpoints import mediaserver as mediaserver_endpoint
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.core.context import MediaInfo as CoreMediaInfo
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
@@ -79,3 +81,35 @@ def test_media_response_accepts_cross_source_credit_shapes() -> None:
|
||||
assert media["directors"][0] == "导演甲"
|
||||
assert media["directors"][1]["id"] == 3
|
||||
assert media["directors"][1]["name"] == "导演乙"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_media_exists_not_found_is_a_successful_query(monkeypatch) -> None:
|
||||
"""媒体库未命中是查询结果,不应被统一客户端识别为接口失败。"""
|
||||
|
||||
class EmptyMediaServerOper:
|
||||
"""返回未命中的媒体库查询桩。"""
|
||||
|
||||
async def async_exists(self, **_kwargs):
|
||||
"""模拟媒体库中不存在目标媒体。"""
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
mediaserver_endpoint,
|
||||
"MediaServerOper",
|
||||
lambda _db: EmptyMediaServerOper(),
|
||||
)
|
||||
|
||||
response = await mediaserver_endpoint.exists_local(
|
||||
title="未入库电影",
|
||||
year="2026",
|
||||
mtype="电影",
|
||||
media_source=None,
|
||||
media_id=None,
|
||||
season=None,
|
||||
db=object(),
|
||||
_=None,
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
assert response.data == {"item": {}}
|
||||
|
||||
Reference in New Issue
Block a user