mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-04-01 09:51:22 +08:00
feat: 新增 moviepilot-api 技能,支持全量 REST API 调用;技能中间件自动同步内置技能到用户目录
This commit is contained in:
@@ -105,6 +105,7 @@ class MoviePilotAgent:
|
||||
# Skills
|
||||
SkillsMiddleware(
|
||||
sources=[str(settings.CONFIG_PATH / "agent" / "skills")],
|
||||
bundled_skills_dir=str(settings.ROOT_PATH / "skills"),
|
||||
),
|
||||
# Jobs 任务管理
|
||||
JobsMiddleware(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import re
|
||||
import shutil
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Annotated, List
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
@@ -285,17 +287,69 @@ Remember: Skills make you more capable and consistent. When in doubt, check if a
|
||||
"""
|
||||
|
||||
|
||||
def _sync_bundled_skills(bundled_dir: Path, target_dir: Path) -> None:
|
||||
"""将项目自带的技能同步到用户目录。
|
||||
|
||||
仅当目标目录中不存在对应技能子目录时才复制,已存在则跳过(不覆盖用户修改)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bundled_dir : Path
|
||||
项目内置技能目录(如 ``ROOT_PATH / "skills"``)。
|
||||
target_dir : Path
|
||||
用户配置技能目录(如 ``CONFIG_PATH / "agent" / "skills"``)。
|
||||
"""
|
||||
if not bundled_dir.is_dir():
|
||||
return
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for skill_src in bundled_dir.iterdir():
|
||||
if not skill_src.is_dir():
|
||||
continue
|
||||
skill_md = skill_src / "SKILL.md"
|
||||
if not skill_md.is_file():
|
||||
continue
|
||||
|
||||
skill_dst = target_dir / skill_src.name
|
||||
if skill_dst.exists():
|
||||
# 目标已存在,跳过(不覆盖用户自定义修改)
|
||||
continue
|
||||
|
||||
try:
|
||||
shutil.copytree(str(skill_src), str(skill_dst))
|
||||
logger.info("已自动复制内置技能 '%s' -> '%s'", skill_src.name, skill_dst)
|
||||
except Exception as e:
|
||||
logger.warning("复制内置技能 '%s' 失败: %s", skill_src.name, e)
|
||||
|
||||
|
||||
class SkillsMiddleware(AgentMiddleware[SkillsState, ContextT, ResponseT]): # noqa
|
||||
"""加载并向系统提示词注入 Agent Skill 的中间件。
|
||||
|
||||
按源顺序加载 Skill,后加载的会覆盖重名的。
|
||||
启动时自动将项目内置技能(bundled_skills_dir)同步到用户技能目录。
|
||||
"""
|
||||
|
||||
state_schema = SkillsState
|
||||
|
||||
def __init__(self, *, sources: list[str]) -> None:
|
||||
"""初始化 Skill 中间件。"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sources: list[str],
|
||||
bundled_skills_dir: str | None = None,
|
||||
) -> None:
|
||||
"""初始化 Skill 中间件。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sources : list[str]
|
||||
用户技能目录列表。
|
||||
bundled_skills_dir : str | None
|
||||
项目内置技能目录路径。若提供,在首次加载前会将其中不存在于
|
||||
sources 首个目录的技能自动复制过去。
|
||||
"""
|
||||
self.sources = sources
|
||||
self.bundled_skills_dir = bundled_skills_dir
|
||||
self.system_prompt_template = SKILLS_SYSTEM_PROMPT
|
||||
|
||||
def _format_skills_locations(self) -> str:
|
||||
@@ -350,11 +404,21 @@ class SkillsMiddleware(AgentMiddleware[SkillsState, ContextT, ResponseT]): # no
|
||||
"""在 Agent 执行前异步加载技能元数据。
|
||||
|
||||
每个会话仅加载一次。若 state 中已有则跳过。
|
||||
首次加载时,会先将内置技能同步到用户目录(如不存在)。
|
||||
"""
|
||||
# 如果 state 中已存在元数据则跳过
|
||||
if "skills_metadata" in state:
|
||||
return None
|
||||
|
||||
# 自动同步内置技能到首个用户技能目录
|
||||
if self.bundled_skills_dir and self.sources:
|
||||
bundled = Path(self.bundled_skills_dir)
|
||||
target = Path(self.sources[0])
|
||||
try:
|
||||
_sync_bundled_skills(bundled, target)
|
||||
except Exception as e:
|
||||
logger.warning("同步内置技能失败: %s", e)
|
||||
|
||||
all_skills: dict[str, SkillMetadata] = {}
|
||||
|
||||
# 遍历源按顺序加载技能,重名时后者覆盖前者
|
||||
|
||||
544
skills/moviepilot-api/SKILL.md
Normal file
544
skills/moviepilot-api/SKILL.md
Normal file
@@ -0,0 +1,544 @@
|
||||
---
|
||||
name: moviepilot-api
|
||||
description: Use this skill when you need to call MoviePilot REST API endpoints directly. Covers all 237 API endpoints across 27 categories including media search, downloads, subscriptions, library management, site management, system administration, plugins, workflows, and more. Use this skill whenever the user asks to interact with MoviePilot via its HTTP API, or when the moviepilot-cli skill cannot cover a specific operation.
|
||||
---
|
||||
|
||||
# MoviePilot REST API
|
||||
|
||||
> All script paths are relative to this skill file.
|
||||
|
||||
Use `scripts/mp-api.py` to call any MoviePilot REST API endpoint directly.
|
||||
|
||||
## Setup
|
||||
|
||||
Configure the backend host and API key (persisted to `~/.config/moviepilot_api/config`):
|
||||
|
||||
```
|
||||
python scripts/mp-api.py configure --host http://localhost:3000 --apikey <API_TOKEN>
|
||||
```
|
||||
|
||||
The API key is the `API_TOKEN` value from MoviePilot settings.
|
||||
|
||||
## How to Call APIs
|
||||
|
||||
### General syntax
|
||||
|
||||
```
|
||||
python scripts/mp-api.py <METHOD> <PATH> [key=value ...] [--json '<body>']
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
- By default, the key is sent via the `X-API-KEY` header.
|
||||
- For endpoints suffixed with `2` (e.g. `/api/v1/dashboard/statistic2`), use `--token-param` to send the key as `?token=`.
|
||||
- Both methods validate against the same `API_TOKEN` value.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# GET with query params
|
||||
python scripts/mp-api.py GET /api/v1/media/search title="Avatar" type="movie"
|
||||
|
||||
# POST with JSON body
|
||||
python scripts/mp-api.py POST /api/v1/download/add --json '{"torrent_url":"abc1234:1"}'
|
||||
|
||||
# DELETE
|
||||
python scripts/mp-api.py DELETE /api/v1/subscribe/123
|
||||
|
||||
# Endpoints that require ?token= auth
|
||||
python scripts/mp-api.py GET /api/v1/dashboard/statistic2 --token-param
|
||||
```
|
||||
|
||||
## Complete API Reference
|
||||
|
||||
All endpoints are under the base URL `{MP_HOST}`. Path parameters are shown as `{param}`.
|
||||
|
||||
---
|
||||
|
||||
### Media Search (13 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/media/search` | Search media/person by title. Params: `title` (required), `type`, `page`, `count` |
|
||||
| GET | `/api/v1/media/recognize` | Recognize media from torrent title. Params: `title` (required), `subtitle` |
|
||||
| GET | `/api/v1/media/recognize2` | Recognize media (API_TOKEN auth, use `--token-param`). Params: `title`, `subtitle` |
|
||||
| GET | `/api/v1/media/recognize_file` | Recognize media from file path. Params: `path` (required) |
|
||||
| GET | `/api/v1/media/recognize_file2` | Recognize file (API_TOKEN auth). Params: `path` |
|
||||
| POST | `/api/v1/media/scrape/{storage}` | Scrape media metadata. Body: FileItem JSON |
|
||||
| GET | `/api/v1/media/category/config` | Get category strategy config |
|
||||
| POST | `/api/v1/media/category/config` | Save category strategy config. Body: CategoryConfig |
|
||||
| GET | `/api/v1/media/category` | Get auto-categorization config |
|
||||
| GET | `/api/v1/media/group/seasons/{episode_group}` | Get episode group seasons |
|
||||
| GET | `/api/v1/media/groups/{tmdbid}` | Get media episode groups |
|
||||
| GET | `/api/v1/media/seasons` | Get media season info. Params: `mediaid`, `title`, `year`, `season` |
|
||||
| GET | `/api/v1/media/{mediaid}` | Get media detail. Params: `type_name` (required: movie/tv), `title`, `year` |
|
||||
|
||||
### TMDB (8 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/tmdb/seasons/{tmdbid}` | All seasons for a TMDB title |
|
||||
| GET | `/api/v1/tmdb/similar/{tmdbid}/{type_name}` | Similar movies/TV shows |
|
||||
| GET | `/api/v1/tmdb/recommend/{tmdbid}/{type_name}` | Recommended movies/TV shows |
|
||||
| GET | `/api/v1/tmdb/collection/{collection_id}` | Collection details. Params: `page`, `count` |
|
||||
| GET | `/api/v1/tmdb/credits/{tmdbid}/{type_name}` | Cast and crew. Params: `page` |
|
||||
| GET | `/api/v1/tmdb/person/{person_id}` | Person details |
|
||||
| GET | `/api/v1/tmdb/person/credits/{person_id}` | Person's filmography. Params: `page` |
|
||||
| GET | `/api/v1/tmdb/{tmdbid}/{season}` | All episodes of a season. Params: `episode_group` |
|
||||
|
||||
### Douban (5 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/douban/{doubanid}` | Douban media detail |
|
||||
| GET | `/api/v1/douban/person/{person_id}` | Person detail |
|
||||
| GET | `/api/v1/douban/person/credits/{person_id}` | Person filmography. Params: `page` |
|
||||
| GET | `/api/v1/douban/credits/{doubanid}/{type_name}` | Cast info (type_name: movie/tv) |
|
||||
| GET | `/api/v1/douban/recommend/{doubanid}/{type_name}` | Recommendations |
|
||||
|
||||
### Bangumi (5 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/bangumi/{bangumiid}` | Bangumi detail |
|
||||
| GET | `/api/v1/bangumi/credits/{bangumiid}` | Cast. Params: `page`, `count` |
|
||||
| GET | `/api/v1/bangumi/recommend/{bangumiid}` | Recommendations. Params: `page`, `count` |
|
||||
| GET | `/api/v1/bangumi/person/{person_id}` | Person detail |
|
||||
| GET | `/api/v1/bangumi/person/credits/{person_id}` | Person filmography. Params: `page`, `count` |
|
||||
|
||||
### Search / Torrents (4 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/search/media/{mediaid}` | Search torrents by media ID (format: `tmdb:123` / `douban:123` / `bangumi:123`). Params: `mtype`, `area`, `title`, `year`, `season`, `sites` |
|
||||
| GET | `/api/v1/search/title` | Fuzzy search torrents by keyword. Params: `keyword`, `page`, `sites` |
|
||||
| GET | `/api/v1/search/last` | Get latest search results |
|
||||
| POST | `/api/v1/search/recommend` | AI recommended resources. Body: `filtered_indices`, `check_only`, `force` |
|
||||
|
||||
### Download (7 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/download/` | List active downloads. Params: `name` (downloader name) |
|
||||
| POST | `/api/v1/download/` | Add download (with media info). Body: JSON |
|
||||
| POST | `/api/v1/download/add` | Add download (without media info). Body: JSON with `torrent_url` |
|
||||
| GET | `/api/v1/download/start/{hashString}` | Resume download task |
|
||||
| GET | `/api/v1/download/stop/{hashString}` | Pause download task |
|
||||
| GET | `/api/v1/download/clients` | List available download clients |
|
||||
| DELETE | `/api/v1/download/{hashString}` | Delete download task. Params: `name` |
|
||||
|
||||
### Subscribe (28 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/subscribe/` | List all subscriptions |
|
||||
| POST | `/api/v1/subscribe/` | Add subscription. Body: Subscribe JSON |
|
||||
| PUT | `/api/v1/subscribe/` | Update subscription. Body: Subscribe JSON |
|
||||
| GET | `/api/v1/subscribe/list` | List subscriptions (API_TOKEN auth, use `--token-param`) |
|
||||
| GET | `/api/v1/subscribe/{subscribe_id}` | Subscription detail |
|
||||
| DELETE | `/api/v1/subscribe/{subscribe_id}` | Delete subscription |
|
||||
| PUT | `/api/v1/subscribe/status/{subid}` | Update subscription status. Params: `state` (required) |
|
||||
| GET | `/api/v1/subscribe/media/{mediaid}` | Query subscription by media ID. Params: `season`, `title` |
|
||||
| DELETE | `/api/v1/subscribe/media/{mediaid}` | Delete subscription by media ID. Params: `season` |
|
||||
| GET | `/api/v1/subscribe/refresh` | Refresh all subscriptions |
|
||||
| GET | `/api/v1/subscribe/reset/{subid}` | Reset subscription |
|
||||
| GET | `/api/v1/subscribe/check` | Refresh subscription TMDB info |
|
||||
| GET | `/api/v1/subscribe/search` | Search all subscriptions |
|
||||
| GET | `/api/v1/subscribe/search/{subscribe_id}` | Search specific subscription |
|
||||
| POST | `/api/v1/subscribe/seerr` | Overseerr/Jellyseerr notification subscription |
|
||||
| GET | `/api/v1/subscribe/history/{mtype}` | Subscription history. Params: `page`, `count` |
|
||||
| DELETE | `/api/v1/subscribe/history/{history_id}` | Delete subscription history |
|
||||
| GET | `/api/v1/subscribe/popular` | Popular subscriptions. Params: `stype` (required), `page`, `count`, `min_sub`, `genre_id`, `min_rating`, `max_rating`, `sort_type` |
|
||||
| GET | `/api/v1/subscribe/user/{username}` | User's subscriptions |
|
||||
| GET | `/api/v1/subscribe/files/{subscribe_id}` | Subscription related files |
|
||||
| POST | `/api/v1/subscribe/share` | Share subscription. Body: SubscribeShare JSON |
|
||||
| DELETE | `/api/v1/subscribe/share/{share_id}` | Delete shared subscription |
|
||||
| POST | `/api/v1/subscribe/fork` | Fork shared subscription. Body: SubscribeShare JSON |
|
||||
| GET | `/api/v1/subscribe/follow` | List followed share users |
|
||||
| POST | `/api/v1/subscribe/follow` | Follow a share user. Params: `share_uid` |
|
||||
| DELETE | `/api/v1/subscribe/follow` | Unfollow a share user. Params: `share_uid` |
|
||||
| GET | `/api/v1/subscribe/shares` | List shared subscriptions. Params: `name`, `page`, `count`, `genre_id`, `min_rating`, `max_rating`, `sort_type` |
|
||||
| GET | `/api/v1/subscribe/share/statistics` | Share statistics |
|
||||
|
||||
### Site (24 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/site/` | List all sites |
|
||||
| POST | `/api/v1/site/` | Add site. Body: Site JSON |
|
||||
| PUT | `/api/v1/site/` | Update site. Body: Site JSON |
|
||||
| GET | `/api/v1/site/{site_id}` | Site detail by ID |
|
||||
| DELETE | `/api/v1/site/{site_id}` | Delete site |
|
||||
| GET | `/api/v1/site/domain/{site_url}` | Site detail by domain |
|
||||
| GET | `/api/v1/site/cookiecloud` | Sync CookieCloud |
|
||||
| GET | `/api/v1/site/reset` | Reset sites |
|
||||
| POST | `/api/v1/site/priorities` | Batch update site priorities. Body: array |
|
||||
| GET | `/api/v1/site/cookie/{site_id}` | Update site cookie & UA. Params: `username`, `password`, `code` |
|
||||
| POST | `/api/v1/site/userdata/{site_id}` | Refresh site user data |
|
||||
| GET | `/api/v1/site/userdata/{site_id}` | Get site user data. Params: `workdate` |
|
||||
| GET | `/api/v1/site/userdata/latest` | All sites latest user data |
|
||||
| GET | `/api/v1/site/test/{site_id}` | Test site connection |
|
||||
| GET | `/api/v1/site/icon/{site_id}` | Site icon |
|
||||
| GET | `/api/v1/site/category/{site_id}` | Site categories |
|
||||
| GET | `/api/v1/site/resource/{site_id}` | Site resources. Params: `keyword`, `cat`, `page` |
|
||||
| GET | `/api/v1/site/statistic/{site_url}` | Specific site statistics |
|
||||
| GET | `/api/v1/site/statistic` | All site statistics |
|
||||
| GET | `/api/v1/site/rss` | RSS subscription sites |
|
||||
| GET | `/api/v1/site/auth` | Check authenticated sites |
|
||||
| POST | `/api/v1/site/auth` | Authenticate a site. Body: SiteAuth |
|
||||
| GET | `/api/v1/site/mapping` | Site domain-to-name mapping |
|
||||
| GET | `/api/v1/site/supporting` | Supported site list |
|
||||
|
||||
### History (5 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/history/download` | Download history. Params: `page`, `count` |
|
||||
| DELETE | `/api/v1/history/download` | Delete download history. Body: DownloadHistory JSON |
|
||||
| GET | `/api/v1/history/transfer` | Transfer history. 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 |
|
||||
|
||||
### Media Server (8 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/mediaserver/play/{itemid}` | Play media online |
|
||||
| GET | `/api/v1/mediaserver/exists` | Check if media exists in library. Params: `title`, `year`, `mtype`, `tmdbid`, `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` |
|
||||
| GET | `/api/v1/mediaserver/playing` | Currently playing. Params: `server` (required), `count` |
|
||||
| GET | `/api/v1/mediaserver/library` | Library list. Params: `server` (required), `hidden` |
|
||||
| GET | `/api/v1/mediaserver/clients` | Available media servers |
|
||||
|
||||
### Storage / Files (13 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/v1/storage/list` | List directory contents. Params: `sort`. Body: FileItem JSON |
|
||||
| POST | `/api/v1/storage/mkdir` | Create directory. Params: `name` (required). Body: FileItem |
|
||||
| POST | `/api/v1/storage/delete` | Delete file or directory. Body: FileItem JSON |
|
||||
| POST | `/api/v1/storage/download` | Download file. Body: FileItem JSON |
|
||||
| POST | `/api/v1/storage/image` | Preview image. Body: FileItem JSON |
|
||||
| POST | `/api/v1/storage/rename` | Rename file/dir. Params: `new_name` (required), `recursive`. Body: FileItem |
|
||||
| GET | `/api/v1/storage/usage/{name}` | Storage usage info |
|
||||
| GET | `/api/v1/storage/transtype/{name}` | Supported transfer types |
|
||||
| GET | `/api/v1/storage/qrcode/{name}` | Generate QR code for auth |
|
||||
| GET | `/api/v1/storage/auth_url/{name}` | Get OAuth2 auth URL |
|
||||
| GET | `/api/v1/storage/check/{name}` | Confirm QR login. Params: `ck`, `t` |
|
||||
| POST | `/api/v1/storage/save/{name}` | Save storage config. Body: JSON object |
|
||||
| GET | `/api/v1/storage/reset/{name}` | Reset storage config |
|
||||
|
||||
### Transfer (5 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/transfer/name` | Preview transfer name. Params: `path` (required), `filetype` (required) |
|
||||
| GET | `/api/v1/transfer/queue` | Transfer queue |
|
||||
| DELETE | `/api/v1/transfer/queue` | Remove from transfer queue. Body: FileItem JSON |
|
||||
| POST | `/api/v1/transfer/manual` | Manual transfer. Params: `background`. Body: ManualTransferItem JSON |
|
||||
| GET | `/api/v1/transfer/now` | Run immediate transfer |
|
||||
|
||||
### Dashboard (16 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/dashboard/statistic` | Media statistics. Params: `name` |
|
||||
| GET | `/api/v1/dashboard/statistic2` | Media statistics (API_TOKEN, use `--token-param`) |
|
||||
| GET | `/api/v1/dashboard/storage` | Local storage space |
|
||||
| GET | `/api/v1/dashboard/storage2` | Local storage space (API_TOKEN) |
|
||||
| GET | `/api/v1/dashboard/processes` | Process info |
|
||||
| GET | `/api/v1/dashboard/downloader` | Downloader info. Params: `name` |
|
||||
| GET | `/api/v1/dashboard/downloader2` | Downloader info (API_TOKEN) |
|
||||
| GET | `/api/v1/dashboard/schedule` | Scheduled services |
|
||||
| GET | `/api/v1/dashboard/schedule2` | Scheduled services (API_TOKEN) |
|
||||
| GET | `/api/v1/dashboard/transfer` | Transfer statistics. Params: `days` |
|
||||
| GET | `/api/v1/dashboard/cpu` | CPU usage |
|
||||
| GET | `/api/v1/dashboard/cpu2` | CPU usage (API_TOKEN) |
|
||||
| GET | `/api/v1/dashboard/memory` | Memory usage |
|
||||
| GET | `/api/v1/dashboard/memory2` | Memory usage (API_TOKEN) |
|
||||
| GET | `/api/v1/dashboard/network` | Network traffic |
|
||||
| GET | `/api/v1/dashboard/network2` | Network traffic (API_TOKEN) |
|
||||
|
||||
### Plugin (22 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/plugin/` | List plugins. Params: `state` (installed/market/all), `force` |
|
||||
| GET | `/api/v1/plugin/installed` | List installed plugins |
|
||||
| GET | `/api/v1/plugin/statistic` | Plugin install statistics |
|
||||
| GET | `/api/v1/plugin/install/{plugin_id}` | Install plugin. Params: `repo_url`, `force` |
|
||||
| GET | `/api/v1/plugin/reload/{plugin_id}` | Reload plugin |
|
||||
| GET | `/api/v1/plugin/reset/{plugin_id}` | Reset plugin config & data |
|
||||
| GET | `/api/v1/plugin/{plugin_id}` | Get plugin config |
|
||||
| PUT | `/api/v1/plugin/{plugin_id}` | Update plugin config. Body: JSON object |
|
||||
| DELETE | `/api/v1/plugin/{plugin_id}` | Uninstall plugin |
|
||||
| POST | `/api/v1/plugin/clone/{plugin_id}` | Clone plugin. Body: JSON object |
|
||||
| GET | `/api/v1/plugin/form/{plugin_id}` | Plugin form page |
|
||||
| GET | `/api/v1/plugin/page/{plugin_id}` | Plugin data page |
|
||||
| GET | `/api/v1/plugin/remotes` | Plugin federation list. Params: `token` (required) |
|
||||
| GET | `/api/v1/plugin/dashboard/meta` | All plugin dashboard metadata |
|
||||
| GET | `/api/v1/plugin/dashboard/{plugin_id}/{key}` | Plugin dashboard by key |
|
||||
| GET | `/api/v1/plugin/dashboard/{plugin_id}` | Plugin dashboard |
|
||||
| GET | `/api/v1/plugin/file/{plugin_id}/{filepath}` | Plugin static file |
|
||||
| GET | `/api/v1/plugin/folders` | Plugin folder config |
|
||||
| POST | `/api/v1/plugin/folders` | Save plugin folder config |
|
||||
| POST | `/api/v1/plugin/folders/{folder_name}` | Create plugin folder |
|
||||
| DELETE | `/api/v1/plugin/folders/{folder_name}` | Delete plugin folder |
|
||||
| PUT | `/api/v1/plugin/folders/{folder_name}/plugins` | Update folder plugins. Body: array |
|
||||
|
||||
### Workflow (16 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/workflow/` | List all workflows |
|
||||
| POST | `/api/v1/workflow/` | Create workflow. Body: Workflow JSON |
|
||||
| GET | `/api/v1/workflow/{workflow_id}` | Workflow detail |
|
||||
| PUT | `/api/v1/workflow/{workflow_id}` | Update workflow. Body: Workflow JSON |
|
||||
| DELETE | `/api/v1/workflow/{workflow_id}` | Delete workflow |
|
||||
| POST | `/api/v1/workflow/{workflow_id}/run` | Run workflow. Params: `from_begin` |
|
||||
| POST | `/api/v1/workflow/{workflow_id}/start` | Enable workflow |
|
||||
| POST | `/api/v1/workflow/{workflow_id}/pause` | Disable workflow |
|
||||
| POST | `/api/v1/workflow/{workflow_id}/reset` | Reset workflow |
|
||||
| GET | `/api/v1/workflow/actions` | List all actions |
|
||||
| GET | `/api/v1/workflow/plugin/actions` | Plugin actions. Params: `plugin_id` |
|
||||
| GET | `/api/v1/workflow/event_types` | List event types |
|
||||
| POST | `/api/v1/workflow/share` | Share workflow. Body: WorkflowShare JSON |
|
||||
| DELETE | `/api/v1/workflow/share/{share_id}` | Delete shared workflow |
|
||||
| POST | `/api/v1/workflow/fork` | Fork shared workflow. Body: WorkflowShare JSON |
|
||||
| GET | `/api/v1/workflow/shares` | List shared workflows. Params: `name`, `page`, `count` |
|
||||
|
||||
### System (20 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/system/env` | Get system configuration |
|
||||
| POST | `/api/v1/system/env` | Update system configuration. Body: JSON object |
|
||||
| GET | `/api/v1/system/setting/{key}` | Get system setting |
|
||||
| POST | `/api/v1/system/setting/{key}` | Update system setting |
|
||||
| GET | `/api/v1/system/global` | Non-sensitive settings. Params: `token` (required) |
|
||||
| GET | `/api/v1/system/global/user` | User-related settings |
|
||||
| GET | `/api/v1/system/restart` | Restart system |
|
||||
| GET | `/api/v1/system/runscheduler` | Run scheduled service. Params: `jobid` (required) |
|
||||
| GET | `/api/v1/system/runscheduler2` | Run scheduler (API_TOKEN, use `--token-param`). Params: `jobid` |
|
||||
| GET | `/api/v1/system/modulelist` | List loaded modules |
|
||||
| GET | `/api/v1/system/moduletest/{moduleid}` | Test module availability |
|
||||
| GET | `/api/v1/system/versions` | List all GitHub releases |
|
||||
| GET | `/api/v1/system/ruletest` | Test filter rule. Params: `title` (required), `rulegroup_name` (required), `subtitle` |
|
||||
| GET | `/api/v1/system/nettest` | Test network connectivity. Params: `url` (required), `proxy` (required), `include` |
|
||||
| GET | `/api/v1/system/llm-models` | List LLM models. Params: `provider` (required), `api_key` (required), `base_url` |
|
||||
| GET | `/api/v1/system/progress/{process_type}` | Real-time progress (SSE) |
|
||||
| GET | `/api/v1/system/message` | Real-time messages (SSE). Params: `role` |
|
||||
| GET | `/api/v1/system/logging` | Real-time logs (SSE). Params: `length`, `logfile` |
|
||||
| GET | `/api/v1/system/img/{proxy}` | Image proxy. Params: `imgurl` (required), `cache`, `use_cookies` |
|
||||
| GET | `/api/v1/system/cache/image` | Cached image. Params: `url` (required) |
|
||||
|
||||
### Discover (6 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/discover/source` | Discover data sources |
|
||||
| GET | `/api/v1/discover/bangumi` | Discover Bangumi. Params: `type`, `cat`, `sort`, `year`, `page`, `count` |
|
||||
| GET | `/api/v1/discover/douban_movies` | Discover Douban movies. Params: `sort`, `tags`, `page`, `count` |
|
||||
| GET | `/api/v1/discover/douban_tvs` | Discover Douban TV. Params: `sort`, `tags`, `page`, `count` |
|
||||
| GET | `/api/v1/discover/tmdb_movies` | Discover TMDB movies. Params: `sort_by`, `with_genres`, `with_original_language`, `page` |
|
||||
| GET | `/api/v1/discover/tmdb_tvs` | Discover TMDB TV. Params: same as movies |
|
||||
|
||||
### Recommend (14 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/recommend/source` | Recommendation data sources |
|
||||
| GET | `/api/v1/recommend/bangumi_calendar` | Bangumi daily schedule. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_showing` | Douban now showing. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_movies` | Douban movies. Params: `sort`, `tags`, `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_tvs` | Douban TV. Params: `sort`, `tags`, `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_movie_top250` | Douban Top 250 movies. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_tv_weekly_chinese` | Douban Chinese TV weekly. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_tv_weekly_global` | Douban Global TV weekly. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_tv_animation` | Douban animation. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_movie_hot` | Douban hot movies. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/douban_tv_hot` | Douban hot TV. Params: `page`, `count` |
|
||||
| GET | `/api/v1/recommend/tmdb_movies` | TMDB movies. Params: `sort_by`, `with_genres`, `page` |
|
||||
| GET | `/api/v1/recommend/tmdb_tvs` | TMDB TV. Params: `sort_by`, `with_genres`, `page` |
|
||||
| GET | `/api/v1/recommend/tmdb_trending` | TMDB trending. Params: `page` |
|
||||
|
||||
### Torrent Cache (5 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/torrent/cache` | Get torrent cache |
|
||||
| DELETE | `/api/v1/torrent/cache` | Clear torrent cache |
|
||||
| DELETE | `/api/v1/torrent/cache/{domain}/{torrent_hash}` | Delete specific torrent cache |
|
||||
| POST | `/api/v1/torrent/cache/refresh` | Refresh torrent cache |
|
||||
| POST | `/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}` | Re-identify torrent. Params: `tmdbid`, `doubanid` |
|
||||
|
||||
### Message (6 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/v1/message/` | Receive user message. Params: `token`, `source` |
|
||||
| GET | `/api/v1/message/` | Callback verification. Params: `token`, `echostr`, `msg_signature`, `timestamp`, `nonce`, `source` |
|
||||
| POST | `/api/v1/message/web` | Send web message. Params: `text` (required) |
|
||||
| GET | `/api/v1/message/web` | Get web messages. Params: `page`, `count` |
|
||||
| POST | `/api/v1/message/webpush/subscribe` | WebPush subscribe. Body: Subscription JSON |
|
||||
| POST | `/api/v1/message/webpush/send` | Send WebPush notification. Body: SubscriptionMessage JSON |
|
||||
|
||||
### User (10 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/user/` | List all users |
|
||||
| POST | `/api/v1/user/` | Create user. Body: UserCreate JSON |
|
||||
| PUT | `/api/v1/user/` | Update user. Body: UserUpdate JSON |
|
||||
| GET | `/api/v1/user/current` | Current logged-in user |
|
||||
| GET | `/api/v1/user/{username}` | User detail |
|
||||
| DELETE | `/api/v1/user/id/{user_id}` | Delete user by ID |
|
||||
| DELETE | `/api/v1/user/name/{user_name}` | Delete user by username |
|
||||
| POST | `/api/v1/user/avatar/{user_id}` | Upload avatar. Body: multipart/form-data |
|
||||
| GET | `/api/v1/user/config/{key}` | Get user config |
|
||||
| POST | `/api/v1/user/config/{key}` | Update user config |
|
||||
|
||||
### Login (3 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/v1/login/access-token` | Get JWT access token. Body: form (username, password) |
|
||||
| GET | `/api/v1/login/wallpaper` | Login page wallpaper |
|
||||
| GET | `/api/v1/login/wallpapers` | Login page wallpaper list |
|
||||
|
||||
### MCP Tools (6 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/v1/mcp` | MCP JSON-RPC 2.0 endpoint |
|
||||
| DELETE | `/api/v1/mcp` | Terminate MCP session |
|
||||
| GET | `/api/v1/mcp/tools` | List all exposed tools |
|
||||
| POST | `/api/v1/mcp/tools/call` | Call a tool. Body: `{"tool_name":"...","arguments":{...}}` |
|
||||
| GET | `/api/v1/mcp/tools/{tool_name}` | Get tool definition |
|
||||
| GET | `/api/v1/mcp/tools/{tool_name}/schema` | Get tool input schema |
|
||||
|
||||
### Webhook (2 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/webhook/` | Webhook message (GET). Params: `token`, `source` |
|
||||
| POST | `/api/v1/webhook/` | Webhook message (POST). Params: `token`, `source` |
|
||||
|
||||
### Servarr Compatibility -- /api/v3 (16 endpoints)
|
||||
|
||||
Radarr/Sonarr compatible API for integration with external tools.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v3/system/status` | System status |
|
||||
| GET | `/api/v3/qualityProfile` | Quality profiles |
|
||||
| GET | `/api/v3/rootfolder` | Root folders |
|
||||
| GET | `/api/v3/tag` | Tags |
|
||||
| GET | `/api/v3/languageprofile` | Languages |
|
||||
| GET | `/api/v3/movie` | All subscribed movies |
|
||||
| POST | `/api/v3/movie` | Add movie subscription. Body: RadarrMovie JSON |
|
||||
| GET | `/api/v3/movie/lookup` | Search movie. Params: `term` (format: `tmdb:123`) |
|
||||
| GET | `/api/v3/movie/{mid}` | Movie detail |
|
||||
| DELETE | `/api/v3/movie/{mid}` | Delete movie subscription |
|
||||
| GET | `/api/v3/series` | All TV series |
|
||||
| POST | `/api/v3/series` | Add TV subscription. Body: SonarrSeries JSON |
|
||||
| PUT | `/api/v3/series` | Update TV subscription. Body: SonarrSeries JSON |
|
||||
| GET | `/api/v3/series/lookup` | Search TV. Params: `term` (format: `tvdb:123`) |
|
||||
| GET | `/api/v3/series/{tid}` | TV detail |
|
||||
| DELETE | `/api/v3/series/{tid}` | Delete TV subscription |
|
||||
|
||||
### CookieCloud -- /cookiecloud (5 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/cookiecloud/` | Root |
|
||||
| POST | `/cookiecloud/` | Root |
|
||||
| POST | `/cookiecloud/update` | Upload cookie data. Body: CookieData JSON |
|
||||
| GET | `/cookiecloud/get/{uuid}` | Download encrypted data |
|
||||
| POST | `/cookiecloud/get/{uuid}` | Download encrypted data (POST) |
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Search and download a movie
|
||||
|
||||
```bash
|
||||
# 1. Search TMDB for the movie
|
||||
python scripts/mp-api.py GET /api/v1/media/search title="Inception" type="movie"
|
||||
|
||||
# 2. Get media detail (replace {tmdbid} with actual ID)
|
||||
python scripts/mp-api.py GET /api/v1/media/27205 type_name="movie"
|
||||
|
||||
# 3. Search torrents
|
||||
python scripts/mp-api.py GET /api/v1/search/media/tmdb:27205 mtype="movie"
|
||||
|
||||
# 4. Get latest search results
|
||||
python scripts/mp-api.py GET /api/v1/search/last
|
||||
|
||||
# 5. Add download
|
||||
python scripts/mp-api.py POST /api/v1/download/add --json '{"torrent_url":"<url_from_search>"}'
|
||||
```
|
||||
|
||||
### Add a subscription
|
||||
|
||||
```bash
|
||||
# 1. Search for the show
|
||||
python scripts/mp-api.py GET /api/v1/media/search title="Breaking Bad" type="tv"
|
||||
|
||||
# 2. Check if already subscribed
|
||||
python scripts/mp-api.py GET /api/v1/subscribe/media/tmdb:1396
|
||||
|
||||
# 3. Check if already in library
|
||||
python scripts/mp-api.py GET /api/v1/mediaserver/exists tmdbid=1396 mtype="tv"
|
||||
|
||||
# 4. Add subscription
|
||||
python scripts/mp-api.py POST /api/v1/subscribe/ --json '{"name":"Breaking Bad","year":"2008","type":"tv","tmdbid":1396}'
|
||||
```
|
||||
|
||||
### System monitoring
|
||||
|
||||
```bash
|
||||
# CPU, memory, network
|
||||
python scripts/mp-api.py GET /api/v1/dashboard/cpu
|
||||
python scripts/mp-api.py GET /api/v1/dashboard/memory
|
||||
python scripts/mp-api.py GET /api/v1/dashboard/network
|
||||
|
||||
# Storage
|
||||
python scripts/mp-api.py GET /api/v1/dashboard/storage
|
||||
|
||||
# Active downloads
|
||||
python scripts/mp-api.py GET /api/v1/download/
|
||||
|
||||
# Run a scheduled task
|
||||
python scripts/mp-api.py GET /api/v1/system/runscheduler jobid="subscribe_search_all"
|
||||
```
|
||||
|
||||
### Site management
|
||||
|
||||
```bash
|
||||
# List all sites
|
||||
python scripts/mp-api.py GET /api/v1/site/
|
||||
|
||||
# Test site connectivity
|
||||
python scripts/mp-api.py GET /api/v1/site/test/1
|
||||
|
||||
# Get site user data
|
||||
python scripts/mp-api.py GET /api/v1/site/userdata/1
|
||||
|
||||
# Sync CookieCloud
|
||||
python scripts/mp-api.py GET /api/v1/site/cookiecloud
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| HTTP 401 | API key is invalid or missing. Re-run `configure` with correct `--apikey`. |
|
||||
| HTTP 403 | Insufficient permissions. The API key grants superuser access; check if the endpoint requires special auth. |
|
||||
| HTTP 404 | Endpoint or resource not found. Verify the path and path parameters. |
|
||||
| HTTP 422 | Validation error. Check required parameters and JSON body format. |
|
||||
| Connection error | Verify `--host` URL is reachable. Check if MoviePilot is running. |
|
||||
| Missing config | Run `python scripts/mp-api.py configure --host <HOST> --apikey <KEY>` first. |
|
||||
336
skills/moviepilot-api/scripts/mp-api.py
Normal file
336
skills/moviepilot-api/scripts/mp-api.py
Normal file
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MoviePilot REST API CLI -- a lightweight command-line client for calling
|
||||
any MoviePilot API endpoint directly.
|
||||
|
||||
Usage:
|
||||
python mp-api.py configure --host <HOST> --apikey <KEY>
|
||||
python mp-api.py GET /api/v1/media/search title="Avatar" type="movie"
|
||||
python mp-api.py POST /api/v1/download/add --json '{"torrent_url":"..."}'
|
||||
python mp-api.py DELETE /api/v1/subscribe/123
|
||||
|
||||
Authentication:
|
||||
The script sends the API key via the ``X-API-KEY`` header.
|
||||
It can also fall back to ``?token=`` for endpoints that require it.
|
||||
|
||||
Configuration priority:
|
||||
CLI flags > Environment variables (MP_HOST / MP_API_KEY) > Config file
|
||||
|
||||
Config file location: ~/.config/moviepilot_api/config
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_NAME = os.path.basename(sys.argv[0]) if sys.argv else "mp-api.py"
|
||||
CONFIG_DIR = Path.home() / ".config" / "moviepilot_api"
|
||||
CONFIG_FILE = CONFIG_DIR / "config"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def read_config() -> tuple[str, str]:
|
||||
"""Return (host, apikey) from the config file."""
|
||||
host = ""
|
||||
apikey = ""
|
||||
if not CONFIG_FILE.exists():
|
||||
return host, apikey
|
||||
for line in CONFIG_FILE.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key == "MP_HOST":
|
||||
host = value
|
||||
elif key == "MP_API_KEY":
|
||||
apikey = value
|
||||
return host, apikey
|
||||
|
||||
|
||||
def save_config(host: str, apikey: str) -> None:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CONFIG_FILE.write_text(f"MP_HOST={host}\nMP_API_KEY={apikey}\n", encoding="utf-8")
|
||||
CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
|
||||
def resolve_config(
|
||||
cli_host: str = "",
|
||||
cli_key: str = "",
|
||||
) -> tuple[str, str]:
|
||||
"""Resolve effective host & key using priority: CLI > env > file."""
|
||||
cfg_host, cfg_key = read_config()
|
||||
host = cli_host or os.environ.get("MP_HOST", "") or cfg_host
|
||||
apikey = cli_key or os.environ.get("MP_API_KEY", "") or cfg_key
|
||||
return host, apikey
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Allow self-signed certs (common in home-lab setups)
|
||||
_SSL_CTX = ssl.create_default_context()
|
||||
_SSL_CTX.check_hostname = False
|
||||
_SSL_CTX.verify_mode = ssl.CERT_NONE
|
||||
|
||||
|
||||
def http_request(
|
||||
method: str,
|
||||
url: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
body: bytes | None = None,
|
||||
timeout: int = 120,
|
||||
) -> tuple[int, str]:
|
||||
"""Perform an HTTP request and return (status_code, response_body)."""
|
||||
headers = headers or {}
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=_SSL_CTX) as resp:
|
||||
return resp.status, resp.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, exc.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.URLError as exc:
|
||||
return 0, f"Connection error: {exc.reason}"
|
||||
|
||||
|
||||
def build_url(host: str, path: str, query_params: dict[str, str] | None = None) -> str:
|
||||
"""Build a full URL from host + path + optional query parameters."""
|
||||
base = host.rstrip("/")
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
url = base + path
|
||||
if query_params:
|
||||
url += "?" + urllib.parse.urlencode(query_params)
|
||||
return url
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core API call
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def api_call(
|
||||
host: str,
|
||||
apikey: str,
|
||||
method: str,
|
||||
path: str,
|
||||
query_params: dict[str, str] | None = None,
|
||||
json_body: object | None = None,
|
||||
use_token_param: bool = False,
|
||||
timeout: int = 120,
|
||||
) -> tuple[int, object]:
|
||||
"""
|
||||
Call a MoviePilot REST API endpoint.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
host : str
|
||||
MoviePilot base URL (e.g. ``http://localhost:3000``).
|
||||
apikey : str
|
||||
The API key (``settings.API_TOKEN`` value).
|
||||
method : str
|
||||
HTTP method: GET, POST, PUT, DELETE.
|
||||
path : str
|
||||
API path (e.g. ``/api/v1/media/search``).
|
||||
query_params : dict, optional
|
||||
Additional query-string parameters.
|
||||
json_body : object, optional
|
||||
A JSON-serialisable body for POST/PUT requests.
|
||||
use_token_param : bool
|
||||
If True, send the key as ``?token=`` instead of the header.
|
||||
timeout : int
|
||||
Request timeout in seconds.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(status_code, parsed_json_or_text)
|
||||
"""
|
||||
headers: dict[str, str] = {}
|
||||
qp = dict(query_params or {})
|
||||
|
||||
if use_token_param:
|
||||
qp["token"] = apikey
|
||||
else:
|
||||
headers["X-API-KEY"] = apikey
|
||||
|
||||
body_bytes: bytes | None = None
|
||||
if json_body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
body_bytes = json.dumps(json_body, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
url = build_url(host, path, qp if qp else None)
|
||||
status, raw = http_request(method, url, headers, body_bytes, timeout)
|
||||
|
||||
# Try to parse JSON
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
data = raw
|
||||
return status, data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def print_json(obj: object) -> None:
|
||||
"""Pretty-print a JSON-serialisable object to stdout."""
|
||||
if isinstance(obj, str):
|
||||
print(obj)
|
||||
else:
|
||||
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
def print_usage() -> None:
|
||||
print(f"""Usage: python {SCRIPT_NAME} [options] <METHOD> <PATH> [key=value ...] [--json '<body>']
|
||||
python {SCRIPT_NAME} configure --host <HOST> --apikey <KEY>
|
||||
|
||||
Options:
|
||||
--host HOST MoviePilot backend URL
|
||||
--apikey KEY API key (API_TOKEN)
|
||||
--token-param Send key as ?token= query param instead of X-API-KEY header
|
||||
--timeout SECS Request timeout (default: 120)
|
||||
--help Show this help message
|
||||
|
||||
Methods: GET POST PUT DELETE
|
||||
|
||||
Examples:
|
||||
python {SCRIPT_NAME} configure --host http://localhost:3000 --apikey mytoken123
|
||||
|
||||
python {SCRIPT_NAME} GET /api/v1/media/search title="Avatar" type="movie"
|
||||
python {SCRIPT_NAME} GET /api/v1/subscribe/
|
||||
python {SCRIPT_NAME} POST /api/v1/download/add --json '{{"torrent_url":"abc:1"}}'
|
||||
python {SCRIPT_NAME} DELETE /api/v1/subscribe/123
|
||||
python {SCRIPT_NAME} GET /api/v1/dashboard/statistic2 --token-param
|
||||
""")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
argv = sys.argv[1:]
|
||||
if not argv or "--help" in argv or "-h" in argv:
|
||||
print_usage()
|
||||
sys.exit(0)
|
||||
|
||||
# Parse options
|
||||
cli_host = ""
|
||||
cli_key = ""
|
||||
use_token_param = False
|
||||
timeout = 120
|
||||
positional: list[str] = []
|
||||
json_body_str: str | None = None
|
||||
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "--host":
|
||||
i += 1
|
||||
cli_host = argv[i] if i < len(argv) else ""
|
||||
elif arg == "--apikey":
|
||||
i += 1
|
||||
cli_key = argv[i] if i < len(argv) else ""
|
||||
elif arg == "--token-param":
|
||||
use_token_param = True
|
||||
elif arg == "--timeout":
|
||||
i += 1
|
||||
timeout = int(argv[i]) if i < len(argv) else 120
|
||||
elif arg == "--json":
|
||||
i += 1
|
||||
json_body_str = argv[i] if i < len(argv) else "{}"
|
||||
else:
|
||||
positional.append(arg)
|
||||
i += 1
|
||||
|
||||
# Sub-command: configure
|
||||
if positional and positional[0].lower() == "configure":
|
||||
if not cli_host and not cli_key:
|
||||
print(
|
||||
"Error: --host and --apikey are required for configure", file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
cfg_host, cfg_key = read_config()
|
||||
save_config(cli_host or cfg_host, cli_key or cfg_key)
|
||||
print("Configuration saved.")
|
||||
sys.exit(0)
|
||||
|
||||
# Normal API call
|
||||
if len(positional) < 2:
|
||||
print("Error: expected <METHOD> <PATH>", file=sys.stderr)
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
method = positional[0].upper()
|
||||
path = positional[1]
|
||||
|
||||
# Remaining positional args are key=value query params
|
||||
query_params: dict[str, str] = {}
|
||||
for kv in positional[2:]:
|
||||
if "=" in kv:
|
||||
k, _, v = kv.partition("=")
|
||||
query_params[k] = v
|
||||
else:
|
||||
print(f"Warning: ignoring argument without '=': {kv}", file=sys.stderr)
|
||||
|
||||
# Parse JSON body
|
||||
json_body = None
|
||||
if json_body_str:
|
||||
try:
|
||||
json_body = json.loads(json_body_str)
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"Error: invalid JSON body: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Resolve config
|
||||
host, apikey = resolve_config(cli_host, cli_key)
|
||||
if not host:
|
||||
print("Error: backend host is not configured.", file=sys.stderr)
|
||||
print(" Use: --host HOST or set MP_HOST environment variable", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not apikey:
|
||||
print("Error: API key is not configured.", file=sys.stderr)
|
||||
print(
|
||||
" Use: --apikey KEY or set MP_API_KEY environment variable",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Persist if CLI flags provided
|
||||
if cli_host or cli_key:
|
||||
save_config(host, apikey)
|
||||
|
||||
status, data = api_call(
|
||||
host=host,
|
||||
apikey=apikey,
|
||||
method=method,
|
||||
path=path,
|
||||
query_params=query_params if query_params else None,
|
||||
json_body=json_body,
|
||||
use_token_param=use_token_param,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if status and status not in (200, 201):
|
||||
print(f"HTTP {status}", file=sys.stderr)
|
||||
|
||||
print_json(data)
|
||||
if status and status >= 400:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user