diff --git a/docs/api/index.md b/docs/api/index.md index 5b622fc5..2515fbcf 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,619 +1,221 @@ # REST API 参考 -AutoBangumi 在 `/api/v1` 路径下提供 REST API。除登录和设置向导外,所有端点都需要 JWT 认证。 +AutoBangumi 在 `/api/v1` 下提供 REST API。WebUI 使用的也是这套接口。 **基础 URL:** `http://your-host:7892/api/v1` -**认证:** 将 JWT 令牌作为 cookie 或 `Authorization: Bearer ` 请求头传入。 - -**交互式文档:** 在开发模式下运行时,可在 `http://your-host:7892/docs` 访问 Swagger UI。 - ---- +**交互式文档:** 本页只列出端点和用途。请求与响应的字段以运行中实例的 `http://your-host:7892/docs`(Swagger UI,由 FastAPI 根据代码生成)为准。插件路由不出现在 `/docs` 中。 ## 认证 -### 登录 +AB 接受两种凭据: -``` -POST /auth/login +- **浏览器会话**:`POST /auth/login` 以表单字段 `username`、`password` 登录,成功后 AB 设置 HttpOnly Cookie `token`。WebUI 使用这种方式。 +- **API 令牌**:在 设置 → 用户与访问控制 → API 令牌 中创建 `scope=api` 的令牌(或调用 `POST /tokens`),明文只显示一次。请求时带上 `Authorization: Bearer <令牌>`。 + +```bash +curl -H "Authorization: Bearer $AB_TOKEN" http://your-host:7892/api/v1/status ``` -使用用户名和密码进行认证。 +- 请求带 `Authorization` 头时,AB 只校验该令牌,不再看 Cookie。 +- 未认证或凭据无效返回 `401`。 +- 账户管理端点(`/auth/update`、`/users`、`/tokens`、Passkey 的注册与管理)只接受浏览器会话,用 API 令牌访问返回 `403`。 +- 以下端点不需要认证:`/auth/login`、`/passkey/auth/*`、设置向导 `/setup/*`(`GET /setup/status` 始终可用,其它端点在设置完成后返回 `403`)和根路径的 `/health`。 +- `/auth/refresh_token` 与 `/auth/logout` 不看 `Authorization` 头,只使用会话 Cookie。`refresh_token` 没有有效 Cookie 时返回 `401`。登录与 Passkey 登录受 `security.login_whitelist` 限制。 +- 本地开发时,设置环境变量 `AB_DEV_NO_AUTH=1` 可跳过全部认证。不要在生产环境中设置它。 -**请求体:** -```json -{ - "username": "string", - "password": "string" -} -``` +## 认证与账户 -**响应:** 设置包含 JWT 令牌的认证 cookie。 +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `POST` | `/auth/login` | 用户名和密码登录,设置会话 Cookie | +| `POST` | `/auth/refresh_token` | 延长当前会话 | +| `POST` | `/auth/logout` | 注销当前会话并清除 Cookie | +| `GET` | `/auth/me` | 当前用户 | +| `POST` | `/auth/update` | 修改当前账户,并轮换它的全部会话 | +| `GET` / `POST` | `/users` | 列出 / 创建用户 | +| `PATCH` / `DELETE` | `/users/{user_id}` | 修改 / 删除用户 | +| `GET` / `POST` | `/tokens` | 列出 / 创建 API 令牌(`scope` 为 `api` 或 `mcp`,可设 `expires_at`) | +| `DELETE` | `/tokens/{token_id}` | 吊销令牌 | +| `POST` | `/passkey/register/options`、`/passkey/register/verify` | 注册 Passkey | +| `POST` | `/passkey/auth/options`、`/passkey/auth/verify` | 用 Passkey 登录 | +| `GET` | `/passkey/list` | 列出当前用户的 Passkey | +| `POST` | `/passkey/delete` | 删除 Passkey | -### 刷新令牌 +## 程序 -``` -GET /auth/refresh_token -``` - -刷新当前的认证令牌。 - -### 登出 - -``` -GET /auth/logout -``` - -清除认证 cookie 并登出。 - -### 更新凭据 - -``` -POST /auth/update -``` - -更新用户名和/或密码。 - -**请求体:** -```json -{ - "username": "string", - "password": "string" -} -``` - ---- - -## Passkey / WebAuthn - -使用 WebAuthn/FIDO2 Passkey 进行无密码认证。 - -### 注册 Passkey - -``` -POST /passkey/register/options -``` - -获取 WebAuthn 注册选项(质询、依赖方信息)。 - -``` -POST /passkey/register/verify -``` - -验证并保存来自浏览器的 Passkey 注册响应。 - -### 使用 Passkey 认证 - -``` -POST /passkey/auth/options -``` - -获取 WebAuthn 认证质询选项。 - -``` -POST /passkey/auth/verify -``` - -验证 Passkey 认证响应并签发 JWT 令牌。 - -### 管理 Passkey - -``` -GET /passkey/list -``` - -列出当前用户所有已注册的 Passkey。 - -``` -POST /passkey/delete -``` - -通过凭据 ID 删除已注册的 Passkey。 - ---- +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/status` | 版本、运行状态、是否首次运行 | +| `POST` | `/start`、`/stop`、`/restart` | 启动 / 停止 / 重启后台任务 | +| `POST` | `/shutdown` | 关闭程序 | +| `GET` | `/check/downloader` | 检查默认下载器是否可用 | +| `GET` | `/log` | 读取日志 | +| `POST` | `/log/clear` | 清空日志 | +| `GET` | `/update/check` | 查询最新版本与在线更新状态 | +| `POST` | `/update/apply` | 下载并应用最新更新,成功后重启 | +| `POST` | `/update/rollback` | 回滚到上一个更新版本,没有备份时回到镜像自带版本 | +| `GET` | `/health`(根路径,不在 `/api/v1` 下) | 存活探针,总是返回 `200`:`{"status", "version", "db_ok"}` | ## 配置 -### 获取配置 - -``` -GET /config/get -``` - -获取当前应用程序配置。 - -**响应:** 完整配置对象,包括 `program`、`downloader`、`rss_parser`、`bangumi_manager`、`notification`、`proxy`、`llm` 等部分。 - -### 更新配置 - -``` -PATCH /config/update -``` - -部分更新应用程序配置。只需包含您想要更改的字段。 - -**请求体:** 部分配置对象。 - ---- - -## 番剧(动画规则) - -### 列出所有番剧 - -``` -GET /bangumi/get/all -``` - -获取所有动画下载规则。 - -### 通过 ID 获取番剧 - -``` -GET /bangumi/get/{bangumi_id} -``` - -通过 ID 获取特定动画规则。 - -### 更新番剧 - -``` -PATCH /bangumi/update/{bangumi_id} -``` - -更新动画规则的元数据(标题、季度、集数偏移等)。 - -### 删除番剧 - -``` -DELETE /bangumi/delete/{bangumi_id} -``` - -删除单个动画规则及其关联的种子。 - -``` -DELETE /bangumi/delete/many/ -``` - -批量删除多个动画规则。 - -**请求体:** -```json -{ - "bangumi_ids": [1, 2, 3] -} -``` - -### 禁用/启用番剧 - -``` -DELETE /bangumi/disable/{bangumi_id} -``` - -禁用动画规则(保留文件,停止下载)。 - -``` -DELETE /bangumi/disable/many/ -``` - -批量禁用多个动画规则。 - -``` -GET /bangumi/enable/{bangumi_id} -``` - -重新启用之前禁用的动画规则。 - -### 刷新海报 - -``` -GET /bangumi/refresh/poster/all -``` - -从 TMDB 刷新所有动画的海报图片。 - -``` -GET /bangumi/refresh/poster/{bangumi_id} -``` - -刷新特定动画的海报图片。 - -### 日历 - -``` -GET /bangumi/refresh/calendar -``` - -从 Bangumi.tv 刷新动画放送日历数据。 - -### 重置全部 - -``` -GET /bangumi/reset/all -``` - -删除所有动画规则。请谨慎使用。 - ---- - -## RSS 订阅源 - -### 列出所有订阅源 - -``` -GET /rss -``` - -获取所有已配置的 RSS 订阅源。 - -### 添加订阅源 - -``` -POST /rss/add -``` - -添加新的 RSS 订阅源。 - -**请求体:** -```json -{ - "url": "string", - "aggregate": true, - "parser": "mikan" -} -``` - -### 启用/禁用订阅源 - -``` -POST /rss/enable/many -``` - -启用多个 RSS 订阅源。 - -``` -PATCH /rss/disable/{rss_id} -``` - -禁用单个 RSS 订阅源。 - -``` -POST /rss/disable/many -``` - -批量禁用多个 RSS 订阅源。 - -### 删除订阅源 - -``` -DELETE /rss/delete/{rss_id} -``` - -删除单个 RSS 订阅源。 - -``` -POST /rss/delete/many -``` - -批量删除多个 RSS 订阅源。 - -### 更新订阅源 - -``` -PATCH /rss/update/{rss_id} -``` - -更新 RSS 订阅源的配置。 - -### 刷新订阅源 - -``` -GET /rss/refresh/all -``` - -手动触发刷新所有 RSS 订阅源。 - -``` -GET /rss/refresh/{rss_id} -``` - -刷新特定的 RSS 订阅源。 - -### 获取订阅源中的种子 - -``` -GET /rss/torrent/{rss_id} -``` - -获取从特定 RSS 订阅源解析的种子列表。 - -### 分析与订阅 - -``` -POST /rss/analysis -``` - -分析 RSS URL 并提取动画元数据,但不订阅。 - -**请求体:** -```json -{ - "url": "string" -} -``` - -``` -POST /rss/collect -``` - -从 RSS 订阅源下载所有剧集(用于已完结动画)。 - -``` -POST /rss/subscribe -``` - -订阅 RSS 订阅源以自动下载连载中的动画。 - ---- +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/config/get` | 当前配置,秘密字段已掩码 | +| `PATCH` | `/config/update` | 保存并重新加载配置;收到掩码的秘密字段保留原值 | +| `POST` | `/config/llm/models` | 列出所选 LLM 提供商的可用模型 | +| `GET` | `/config/llm/providers` | LLM 提供商列表 | +| `POST` | `/config/llm/providers/{provider_id}/install` | 安装 LLM 提供商插件 | +| `DELETE` | `/config/llm/providers/{provider_id}` | 卸载 LLM 提供商插件 | +| `POST` | `/config/llm/providers/{provider_id}/auth/begin`、`/auth/complete` | 订阅类提供商的授权流程 | +| `GET` | `/config/llm/providers/{provider_id}/auth/status` | 授权状态 | +| `DELETE` | `/config/llm/providers/{provider_id}/auth` | 断开授权 | + +## 番剧 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/bangumi/get/all` | 全部番剧规则 | +| `GET` | `/bangumi/get/{bangumi_id}` | 单个规则 | +| `PATCH` | `/bangumi/update/{bangumi_id}` | 修改规则 | +| `DELETE` | `/bangumi/delete/{bangumi_id}` | 删除规则 | +| `POST` | `/bangumi/delete/many` | 批量删除 | +| `POST` | `/bangumi/disable/{bangumi_id}`、`/bangumi/enable/{bangumi_id}` | 停用 / 启用规则 | +| `POST` | `/bangumi/disable/many` | 批量停用 | +| `PATCH` | `/bangumi/archive/{bangumi_id}`、`/bangumi/unarchive/{bangumi_id}` | 归档 / 取消归档 | +| `PATCH` | `/bangumi/{bangumi_id}/weekday` | 手动设置放送星期 | +| `GET` | `/bangumi/refresh/poster/all`、`/bangumi/refresh/poster/{bangumi_id}` | 刷新海报 | +| `GET` | `/bangumi/refresh/calendar` | 刷新放送日历 | +| `GET` | `/bangumi/refresh/metadata` | 刷新 TMDB 元数据,并自动归档已完结的番剧 | +| `POST` | `/bangumi/reset/all` | 删除全部规则 | +| `GET` | `/bangumi/needs-review` | 需要确认集数偏移的番剧 | +| `GET` | `/bangumi/suggest-offset/{bangumi_id}` | 根据 TMDB 集数建议偏移 | +| `POST` | `/bangumi/detect-offset` | 检测季度 / 集数与 TMDB 不一致 | +| `POST` | `/bangumi/apply-offset/{bangumi_id}`、`/bangumi/apply-offset/many` | 应用建议的偏移并触发一次重命名 | +| `POST` | `/bangumi/dismiss-review/{bangumi_id}` | 清除「待确认」标记 | +| `GET` / `DELETE` | `/bangumi/{bangumi_id}/torrents` | 列出 / 删除该番剧的种子记录 | +| `DELETE` | `/bangumi/{bangumi_id}/torrents/{torrent_id}` | 删除一条种子记录 | +| `GET` / `DELETE` | `/bangumi/torrents/orphans` | 列出 / 删除不属于任何番剧的种子记录 | +| `GET` | `/bangumi/torrents/orphans/count` | 孤立种子记录的数量 | +| `DELETE` | `/bangumi/torrents/orphans/{torrent_id}` | 删除一条孤立种子记录 | + +## 电影 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/movie/get/all`、`/movie/get/{movie_id}` | 全部 / 单个电影规则 | +| `PATCH` | `/movie/update/{movie_id}` | 修改 | +| `DELETE` | `/movie/delete/{movie_id}` | 删除 | +| `DELETE` | `/movie/disable/{movie_id}` | 停用 | +| `GET` | `/movie/enable/{movie_id}` | 启用 | + +## RSS 订阅 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/rss` | 全部订阅 | +| `POST` | `/rss/add` | 添加订阅 | +| `PATCH` | `/rss/update/{rss_id}` | 修改订阅 | +| `DELETE` | `/rss/delete/{rss_id}` | 删除订阅 | +| `POST` | `/rss/delete/many` | 批量删除 | +| `PATCH` | `/rss/disable/{rss_id}` | 停用 | +| `POST` | `/rss/disable/many`、`/rss/enable/many` | 批量停用 / 启用 | +| `POST` | `/rss/refresh/all`、`/rss/refresh/{rss_id}` | 立即刷新 | +| `GET` | `/rss/torrent/{rss_id}` | 该订阅的种子 | +| `POST` | `/rss/analysis` | 解析一个 RSS 链接,返回识别出的番剧 | +| `POST` | `/rss/collect` | 下载整季(收集) | +| `POST` | `/rss/subscribe` | 订阅 | ## 搜索 -### 搜索番剧(Server-Sent Events) +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/search/bangumi?site=<站点>&keywords=<关键词>` | Server-Sent Events,逐条推送搜索结果;多个关键词以空格分隔 | +| `GET` | `/search/provider` | 可用的搜索站点(含插件提供的站点) | +| `GET` / `PUT` | `/search/provider/config` | 读取 / 保存用户配置的搜索站点 | -``` -GET /search/bangumi?keyword={keyword}&provider={provider} +## 下载器 + +AB 4.0 可以配置多个下载器实例(`plugins.instances`)。种子列表合并所有实例,每条带 `downloader_id`。 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/downloader/instances` | 下载器实例:`{"default": <默认实例 id>, "instances": [{"id", "provider"}]}` | +| `GET` | `/downloader/torrents` | 所有实例的种子;不可用的实例跳过 | +| `POST` | `/downloader/torrents/pause`、`/resume`、`/delete` | 暂停 / 恢复 / 删除种子 | +| `POST` | `/downloader/torrents/tag` | 给种子打番剧 id 标签 | +| `POST` | `/downloader/torrents/tag/auto` | 按名称与路径为未打标签的种子自动打标签 | +| `GET` | `/downloader/rename-conflicts` | 等待用户处理的重命名冲突 | +| `POST` | `/downloader/rename-conflicts/{operation_id}/retry` | 清除一条冲突,下一轮重命名时重新检查 | + +## 通知中心 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` / `DELETE` | `/notification/messages` | 列出 / 清空站内通知 | +| `GET` | `/notification/messages/unread-count` | 未读数量 | +| `POST` | `/notification/messages/read-all` | 全部标为已读 | +| `POST` | `/notification/messages/{message_id}/read` | 标为已读 | +| `DELETE` | `/notification/messages/{message_id}` | 删除一条 | +| `POST` | `/notification/test` | 按序号测试已保存的通知渠道 | +| `POST` | `/notification/test-config` | 测试未保存的通知渠道配置 | + +## 事件流 + +`GET /events/stream` 是一条 Server-Sent Events 连接,WebUI 用它代替轮询。 + +| `event` | 何时推送 | `data` | +| --- | --- | --- | +| `status` | 每 3 秒 | 与 `GET /status` 相同的结构 | +| `downloader` | 每 5 秒 | 所有实例的种子;下载器不可用时为 `null` | +| `log` | 每 10 秒 | 日志尾部 | +| `update` | 在线更新进行中且进度变化时 | 更新进度 | +| `notification` | 连接建立时,以及通知中心变化时 | 通知中心状态,含未读数 `unread_count` | +| `bus` | 事件总线上有事件时 | `{"kind": <事件名>, "payload": {...}}`,包括宿主事件和插件事件 | + +```bash +curl -N -H "Authorization: Bearer $AB_TOKEN" http://your-host:7892/api/v1/events/stream ``` -搜索动画种子。以 Server-Sent Events (SSE) 流的形式返回结果,提供实时更新。 +`bus` 帧中的事件名与字段见 [插件开发 → 事件](/dev/plugins/events)。前端插件组件用 `host.events.on(kind, callback)` 订阅同样的帧。 -**查询参数:** -- `keyword` — 搜索关键词 -- `provider` — 搜索提供者(例如 `mikan`、`nyaa`、`dmhy`) +## 插件 -**响应:** 包含解析后搜索结果的 SSE 流。 +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/plugins` | 已发现的插件、状态、配置表单 schema 与当前配置(已掩码) | +| `PUT` | `/plugins/settings` | 修改 `allow_unsigned`(允许未签名插件) | +| `PUT` | `/plugins/{plugin_id}` | 启用 / 停用插件或修改配置,保存后立即生效;配置不合法返回 `422` | +| `GET` | `/plugins/providers` | 插件提供的 Provider id,按扩展点分组 | +| `GET` | `/plugins/ui` | 已启用插件声明的前端挂载点 | +| `GET` | `/plugins/catalog` | 签名目录中可安装的插件;目录不可达返回 `502` | +| `POST` | `/plugins/{plugin_id}/install` | 从签名目录安装或升级插件,并启用 | +| `DELETE` | `/plugins/{plugin_id}` | 卸载经签名目录安装的插件 | +| `GET` | `/plugins/{plugin_id}/web/{path}` | 插件 `web/` 目录下的前端静态文件 | +| 任意 | `/plugins/{plugin_id}/{path}` | 插件自己的路由,见 [REST 路由](/dev/plugins/points/api-router) | -### 列出搜索提供者 +## 设置向导 -``` -GET /search/provider -``` +这些端点不需要认证。`GET /setup/status` 始终可用,返回 `need_setup`(设置完成后为 `false`)。其它端点只在首次设置完成前可用,完成后返回 `403`。`/setup/complete` 还要求浏览器会话,或 `admin` 账户仍为出厂密码 `adminadmin`;否则返回 `403`。 -获取可用搜索提供者的列表。 +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/setup/status` | 是否需要设置向导 | +| `POST` | `/setup/test-downloader` | 测试下载器连接 | +| `POST` | `/setup/test-rss` | 测试 RSS 链接 | +| `POST` | `/setup/test-notification` | 发送测试通知 | +| `POST` | `/setup/complete` | 保存向导中的全部配置,并标记设置完成 | ---- +## MCP -## 程序控制 +AB 在根路径的 `/mcp` 下提供 MCP 服务(SSE 传输):客户端连接 `GET /mcp/sse`,并向 `POST /mcp/messages/` 发送消息。 -### 获取状态 +- 访问控制与 REST API 分开:客户端 IP 在 `security.mcp_whitelist` 内,或请求带 `scope=mcp` 的令牌(`Authorization: Bearer <令牌>`)。`mcp_whitelist` 为空时拒绝所有基于 IP 的访问,令牌仍然有效。 +- 内置工具:`list_anime`、`get_anime`、`search_anime`、`subscribe_anime`、`unsubscribe_anime`、`list_downloads`、`list_rss_feeds`、`get_program_status`、`refresh_feeds`、`update_anime`。 +- 内置资源:`autobangumi://anime/list`、`autobangumi://anime/{id}`、`autobangumi://status`、`autobangumi://rss/feeds`。 +- 已启用插件提供的工具名为 `<插件 id>__`,资源 URI 为 `autobangumi://plugins/<插件 id>/`,见 [MCP 工具与资源](/dev/plugins/points/mcp)。 -``` -GET /status -``` +## 响应与错误 -获取程序状态,包括版本、运行状态和 first_run 标志。 - -**响应:** -```json -{ - "status": "running", - "version": "3.2.0", - "first_run": false -} -``` - -### 启动程序 - -``` -GET /start -``` - -启动主程序(RSS 检查、下载、重命名)。 - -### 重启程序 - -``` -GET /restart -``` - -重启主程序。 - -### 停止程序 - -``` -GET /stop -``` - -停止主程序(WebUI 仍可访问)。 - -### 关闭 - -``` -GET /shutdown -``` - -关闭整个应用程序(重启 Docker 容器)。 - -### 检查下载器 - -``` -GET /check/downloader -``` - -测试与已配置的下载器(qBittorrent)的连接。 - ---- - -## 下载器管理 - -直接从 AutoBangumi 管理下载器中的种子。 - -### 列出种子 - -``` -GET /downloader/torrents -``` - -获取 Bangumi 分类中的所有种子。 - -### 暂停种子 - -``` -POST /downloader/torrents/pause -``` - -通过哈希暂停种子。 - -**请求体:** -```json -{ - "hashes": ["hash1", "hash2"] -} -``` - -### 恢复种子 - -``` -POST /downloader/torrents/resume -``` - -通过哈希恢复已暂停的种子。 - -**请求体:** -```json -{ - "hashes": ["hash1", "hash2"] -} -``` - -### 删除种子 - -``` -POST /downloader/torrents/delete -``` - -删除种子,可选择是否删除文件。 - -**请求体:** -```json -{ - "hashes": ["hash1", "hash2"], - "delete_files": false -} -``` - ---- - -## 设置向导 - -这些端点仅在首次运行设置期间可用(设置完成前)。它们**不**需要认证。设置完成后,所有端点返回 `403 Forbidden`。 - -### 检查设置状态 - -``` -GET /setup/status -``` - -检查是否需要设置向导(首次运行)。 - -**响应:** -```json -{ - "need_setup": true -} -``` - -### 测试下载器连接 - -``` -POST /setup/test-downloader -``` - -使用提供的凭据测试与下载器的连接。 - -**请求体:** -```json -{ - "type": "qbittorrent", - "host": "172.17.0.1:8080", - "username": "admin", - "password": "adminadmin", - "ssl": false -} -``` - -### 测试 RSS 订阅源 - -``` -POST /setup/test-rss -``` - -验证 RSS 订阅源 URL 是否可访问和可解析。 - -**请求体:** -```json -{ - "url": "https://mikanime.tv/RSS/MyBangumi?token=xxx" -} -``` - -### 测试通知 - -``` -POST /setup/test-notification -``` - -使用提供的设置发送测试通知。 - -**请求体:** -```json -{ - "type": "telegram", - "token": "bot_token", - "chat_id": "chat_id" -} -``` - -### 完成设置 - -``` -POST /setup/complete -``` - -保存所有配置并将设置标记为完成。创建标记文件 `config/.setup_complete`。 - -**请求体:** 完整配置对象。 - ---- - -## 日志 - -### 获取日志 - -``` -GET /log -``` - -获取完整的应用程序日志文件。 - -### 清除日志 - -``` -GET /log/clear -``` - -清除日志文件。 - ---- - -## 响应格式 - -所有 API 响应遵循统一格式: - -```json -{ - "msg_en": "Success message in English", - "msg_zh": "成功消息(中文)", - "status": true -} -``` - -错误响应包含适当的 HTTP 状态码(400、401、403、404、500)以及中英文错误消息。 +- 许多操作类端点返回 `{"status": true, "msg_en": "...", "msg_zh": "..."}`。查询类端点直接返回数据。具体结构见 `/docs`。 +- 错误使用标准 HTTP 状态码:`401` 未认证,`403` 无权限,`404` 不存在,`422` 参数校验失败,`500` 服务器错误。 diff --git a/docs/dev/database.md b/docs/dev/database.md index 359879f0..b3722eae 100644 --- a/docs/dev/database.md +++ b/docs/dev/database.md @@ -1,394 +1,146 @@ # 数据库开发指南 -本指南介绍 AutoBangumi 中的数据库架构、模型和操作。 +本文说明 AutoBangumi 后端的数据层:引擎、`Database` 门面、仓储、会话约定和 schema 迁移。代码在 `backend/src/module/database/`。 ## 概述 -AutoBangumi 使用 **SQLite** 作为数据库,使用 **SQLModel**(Pydantic + SQLAlchemy 混合)作为 ORM。数据库文件位于 `data/data.db`。 - -### 架构 +- 数据库是 SQLite 文件 `data/data.db`,ORM 是 SQLModel(Pydantic + SQLAlchemy)。 +- 整个应用只用异步引擎 `sqlite+aiosqlite`。每个连接设置 `PRAGMA journal_mode=WAL`、`busy_timeout=5000` 和 `foreign_keys=ON`:多个连接可以并发写,冲突的写入会等待而不是立刻报 `database is locked`。 +- 外键约束是生效的。删除被引用的行之前,要先处理引用它的行(例如 `BangumiDatabase.delete_all` 先删种子记录)。 ``` module/database/ -├── engine.py # SQLAlchemy 引擎配置 -├── combine.py # Database 类、迁移、会话管理 -├── bangumi.py # 番剧(动画订阅)操作 -├── rss.py # RSS 订阅源操作 -├── torrent.py # 种子跟踪操作 -└── user.py # 用户认证操作 +├── __init__.py # Database、get_db(FastAPI 依赖) +├── engine.py # async_engine、async_session_factory、连接 PRAGMA +├── combine.py # Database:一个会话加全部仓储 +├── migrations.py # MIGRATIONS 表与迁移执行 +├── bangumi.py、rss.py、torrent.py、movie.py、user.py、auth.py、 +├── inbox.py、llm_credential.py、rename_operation.py、aria2.py、 +└── plugin_kv.py、passkey.py # 各表的仓储 ``` -## 核心组件 +## Database 与会话 -### Database 类 - -`combine.py` 中的 `Database` 类是主入口点。它继承自 SQLModel 的 `Session`,并提供对所有子数据库的访问: +`Database` 是异步上下文管理器。它创建一个 `AsyncSession`,并把仓储挂在自己身上;退出 `async with` 块时关闭会话。 ```python from module.database import Database -with Database() as db: - # 访问子数据库 - bangumis = db.bangumi.search_all() - rss_items = db.rss.search_active() - torrents = db.torrent.search_all() +async with Database() as db: + rules = await db.bangumi.search_all() + feeds = await db.rss.search_all() ``` -### 子数据库类 +| 属性 | 仓储 | 表 | +| --- | --- | --- | +| `db.bangumi` | `BangumiDatabase` | `bangumi` | +| `db.rss` | `RSSDatabase` | `rssitem` | +| `db.torrent` | `TorrentDatabase` | `torrent` | +| `db.movie` | `MovieDatabase` | `movie` | +| `db.user` | `UserDatabase` | `user` | +| `db.auth` | `AuthDatabase` | `auth_session`、`api_token` | +| `db.inbox` | `InboxDatabase` | 通知中心 | +| `db.llm_credential` | `LLMCredentialDatabase` | LLM 订阅凭据 | +| `db.rename_operation` | `RenameOperationDatabase` | `rename_operation`(重命名冲突与进度) | +| `db.aria2` | `Aria2GidDatabase` | `aria2_gid` | +| `db.plugin_kv` | `PluginKVDatabase` | `plugin_kv`(插件的 `ctx.kv`) | -| 类 | 模型 | 用途 | -|-------|-------|---------| -| `BangumiDatabase` | `Bangumi` | 动画订阅规则 | -| `RSSDatabase` | `RSSItem` | RSS 订阅源 | -| `TorrentDatabase` | `Torrent` | 已下载种子跟踪 | -| `UserDatabase` | `User` | 认证 | +`PasskeyDatabase` 不挂在 `Database` 上,需要时用 `PasskeyDatabase(db.session)` 构造。 -## 模型 +`Database` 还转发几个会话方法:`add`(同步,只是入队)、`commit`、`rollback`、`refresh`、`close`。`begin_write()` 在 SQLite 上执行 `BEGIN IMMEDIATE`,用于「先读后写且须保持不变量」的场景,必须在本会话的第一次访问之前调用。 -### Bangumi 模型 +### 会话约定 -动画订阅的核心模型: +**每次操作一个会话。** 不要把会话或 `Database` 存到比一次请求或一轮循环活得更久的对象上。`AppContext` 不持有会话。 + +- 路由用依赖注入: + + ```python + from fastapi import Depends + from module.database import Database, get_db + + @router.get("/example") + async def example(db: Database = Depends(get_db)): + return await db.bangumi.search_all() + ``` + +- 定时循环与服务在每次执行时自己开:`async with Database() as db:`。 +- 服务在构造函数里接收依赖:`RSSEngine(db)`、`TorrentManager(db)`。服务内部用 `self.db.<仓储>`;只需要一个仓储的调用方直接用 `db.<仓储>`。 +- 引擎设置了 `expire_on_commit=False`,提交后对象的属性仍可读,但对象不再与新会话关联。需要修改时,在新的会话里重新查询。 + +## 仓储 + +仓储接收一个 `AsyncSession`,方法都是 `async def`,大多在方法内部提交。新增查询时: + +- 放进对应表的仓储,不要在路由或服务里直接写 SQL。 +- 批量查询用 `in_` 一次取回,不要在循环里逐条查。 +- 软删除:`bangumi` 和 `movie` 有 `deleted` 字段。「停用」规则(`disable_rule`)只把 `deleted` 置为 `True`;`delete_one` 才真正删除行。查询活动规则时要过滤 `deleted`。 + +### 多下载器 + +4.0 支持多个下载器实例(配置 `plugins.instances`)。`bangumi`、`movie`、`rssitem` 和 `torrent` 都有 `downloader_id` 列: + +- 规则与订阅上为空表示「使用默认实例」(`plugins.slots.downloader`)。 +- 种子记录上是种子实际所在的实例。从 3.3 升级的存量种子为 `default`。 + +## Schema 迁移 + +迁移在 `migrations.py` 的 `MIGRATIONS` 元组中,以表驱动的方式执行。数据库里的 `schema_version` 表记录已应用的版本。 ```python -class Bangumi(SQLModel, table=True): - id: int # 主键 - official_title: str # 显示名称(如"无职转生") - title_raw: str # 用于种子匹配的原始标题(有索引) - season: int = 1 # 季度编号 - episode_offset: int = 0 # 集数编号调整 - season_offset: int = 0 # 季度编号调整 - rss_link: str # 逗号分隔的 RSS 订阅源 URL - filter: str # 排除过滤器(如 "720,\\d+-\\d+") - poster_link: str # TMDB 海报 URL - save_path: str # 下载目标路径 - rule_name: str # qBittorrent RSS 规则名称 - added: bool = False # 规则是否已添加到下载器 - deleted: bool = False # 软删除标志(有索引) - archived: bool = False # 用于已完结系列(有索引) - needs_review: bool = False # 检测到偏移不匹配 - needs_review_reason: str # 需要审核的原因 - suggested_season_offset: int # 建议的季度偏移 - suggested_episode_offset: int # 建议的集数偏移 - air_weekday: int # 放送日(0=周日,6=周六) +Migration( + 26, + "add downloader_id to bangumi, movie, rssitem and torrent", + (...SQL 语句...), + already_applied=..., # 守卫:该迁移是否已生效 + guarded_statements=(...), # 可选:逐条语句的守卫 +) ``` -### RSSItem 模型 +启动时 AB 先执行 `SQLModel.metadata.create_all`,再执行 `run_migrations()`: -RSS 订阅源: +1. 读取 `schema_version`,跳过版本号不大于它的迁移。 +2. 对每个待应用的迁移,重新检查数据库结构,再调用守卫:已生效(例如新库已由 `create_all` 按模型建表)时只记录版本号。 +3. 否则在一个 SAVEPOINT 中执行它的语句。任何一条失败,该迁移回滚,异常向上抛出,整个迁移事务回滚,AB 启动失败。AB 不会在只迁移了一半的 schema 上继续运行。 +4. 有迁移被执行时,最后按模型的默认值填充存量行中的 `NULL`(`fill_null_with_defaults_conn`):跳过主键、`default_factory` 字段和默认值为 `None` 的可选字段。 + +### 添加迁移 + +1. 先修改 `module/models/` 中的模型。新库由 `create_all` 直接得到新结构。 +2. 在 `MIGRATIONS` 末尾追加一个 `Migration`,版本号为上一个加 1。`CURRENT_SCHEMA_VERSION` 由列表推出,不要手动修改。 +3. 提供 `already_applied` 守卫:`column_exists(表, 列)`、`table_exists(表)`、`index_exists(表, 索引)`,多个条件用 `all_checks(...)` 组合。一个迁移改多张表、每张表可能处于不同状态时,用 `guarded_statements` 给每条语句单独的守卫。 +4. 在 `backend/src/test/` 中为迁移加测试:从旧 schema 开始,执行迁移,检查结果。 ```python -class RSSItem(SQLModel, table=True): - id: int # 主键 - name: str # 显示名称 - url: str # 订阅源 URL(唯一,有索引) - aggregate: bool = True # 是否解析种子 - parser: str = "mikan" # 解析器类型:mikan、dmhy、nyaa - enabled: bool = True # 启用标志 - connection_status: str # "healthy" 或 "error" - last_checked_at: str # ISO 时间戳 - last_error: str # 最后一次错误消息 +Migration( + 27, + "add note to bangumi", + ("ALTER TABLE bangumi ADD COLUMN note TEXT DEFAULT ''",), + column_exists("bangumi", "note"), +) ``` -### Torrent 模型 - -跟踪已下载的种子: - -```python -class Torrent(SQLModel, table=True): - id: int # 主键 - name: str # 种子名称(有索引) - url: str # 种子/磁力链接 URL(唯一,有索引) - rss_id: int # 来源 RSS 订阅源 ID - bangumi_id: int # 关联的番剧 ID(可为空) - qb_hash: str # qBittorrent 信息哈希(有索引) - downloaded: bool = False # 下载完成 -``` - -## 常用操作 - -### BangumiDatabase - -```python -with Database() as db: - # 创建 - db.bangumi.add(bangumi) # 单条插入 - db.bangumi.add_all(bangumi_list) # 批量插入(去重) - - # 读取 - db.bangumi.search_all() # 所有记录(缓存,5分钟 TTL) - db.bangumi.search_id(123) # 按 ID 查询 - db.bangumi.match_torrent("torrent name") # 按 title_raw 匹配查找 - db.bangumi.not_complete() # 未完结系列 - db.bangumi.get_needs_review() # 标记需要审核的 - - # 更新 - db.bangumi.update(bangumi) # 更新单条记录 - db.bangumi.update_all(bangumi_list) # 批量更新 - - # 删除 - db.bangumi.delete_one(123) # 硬删除 - db.bangumi.disable_rule(123) # 软删除(deleted=True) -``` - -### RSSDatabase - -```python -with Database() as db: - # 创建 - db.rss.add(rss_item) # 单条插入 - db.rss.add_all(rss_items) # 批量插入(去重) - - # 读取 - db.rss.search_all() # 所有订阅源 - db.rss.search_active() # 仅启用的订阅源 - db.rss.search_aggregate() # 启用且 aggregate=True - - # 更新 - db.rss.update(id, rss_update) # 部分更新 - db.rss.enable(id) # 启用订阅源 - db.rss.disable(id) # 禁用订阅源 - db.rss.enable_batch([1, 2, 3]) # 批量启用 - db.rss.disable_batch([1, 2, 3]) # 批量禁用 -``` - -### TorrentDatabase - -```python -with Database() as db: - # 创建 - db.torrent.add(torrent) # 单条插入 - db.torrent.add_all(torrents) # 批量插入 - - # 读取 - db.torrent.search_all() # 所有种子 - db.torrent.search_by_qb_hash(hash) # 按 qBittorrent 哈希查询 - db.torrent.search_by_url(url) # 按 URL 查询 - db.torrent.check_new(torrents) # 过滤掉已存在的 - - # 更新 - db.torrent.update_qb_hash(id, hash) # 设置 qb_hash -``` - -## 缓存 - -### 番剧缓存 - -`search_all()` 的结果在模块级别缓存,TTL 为 5 分钟: - -```python -# bangumi.py 中的模块级缓存 -_bangumi_cache: list[Bangumi] | None = None -_bangumi_cache_time: float = 0 -_BANGUMI_CACHE_TTL: float = 300.0 # 5 分钟 - -# 缓存失效 -def _invalidate_bangumi_cache(): - global _bangumi_cache, _bangumi_cache_time - _bangumi_cache = None - _bangumi_cache_time = 0 -``` - -**重要:** 缓存在以下操作时自动失效: -- `add()`、`add_all()` -- `update()`、`update_all()` -- `delete_one()`、`delete_all()` -- `archive_one()`、`unarchive_one()` -- 任何 RSS 链接更新操作 - -### 会话分离 - -缓存的对象会从会话中**分离**,以防止 `DetachedInstanceError`: - -```python -for b in bangumis: - self.session.expunge(b) # 从会话中分离 -``` - -## 迁移系统 - -### Schema 版本控制 - -迁移通过 `schema_version` 表跟踪: - -```python -CURRENT_SCHEMA_VERSION = 7 - -# 每个迁移:(版本号, 描述, [SQL 语句]) -MIGRATIONS = [ - (1, "add air_weekday column", [...]), - (2, "add connection status columns", [...]), - (3, "create passkey table", [...]), - (4, "add archived column", [...]), - (5, "rename offset to episode_offset", [...]), - (6, "add qb_hash column", [...]), - (7, "add suggested offset columns", [...]), -] -``` - -### 添加新迁移 - -1. 在 `combine.py` 中增加 `CURRENT_SCHEMA_VERSION` -2. 在 `MIGRATIONS` 列表中添加迁移元组: - -```python -MIGRATIONS = [ - # ... 现有迁移 ... - ( - 8, - "add my_new_column to bangumi", - [ - "ALTER TABLE bangumi ADD COLUMN my_new_column TEXT DEFAULT NULL", - ], - ), -] -``` - -3. 在 `run_migrations()` 中添加幂等性检查: - -```python -if "bangumi" in tables and version == 8: - columns = [col["name"] for col in inspector.get_columns("bangumi")] - if "my_new_column" in columns: - needs_run = False -``` - -4. 更新 `module/models/` 中对应的 Pydantic 模型 - -### 默认值回填 - -迁移后,`_fill_null_with_defaults()` 会根据模型默认值自动填充 NULL 值: - -```python -# 如果模型定义为: -class Bangumi(SQLModel, table=True): - my_field: bool = False - -# 那么现有记录中的 NULL 值将被更新为 False -``` - -## 性能模式 - -### 批量查询 - -`add_all()` 使用单个查询检查重复项,而不是 N 个查询: - -```python -# 高效:单个 SELECT -keys_to_check = [(d.title_raw, d.group_name) for d in datas] -conditions = [ - and_(Bangumi.title_raw == tr, Bangumi.group_name == gn) - for tr, gn in keys_to_check -] -statement = select(Bangumi.title_raw, Bangumi.group_name).where(or_(*conditions)) -``` - -### 正则表达式匹配 - -`match_list()` 为所有标题匹配编译单个正则表达式模式: - -```python -# 编译一次,匹配多次 -sorted_titles = sorted(title_index.keys(), key=len, reverse=True) -pattern = "|".join(re.escape(title) for title in sorted_titles) -title_regex = re.compile(pattern) - -# 每个种子 O(1) 查找而不是 O(n) -for torrent in torrent_list: - match = title_regex.search(torrent.name) -``` - -### 索引列 - -以下列具有索引以实现快速查找: - -| 表 | 列 | 索引类型 | -|-------|--------|------------| -| `bangumi` | `title_raw` | 普通 | -| `bangumi` | `deleted` | 普通 | -| `bangumi` | `archived` | 普通 | -| `rssitem` | `url` | 唯一 | -| `torrent` | `name` | 普通 | -| `torrent` | `url` | 唯一 | -| `torrent` | `qb_hash` | 普通 | - ## 测试 -### 测试数据库设置 +`backend/src/test/conftest.py` 提供内存数据库: -测试使用内存中的 SQLite 数据库: +- `db_engine`:`sqlite+aiosqlite://` 加 `StaticPool`,每个测试新建并按模型建表。 +- `db_session`:绑定 `db_engine` 的 `AsyncSession`。 + +直接测试仓储时,用 `Database(engine=db_engine)`,或用 `db_session` 构造单个仓储: ```python -# conftest.py -@pytest.fixture -def db_engine(): - engine = create_engine("sqlite:///:memory:") - SQLModel.metadata.create_all(engine) - yield engine - engine.dispose() +from module.database import Database +from module.database.bangumi import BangumiDatabase +from test.factories import make_bangumi -@pytest.fixture -def db_session(db_engine): - with Session(db_engine) as session: - yield session +async def test_add_bangumi(db_engine): + async with Database(engine=db_engine) as db: + assert await db.bangumi.add(make_bangumi()) + +async def test_search(db_session): + repo = BangumiDatabase(db_session) + assert await repo.search_all() == [] ``` -### 工厂函数 - -使用工厂函数创建测试数据: - -```python -from test.factories import make_bangumi, make_torrent, make_rss_item - -def test_bangumi_search(): - bangumi = make_bangumi(title_raw="Test Title", season=2) - # ... 测试逻辑 -``` - -## 设计说明 - -### 无外键 - -SQLite 外键强制默认是禁用的。关系(如 `Torrent.bangumi_id`)在应用程序逻辑中管理,而不是通过数据库约束。 - -### 软删除 - -`Bangumi.deleted` 标志启用软删除。面向用户的数据查询应按 `deleted=False` 过滤: - -```python -statement = select(Bangumi).where(Bangumi.deleted == false()) -``` - -### 种子标记 - -种子在 qBittorrent 中使用 `ab:{bangumi_id}` 标记,用于重命名操作时的偏移查找。这使得无需数据库查询即可快速识别番剧。 - -## 常见问题 - -### DetachedInstanceError - -如果您从不同的会话访问缓存的对象: - -```python -# 错误:在新会话中访问缓存的对象 -bangumis = db.bangumi.search_all() # 已缓存 -with Database() as new_db: - new_db.session.add(bangumis[0]) # 错误! - -# 正确:对象已分离,可独立工作 -bangumis = db.bangumi.search_all() -bangumis[0].title_raw = "New Title" # 可以,但不会持久化 -``` - -### 缓存过期 - -如果手动 SQL 更新绕过了 ORM,请使缓存失效: - -```python -from module.database.bangumi import _invalidate_bangumi_cache - -with engine.connect() as conn: - conn.execute(text("UPDATE bangumi SET ...")) - conn.commit() - -_invalidate_bangumi_cache() # 重要! -``` +后端的 pytest 已开启 `asyncio_mode = "auto"`,异步测试函数不需要额外的装饰器。运行:`cd backend && uv run pytest src/test/test_xxx.py -v`。 diff --git a/docs/dev/e2e-test-guide.md b/docs/dev/e2e-test-guide.md index 177875e5..7b15e837 100644 --- a/docs/dev/e2e-test-guide.md +++ b/docs/dev/e2e-test-guide.md @@ -1,145 +1,62 @@ -# E2E Integration Test Guide +# E2E 测试指南 -End-to-end tests that exercise the full AutoBangumi workflow against real -Docker services (qBittorrent + mock RSS server). +端到端测试用生产构建的 AutoBangumi 镜像,对接本地的、确定性的上游服务(模拟 RSS、TMDB、播放器与 qBittorrent)。测试不访问任何公网服务。Docker Compose 在回环地址上发布随机端口,服务之间走内部网络。 -## Prerequisites +完整说明(各条测试线、诊断产物、添加模拟场景、更新 qBittorrent 镜像摘要)在仓库的 [`e2e/README.md`](https://github.com/EstrellaXD/Auto_Bangumi/blob/main/e2e/README.md)。本页只给出本地运行的最短路径。 -- **Docker** with `docker compose` (v2) -- **uv** for Python dependency management -- Ports **7892**, **18080**, **18888** must be free +## 准备 -## Quick Start +- Docker Engine 与 Compose v2 +- Python 3.13 与 `uv` +- Node.js 20 与 pnpm 9.11 ```bash -# 1. Build the mock RSS server image -cd backend/src/test/e2e -docker build -f Dockerfile.mock-rss -t ab-mock-rss . - -# 2. Start test infrastructure -docker compose -f docker-compose.test.yml up -d --wait - -# 3. Verify services are healthy -docker compose -f docker-compose.test.yml ps - -# 4. Run E2E tests -cd backend && uv run pytest -m e2e -v --tb=long - -# 5. Cleanup -docker compose -f backend/src/test/e2e/docker-compose.test.yml down -v +uv sync --directory backend --locked --group dev +pnpm --dir webui install --frozen-lockfile +pnpm --dir webui exec playwright install --with-deps chromium webkit firefox ``` -## Architecture - -``` -Host machine -├── pytest (test runner) -│ └── Drives HTTP requests to AutoBangumi at localhost:7892 -├── AutoBangumi subprocess -│ ├── Isolated config/ and data/ in temp directory -│ └── Uses mock downloader (no real qB coupling during setup) -├── qBittorrent container (localhost:18080) -│ └── linuxserver/qbittorrent:latest -└── Mock RSS server container (localhost:18888) - └── Serves static XML fixtures from fixtures/ -``` - -## Test Phases - -| Phase | Tests | What It Validates | -|-------|-------|-------------------| -| 1. Setup Wizard | `test_01` - `test_06` | First-run detection, mock downloader, setup completion, 403 guard | -| 2. Authentication | `test_10` - `test_13` | Login, cookie-based JWT, token refresh, logout | -| 3. Configuration | `test_20` - `test_22` | Config CRUD, password masking | -| 4. RSS Management | `test_30` - `test_32` | Add, list, delete RSS feeds | -| 5. Program Lifecycle | `test_40` - `test_41` | Status check, restart | -| 6. Downloader | `test_50` - `test_51` | Mock downloader health, direct qB connectivity | -| 7. Cleanup | `test_90` | Logout | - -## Key Design Decisions - -### Mock Downloader for Setup - -The setup wizard's `_validate_url()` blocks private/loopback IPs (SSRF -protection). Since the Docker qBittorrent instance is on `localhost`, the -setup wizard's "test downloader" endpoint would reject it. Instead: - -1. Setup uses `downloader_type: "mock"` (bypasses URL validation) -2. Config can be updated to point to real qBittorrent after auth -3. Direct qBittorrent connectivity is tested independently (`test_51`) - -### DEV_VERSION Auth Bypass - -When running from source, `VERSION == "DEV_VERSION"` which bypasses JWT -validation (`get_current_user` returns `"dev_user"` unconditionally). Tests -document this behavior: login/refresh/logout endpoints still work, but -unauthenticated access is also allowed. In production builds, test_13 -would expect HTTP 401. - -### CWD-Based Isolation - -AutoBangumi resolves all paths relative to the working directory: -- `config/` - config files, JWT secret, setup sentinel -- `data/` - SQLite database, posters, logs - -The `ab_process` fixture creates a temp directory with these subdirs and -runs `main.py` from there, ensuring complete isolation from any existing -installation. - -### qBittorrent Password Extraction - -Recent `linuxserver/qbittorrent` images generate a random temporary -password on first start. The `qb_password` fixture polls `docker logs` -until it finds the line: - -``` -A temporary password is provided for this session: XXXXXXXX -``` - -## Debugging Failures - -### AutoBangumi won't start +## 构建测试镜像 ```bash -# Check if port 7892 is in use -lsof -i :7892 - -# Run manually to see startup logs -cd /tmp/test-workdir && uv run python /path/to/backend/src/main.py +pnpm --dir webui run build +python3 e2e/scripts/build_test_image.py \ + --version 3.3.999-e2e.1 \ + --image auto-bangumi:e2e \ + --dist webui/dist ``` -### qBittorrent issues +构建脚本在临时的 Docker 上下文里放入 WebUI 产物和生成的版本模块,不会写入源码树。 + +## 运行 + +每次运行都用新的项目名和工作目录。重用工作目录会同时重用设置状态,首次运行的覆盖就失效了。 ```bash -docker logs ab-test-qbittorrent -docker exec ab-test-qbittorrent curl -s http://localhost:18080 +export LANE=runtime +export AB_E2E_PROJECT="ab-e2e-local-$LANE-$$" +export AB_E2E_WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ab-e2e-$LANE.XXXXXX")" +export AB_E2E_APP_IMAGE=auto-bangumi:e2e + +# 运行时 API 与进程测试 +python3 e2e/scripts/stack.py run \ + --profile browser \ + --project-name "$AB_E2E_PROJECT" \ + --work-dir "$AB_E2E_WORK_DIR" \ + -- uv run --directory backend pytest src/test/e2e/runtime -m e2e -q ``` -### Mock RSS server issues +| 测试线 | `--` 之后的命令 | `--profile` | +| --- | --- | --- | +| 运行时 | `uv run --directory backend pytest src/test/e2e/runtime -m e2e -q` | `browser` | +| Chromium 桌面 | `pnpm --dir webui run test:e2e:chromium --retries=0` | `browser` | +| WebKit 移动端 | `pnpm --dir webui run test:e2e:webkit --retries=0` | `browser` | +| 真实 qBittorrent 与打包镜像 | `python3 e2e/scripts/run_downloader_lane.py` | `downloader` | -```bash -docker logs ab-test-mock-rss -curl http://localhost:18888/health -curl http://localhost:18888/rss/mikan.xml -``` +- 普通的 `pytest` 会跳过 E2E 用例,必须显式传入 `-m e2e`。 +- `stack.py run` 在拆除 Compose 之前收集诊断信息,写到 `$AB_E2E_WORK_DIR/artifacts`。Playwright 的截图与视频在 `webui/test-results`,HTML 报告在 `webui/playwright-report`。 +- 停止一个栈:`python3 e2e/scripts/stack.py stop --profile browser --project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"`。 -### Test infrastructure stuck +## CI -```bash -# Force cleanup -docker compose -f backend/src/test/e2e/docker-compose.test.yml down -v --remove-orphans -``` - -## Adding New Test Scenarios - -1. Add new test methods to `TestE2EWorkflow` in definition order -2. Use `api_client` for HTTP requests (cookies persist across tests) -3. Use `e2e_state` dict to share data between tests -4. For new RSS fixtures, add XML files to `fixtures/` directory -5. Keep test names ordered: `test_XX_description` where XX reflects the phase - -### Adding a new fixture feed - -1. Create `backend/src/test/e2e/fixtures/your_feed.xml` -2. Access via `http://localhost:18888/rss/your_feed.xml` -3. Rebuild the mock RSS image: `docker compose ... build mock-rss` +`.github/workflows/e2e.yml` 在每个 PR 上运行运行时线、浏览器线(Chromium、WebKit 移动端)和真实下载器线。`e2e-nightly.yml` 每晚运行 Firefox 线,并把浏览器线和下载器线各独立运行两遍。端点结构、参数校验等更快的契约测试仍在 `backend/src/test/test_api_*.py` 中。 diff --git a/docs/dev/index.md b/docs/dev/index.md index 61463683..c4c19325 100644 --- a/docs/dev/index.md +++ b/docs/dev/index.md @@ -128,7 +128,12 @@ AutoBangumi 使用"分支开发,主干发布"模式。 ## 发布流程 -目前,发布是在开发团队手动合并特定的"发布 PR"后自动触发的。 +发布只由维护者手动推送版本标签触发,合并 PR 本身不会发布。 + +- **beta**:在开发分支上打 `X.Y.Z-beta.N` 形式的标签(如 `4.0.0-beta.1`)。CI 构建 Docker 镜像,镜像标签为该版本号与 `dev-latest`,并创建预发布的 GitHub Release。 +- **正式版**:开发分支经 PR 合并到 `main` 后,在 `main` 的合并提交上打 `X.Y.Z` 形式的标签。CI 校验标签格式,并确认该提交在 `main` 上,然后构建镜像(标签为版本号与 `latest`),创建 GitHub Release,发布说明取自 `docs/changelog/.md`。 + +每个 Release 的附件包括 WebUI、在线更新包、插件 SDK 轮子与插件开发 skill(见 [获取 SDK](/dev/plugins/sdk))。 Bug 修复 PR 通常会快速发布,一般在一周内。 diff --git a/docs/dev/plugins.md b/docs/dev/plugins.md index 8fba3127..be3ee638 100644 --- a/docs/dev/plugins.md +++ b/docs/dev/plugins.md @@ -8,23 +8,25 @@ ## 五分钟上手 -1. 安装 SDK 与命令行。轮子随每个 4.0 beta / 正式版发布在 [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases) 的附件中(文件名 `autobangumi_sdk-<版本>-py3-none-any.whl`),不发布到 PyPI: +1. 安装 SDK 与命令行。轮子随每个 4.0 beta / 正式版发布在 [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases) 的附件中(文件名 `autobangumi_sdk--py3-none-any.whl`),不发布到 PyPI。详见 [获取 SDK](/dev/plugins/sdk)。 ```bash uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl ``` -2. 生成骨架并测试: +2. 生成骨架并测试。骨架的 `pyproject.toml` 依赖 `autobangumi-sdk`,而 PyPI 上没有这个包,所以先用 `uv add` 让它指向下载的轮子: ```bash ab-plugin new my-rename --kind rename # 也可选 notifier、search - cd my-rename && uv run pytest # 骨架自带契约测试 + cd my-rename + uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # 换成轮子的实际路径 + uv run pytest # 骨架自带契约测试 ``` -3. 链接到本机的 AutoBangumi 并热重载(先启动过一次 AB,使 `config/` 下有配置文件): +3. 链接到本机的 AutoBangumi 并热重载。先启动过一次 AB,使它的配置目录下有配置文件;`--config-dir` 指向这个目录(源码运行时为 `backend/src/config`): ```bash - ab-plugin dev . + ab-plugin dev . --config-dir /path/to/autobangumi/config ``` 重启 AB 一次后,修改插件目录里的文件会自动重载。 @@ -34,6 +36,8 @@ **基础** +- [获取 SDK](/dev/plugins/sdk):GitHub Release 中的轮子与插件开发 skill + - [核心概念](/dev/plugins/concepts):`Plugin`、`ctx`、三种扩展声明、清单、隔离与熔断 - [配置表单](/dev/plugins/config-forms):`config_model` 如何变成 WebUI 表单 - [事件](/dev/plugins/events):系统事件、整理事件、自定义事件 diff --git a/docs/dev/plugins/builtin.md b/docs/dev/plugins/builtin.md index 1dde232c..36f2bf96 100644 --- a/docs/dev/plugins/builtin.md +++ b/docs/dev/plugins/builtin.md @@ -49,7 +49,7 @@ - 本插件之前为同一集创建的链接,在版本升级(新版本替换旧种子,规范文件名不变)后被原子替换为指向新文件的链接。 - 已链接的文件再次收到事件时不做任何事;插件在自己的键值存储里记录它创建过的目标路径。 - **删除种子不会删除媒体库中的链接。** -- 启用前已经下载的文件不会自动处理。在插件的设置分区点「补链已有文件」,或调用 `POST /api/v1/plugins/hardlink/backfill`:遍历 `source_root` 下的 `.mp4` / `.mkv` / `.ass` / `.srt`,返回 `{"linked", "exists", "conflict", "failed"}` 计数。 +- 启用前已经下载的文件不会自动处理。在插件的设置分区点「链接已有文件」,或调用 `POST /api/v1/plugins/hardlink/backfill`:遍历 `source_root` 下的 `.mp4` / `.mkv` / `.ass` / `.srt`,返回 `{"linked", "exists", "conflict", "failed"}` 计数。 ::: tip Docker 硬链接不能跨文件系统。在 Docker 中,请把下载目录与媒体库放在同一块盘上,并以**同一个挂载点**映射进 AB 容器(例如把 `/mnt/media` 整体挂载为 `/media`,下载目录与媒体库都在其下)。两个独立挂载的目录即使在同一块盘上,也会被视为不同文件系统,链接会退化为 `cross_device` 指定的行为。下载器运行在另一个容器里、看到的路径与 AB 不同时,用 `path_map` 做映射。 diff --git a/docs/dev/plugins/cli.md b/docs/dev/plugins/cli.md index 208752bd..34471b27 100644 --- a/docs/dev/plugins/cli.md +++ b/docs/dev/plugins/cli.md @@ -7,7 +7,7 @@ uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl # 或在项目里:uv add ./autobangumi_sdk-0.5.0-py3-none-any.whl ``` -轮子在 4.0 beta / 正式版的 GitHub Release 附件中,不发布到 PyPI。契约测试需要 pytest:`uv add --dev pytest`(或安装 `autobangumi-sdk[test]`)。 +轮子在 4.0 beta / 正式版的 GitHub Release 附件中,不发布到 PyPI,见 [获取 SDK](/dev/plugins/sdk)。契约测试需要 pytest:`new` 生成的 `pyproject.toml` 已在 `dev` 依赖组里带上 pytest;自己建的项目用 `uv add --dev pytest`(或安装 `autobangumi-sdk[test]`)。 ## new @@ -28,6 +28,8 @@ ab-plugin new [--kind rename|notifier|search] [--dir .] `id` 只能由小写字母、数字和连字符组成。目录已存在时拒绝执行。没有下载器骨架:下载器要连接真实后端,一个能直接通过契约的骨架没有意义。 +生成的 `pyproject.toml` 依赖 `autobangumi-sdk`。这个包不在 PyPI 上,运行 `uv run pytest` 前先在插件目录里执行一次 `uv add <轮子路径>`,uv 会把依赖指向本地轮子。 + ## validate ```bash @@ -42,7 +44,7 @@ ab-plugin validate [path] ab-plugin pack [path] [-o dist] ``` -先校验,再打成 `dist/-<版本>.zip`。zip 内容在 zip 根(与签名目录的包布局一致)。排除 `tests/`、`pyproject.toml`、`uv.lock`、缓存和 `dist/`。文件顺序与时间戳固定:同样的内容得到同样的 sha256。 +先校验,再打成 `dist/-<版本>.zip`。zip 内容在 zip 根(与签名目录的包布局一致)。排除 `tests/`、`pyproject.toml`、`uv.lock`、`dist/`、`.venv`、`.git`、`node_modules`、各类缓存和 `.pyc`。文件顺序与时间戳固定:同样的内容得到同样的 sha256。 ## dev @@ -50,7 +52,9 @@ ab-plugin pack [path] [-o dist] ab-plugin dev [path] [--config-dir config] ``` -- 把插件目录软链到 `/plugins/local/`。 +- 先校验,再把插件目录软链到 `/plugins/local/`。 +- `--config-dir` 默认是当前目录下的 `config`。在插件目录里运行时,请指向 AB 的配置目录(源码运行时为 `backend/src/config`)。 +- 软链接指向插件目录的绝对路径,AB 必须能用同一路径访问它(例如源码运行的 AB)。AB 在 Docker 中运行时,请把插件目录复制或挂载到容器的 `config/plugins/local//`,再在 设置 → 插件 中开启「允许未签名插件」并启用它。 - 在宿主配置文件(先找 `config_dev.json`,再找 `config.json`)里写入 `plugins.dev_mode`、`plugins.allow_unsigned` 和 `plugins.enabled.`。 - 配置文件不存在时拒绝执行:请先启动一次 AutoBangumi。 - 运行中的 AB 不会重读配置文件,需要重启一次。之后,`dev_mode` 每秒检查一次本地插件目录,文件变化后自动重载该插件。上次加载失败的插件也被监听,修好源码后自动恢复。 @@ -61,8 +65,9 @@ ab-plugin dev [path] [--config-dir config] ```bash ab-plugin new my-notify --kind notifier cd my-notify +uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # 只需一次 uv run pytest # 契约测试 -ab-plugin dev . # 链接并开启热重载,重启 AB 一次 +ab-plugin dev . --config-dir /path/to/autobangumi/config # 链接并开启热重载,重启 AB 一次 # 改代码 → AB 自动重载 → 在 WebUI 里验证 ab-plugin pack . ``` diff --git a/docs/dev/plugins/config-forms.md b/docs/dev/plugins/config-forms.md index d586b9ba..6db80279 100644 --- a/docs/dev/plugins/config-forms.md +++ b/docs/dev/plugins/config-forms.md @@ -49,4 +49,4 @@ class MyPlugin(Plugin[Options]): ## 超出表单的界面 -表单满足不了时(图表、操作按钮、列表),插件可以提供前端组件,见 [前端挂载点](/dev/plugins/frontend-slots)。内置 `hardlink` 的「补链已有文件」按钮就是一个 `settings.section` 组件。 +表单满足不了时(图表、操作按钮、列表),插件可以提供前端组件,见 [前端挂载点](/dev/plugins/frontend-slots)。内置 `hardlink` 的「链接已有文件」按钮就是一个 `settings.section` 组件。 diff --git a/docs/dev/plugins/examples.md b/docs/dev/plugins/examples.md index 1f062361..95860a1e 100644 --- a/docs/dev/plugins/examples.md +++ b/docs/dev/plugins/examples.md @@ -21,10 +21,12 @@ cp -r examples/plugins/ntfy-notifier config/plugins/local/ ## 运行示例的测试 +在仓库根目录运行,脚本逐个校验示例的清单并运行它的测试(与 CI 相同): + ```bash -cd examples/plugins/ntfy-notifier && uv run pytest -# 或一次跑完全部示例(在仓库根目录): scripts/test_example_plugins.sh ``` +脚本在 `backend/` 的环境里运行,SDK 以可编辑方式装在那里。示例的 `pyproject.toml` 依赖 `autobangumi-sdk`,PyPI 上没有这个包;把示例复制到仓库外单独测试时,先在示例目录里 `uv add <轮子路径>`,再 `uv run pytest`。 + 刷新 Jellyfin / Emby / Plex 媒体库不需要写插件:内置插件 `media-server-refresh` 已经支持。 diff --git a/docs/dev/plugins/points/message-template.md b/docs/dev/plugins/points/message-template.md index 1c2ddafd..b6d323b7 100644 --- a/docs/dev/plugins/points/message-template.md +++ b/docs/dev/plugins/points/message-template.md @@ -21,5 +21,5 @@ class Alerts(Plugin): - 只影响外部推送。通知中心里的文案由前端按 `i18n()` 渲染,不受影响。 - 钩子对每个启用的渠道各调用一次;`channel` 是渠道类型,如 `telegram`。 -- 钩子出错或超时时沿用上一步的文案,并计入熔断;返回值类型不对时整条消息回退到默认文案。 +- 钩子出错、超时或返回的不是 `RenderedMessage` 时,AB 沿用上一步的文案,并计入熔断。后面的钩子照常执行。 - 「新集数」通知暂不经过该钩子,仍使用通知渠道里配置的单集模板。 diff --git a/docs/dev/plugins/sdk.md b/docs/dev/plugins/sdk.md new file mode 100644 index 00000000..b5743896 --- /dev/null +++ b/docs/dev/plugins/sdk.md @@ -0,0 +1,80 @@ +# 获取 SDK + +插件 SDK 不发布到 PyPI。每个 4.0 beta 与正式版的 [GitHub Release](https://github.com/EstrellaXD/Auto_Bangumi/releases) 都附带两个给插件作者的文件: + +| 文件 | 内容 | +| --- | --- | +| `autobangumi_sdk--py3-none-any.whl` | `ab_sdk` 包与 `ab-plugin` 命令行 | +| `autobangumi-plugin-skill-.zip` | 给 AI 编码助手使用的插件开发 skill | + +轮子文件名里是 SDK 版本(`ab_sdk.SDK_VERSION`,如 `0.5.0`),不是 AB 的版本号。请从目标 AB 版本的 Release 下载轮子,两者的 SDK 版本一致。插件清单里的 `sdk` 范围(如 `">=0.5,<1"`)必须包含这个版本。 + +## 安装轮子 + +轮子要求 Python 3.13 或更高,依赖 `pydantic`、`httpx` 与 `packaging`,不需要安装 AutoBangumi。 + +只用命令行时,装成全局工具: + +```bash +uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl +ab-plugin --help +``` + +写插件和跑测试时,把轮子加为插件项目的依赖: + +```bash +cd my-plugin +uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl +uv run pytest +``` + +`uv add` 在 `pyproject.toml` 的 `[tool.uv.sources]` 里记下轮子的本地路径。`ab-plugin pack` 不打包 `pyproject.toml` 与 `uv.lock`,这个路径不会进入插件包。`ab-plugin new` 生成的 `pyproject.toml` 已经依赖 `autobangumi-sdk`,但没有指定来源,所以在新骨架里也要先执行一次 `uv add`。 + +契约测试需要 pytest。骨架的 `dev` 依赖组已带上 pytest;也可以安装可选依赖 `autobangumi-sdk[test]`。 + +升级 SDK 时,下载新 Release 的轮子,再执行一次 `uv tool install --force <新轮子>` 或 `uv add <新轮子>`。 + +## 在 AB 仓库里开发 + +SDK 的源码在 `backend/src/ab_sdk`,打包配置在 `backend/sdk/pyproject.toml`。`backend` 的 `dev` 依赖组以可编辑方式安装它,所以在仓库里不需要轮子: + +```bash +cd backend +uv sync --group dev +uv run ab-plugin validate ../examples/plugins/ntfy-notifier +``` + +自己构建轮子(与 CI 相同的命令): + +```bash +uv build --wheel backend/sdk --out-dir sdk-dist +``` + +## 插件开发 skill + +skill 是一组 Markdown 说明,AI 编码助手读取它们来写、测试和打包 AB 插件。zip 内是 `autobangumi-plugin/` 目录: + +``` +autobangumi-plugin/ +├── SKILL.md # 工作流程、扩展点一览、常见错误 +└── references/ + ├── extension-points.md # 每个扩展点的签名与字段 + ├── frontend.md # 前端挂载点 + └── testing.md # 契约测试与 create_plugin +``` + +把它解压到助手的 skills 目录。以 Claude Code 为例,个人 skills 目录是 `~/.claude/skills/`: + +```bash +unzip autobangumi-plugin-skill-.zip -d ~/.claude/skills/ +``` + +其它助手请按它们各自的 skills 目录放置。skill 的源码在仓库的 `skills/autobangumi-plugin/`。 + +## CI 如何构建 + +`.github/workflows/build.yml` 的 `release` 任务只在维护者推送版本标签时运行: + +1. `uv build --wheel backend/sdk --out-dir sdk-dist` 构建轮子,版本取自 `ab_sdk.SDK_VERSION`。 +2. 把 `skills/autobangumi-plugin` 压缩为 `autobangumi-plugin-skill-.zip`。 +3. 把两者与 WebUI、在线更新包等文件一起附到该版本的 GitHub Release。beta(预发布)与正式版都带这两个文件。 diff --git a/docs/dev/plugins/signing.md b/docs/dev/plugins/signing.md index 39fd07fd..6d1ed9a5 100644 --- a/docs/dev/plugins/signing.md +++ b/docs/dev/plugins/signing.md @@ -1,6 +1,6 @@ # 签名与分发 -插件有三种来源,信任级别不同: +插件有四种来源,信任级别不同: | 来源 | 位置 | 签名 | 如何启用 | | --- | --- | --- | --- | @@ -49,11 +49,7 @@ AB 从 GitHub release `plugins` 下载 `catalog.json`。目录包含: 签名目录的插件只能依赖标准库和 AB 已有的包,不做 pip 安装。需要第三方纯 Python 库时放进 `vendor/`。 -::: warning WebUI 尚无目录界面 -4.0 预览期间,目录的浏览和安装只有 API,设置 → 插件 页还没有对应的按钮。 -::: - -API: +在 WebUI 中,**设置 → 插件** 的「插件目录」区域可以浏览目录,并安装、更新与卸载插件。API: | 请求 | 说明 | | --- | --- | @@ -86,4 +82,4 @@ API: ## 发布 SDK -SDK 以轮子发布在 4.0 beta / 正式版的 GitHub Release 附件里,由 CI 用 `uv build --wheel backend/sdk` 构建。同一个 Release 还附带 `autobangumi-plugin-skill-<版本>.zip`:给 AI 编码助手使用的插件开发 skill,解压到助手的 skills 目录即可。 +SDK 轮子与插件开发 skill 由 CI 构建,附在每个 4.0 beta / 正式版的 GitHub Release 上,维护者不需要手动发布。下载和安装见 [获取 SDK](/dev/plugins/sdk)。 diff --git a/docs/en/api/index.md b/docs/en/api/index.md index 7a037911..8058da7e 100644 --- a/docs/en/api/index.md +++ b/docs/en/api/index.md @@ -1,619 +1,221 @@ # REST API Reference -AutoBangumi exposes a REST API at `/api/v1`. All endpoints (except login and setup) require JWT authentication. +AutoBangumi provides a REST API under `/api/v1`. The WebUI uses the same API. **Base URL:** `http://your-host:7892/api/v1` -**Authentication:** Include the JWT token as a cookie or `Authorization: Bearer ` header. - -**Interactive Docs:** When running in development mode, Swagger UI is available at `http://your-host:7892/docs`. - ---- +**Interactive docs:** This page lists only the endpoints and their purpose. For the request and response fields, use `http://your-host:7892/docs` on a running instance. FastAPI makes this Swagger UI from the code. Plugin routes are not in `/docs`. ## Authentication -### Login +AB accepts two types of credential: -``` -POST /auth/login +- **Browser session**: send the form fields `username` and `password` to `POST /auth/login`. If the login is correct, AB sets the HttpOnly cookie `token`. The WebUI uses this method. +- **API token**: create a token with `scope=api` in Settings → Users & Access → API tokens (or call `POST /tokens`). AB shows the plain token one time only. Send it in the header `Authorization: Bearer `. + +```bash +curl -H "Authorization: Bearer $AB_TOKEN" http://your-host:7892/api/v1/status ``` -Authenticate with username and password. +- If a request has an `Authorization` header, AB checks only that token. It ignores the cookie. +- A request with no credential or a bad credential gets `401`. +- The account endpoints (`/auth/update`, `/users`, `/tokens`, and Passkey registration and management) accept only a browser session. An API token gets `403`. +- These endpoints need no authentication: `/auth/login`, `/passkey/auth/*`, the setup wizard `/setup/*` (`GET /setup/status` is always available; the other endpoints return `403` after setup is complete) and `/health` at the root path. +- `/auth/refresh_token` and `/auth/logout` ignore the `Authorization` header. They use only the session cookie. `refresh_token` returns `401` if there is no valid cookie. `security.login_whitelist` limits login and Passkey login. +- For local development, set the environment variable `AB_DEV_NO_AUTH=1` to skip all authentication. Do not set it in production. -**Request Body:** -```json -{ - "username": "string", - "password": "string" -} -``` +## Authentication and accounts -**Response:** Sets authentication cookie with JWT token. +| Method | Path | Description | +| --- | --- | --- | +| `POST` | `/auth/login` | Log in with user name and password. Sets the session cookie | +| `POST` | `/auth/refresh_token` | Extend the current session | +| `POST` | `/auth/logout` | End the current session and clear the cookie | +| `GET` | `/auth/me` | The current user | +| `POST` | `/auth/update` | Change the current account and rotate all of its sessions | +| `GET` / `POST` | `/users` | List / create users | +| `PATCH` / `DELETE` | `/users/{user_id}` | Change / delete a user | +| `GET` / `POST` | `/tokens` | List / create API tokens (`scope` is `api` or `mcp`; `expires_at` is optional) | +| `DELETE` | `/tokens/{token_id}` | Revoke a token | +| `POST` | `/passkey/register/options`, `/passkey/register/verify` | Register a Passkey | +| `POST` | `/passkey/auth/options`, `/passkey/auth/verify` | Log in with a Passkey | +| `GET` | `/passkey/list` | List the Passkeys of the current user | +| `POST` | `/passkey/delete` | Delete a Passkey | -### Refresh Token +## Program -``` -GET /auth/refresh_token -``` - -Refresh the current authentication token. - -### Logout - -``` -GET /auth/logout -``` - -Clear authentication cookies and log out. - -### Update Credentials - -``` -POST /auth/update -``` - -Update username and/or password. - -**Request Body:** -```json -{ - "username": "string", - "password": "string" -} -``` - ---- - -## Passkey / WebAuthn - -Passwordless authentication using WebAuthn/FIDO2 Passkeys. - -### Register Passkey - -``` -POST /passkey/register/options -``` - -Get WebAuthn registration options (challenge, relying party info). - -``` -POST /passkey/register/verify -``` - -Verify and save the Passkey registration response from the browser. - -### Authenticate with Passkey - -``` -POST /passkey/auth/options -``` - -Get WebAuthn authentication challenge options. - -``` -POST /passkey/auth/verify -``` - -Verify the Passkey authentication response and issue a JWT token. - -### Manage Passkeys - -``` -GET /passkey/list -``` - -List all registered Passkeys for the current user. - -``` -POST /passkey/delete -``` - -Delete a registered Passkey by credential ID. - ---- +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/status` | Version, run state and first-run flag | +| `POST` | `/start`, `/stop`, `/restart` | Start / stop / restart the background tasks | +| `POST` | `/shutdown` | Stop the program | +| `GET` | `/check/downloader` | Check that the default downloader is available | +| `GET` | `/log` | Read the log | +| `POST` | `/log/clear` | Clear the log | +| `GET` | `/update/check` | Get the latest version and the online update state | +| `POST` | `/update/apply` | Download and apply the latest update, then restart | +| `POST` | `/update/rollback` | Go back to the previous update. If there is no backup, go back to the version in the image | +| `GET` | `/health` (root path, not under `/api/v1`) | Liveness probe. Always returns `200`: `{"status", "version", "db_ok"}` | ## Configuration -### Get Configuration - -``` -GET /config/get -``` - -Retrieve the current application configuration. - -**Response:** Full configuration object including `program`, `downloader`, `rss_parser`, `bangumi_manager`, `notification`, `proxy`, `llm`, and other sections. - -### Update Configuration - -``` -PATCH /config/update -``` - -Partially update the application configuration. Only include fields you want to change. - -**Request Body:** Partial configuration object. - ---- - -## Bangumi (Anime Rules) - -### List All Bangumi - -``` -GET /bangumi/get/all -``` - -Get all anime download rules. - -### Get Bangumi by ID - -``` -GET /bangumi/get/{bangumi_id} -``` - -Get a specific anime rule by ID. - -### Update Bangumi - -``` -PATCH /bangumi/update/{bangumi_id} -``` - -Update an anime rule's metadata (title, season, episode offset, etc.). - -### Delete Bangumi - -``` -DELETE /bangumi/delete/{bangumi_id} -``` - -Delete a single anime rule and its associated torrents. - -``` -DELETE /bangumi/delete/many/ -``` - -Batch delete multiple anime rules. - -**Request Body:** -```json -{ - "bangumi_ids": [1, 2, 3] -} -``` - -### Disable / Enable Bangumi - -``` -DELETE /bangumi/disable/{bangumi_id} -``` - -Disable an anime rule (keeps files, stops downloading). - -``` -DELETE /bangumi/disable/many/ -``` - -Batch disable multiple anime rules. - -``` -GET /bangumi/enable/{bangumi_id} -``` - -Re-enable a previously disabled anime rule. - -### Poster Refresh - -``` -GET /bangumi/refresh/poster/all -``` - -Refresh poster images for all anime from TMDB. - -``` -GET /bangumi/refresh/poster/{bangumi_id} -``` - -Refresh the poster image for a specific anime. - -### Calendar - -``` -GET /bangumi/refresh/calendar -``` - -Refresh the anime broadcast calendar data from Bangumi.tv. - -### Reset All - -``` -GET /bangumi/reset/all -``` - -Delete all anime rules. Use with caution. - ---- - -## RSS Feeds - -### List All Feeds - -``` -GET /rss -``` - -Get all configured RSS feeds. - -### Add Feed - -``` -POST /rss/add -``` - -Add a new RSS feed subscription. - -**Request Body:** -```json -{ - "url": "string", - "aggregate": true, - "parser": "mikan" -} -``` - -### Enable / Disable Feeds - -``` -POST /rss/enable/many -``` - -Enable multiple RSS feeds. - -``` -PATCH /rss/disable/{rss_id} -``` - -Disable a single RSS feed. - -``` -POST /rss/disable/many -``` - -Batch disable multiple RSS feeds. - -### Delete Feeds - -``` -DELETE /rss/delete/{rss_id} -``` - -Delete a single RSS feed. - -``` -POST /rss/delete/many -``` - -Batch delete multiple RSS feeds. - -### Update Feed - -``` -PATCH /rss/update/{rss_id} -``` - -Update an RSS feed's configuration. - -### Refresh Feeds - -``` -GET /rss/refresh/all -``` - -Manually trigger a refresh of all RSS feeds. - -``` -GET /rss/refresh/{rss_id} -``` - -Refresh a specific RSS feed. - -### Get Torrents from Feed - -``` -GET /rss/torrent/{rss_id} -``` - -Get the list of torrents parsed from a specific RSS feed. - -### Analysis & Subscription - -``` -POST /rss/analysis -``` - -Analyze an RSS URL and extract anime metadata without subscribing. - -**Request Body:** -```json -{ - "url": "string" -} -``` - -``` -POST /rss/collect -``` - -Download all episodes from an RSS feed (for completed anime). - -``` -POST /rss/subscribe -``` - -Subscribe to an RSS feed for automatic ongoing downloads. - ---- +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/config/get` | The current configuration. Secret fields are masked | +| `PATCH` | `/config/update` | Save and reload the configuration. A masked secret field keeps its old value | +| `POST` | `/config/llm/models` | List the models of the selected LLM provider | +| `GET` | `/config/llm/providers` | List the LLM providers | +| `POST` | `/config/llm/providers/{provider_id}/install` | Install an LLM provider plugin | +| `DELETE` | `/config/llm/providers/{provider_id}` | Remove an LLM provider plugin | +| `POST` | `/config/llm/providers/{provider_id}/auth/begin`, `/auth/complete` | Authorization flow for subscription providers | +| `GET` | `/config/llm/providers/{provider_id}/auth/status` | Authorization state | +| `DELETE` | `/config/llm/providers/{provider_id}/auth` | Disconnect the authorization | + +## Bangumi + +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/bangumi/get/all` | All bangumi rules | +| `GET` | `/bangumi/get/{bangumi_id}` | One rule | +| `PATCH` | `/bangumi/update/{bangumi_id}` | Change a rule | +| `DELETE` | `/bangumi/delete/{bangumi_id}` | Delete a rule | +| `POST` | `/bangumi/delete/many` | Delete many rules | +| `POST` | `/bangumi/disable/{bangumi_id}`, `/bangumi/enable/{bangumi_id}` | Disable / enable a rule | +| `POST` | `/bangumi/disable/many` | Disable many rules | +| `PATCH` | `/bangumi/archive/{bangumi_id}`, `/bangumi/unarchive/{bangumi_id}` | Archive / unarchive | +| `PATCH` | `/bangumi/{bangumi_id}/weekday` | Set the broadcast weekday manually | +| `GET` | `/bangumi/refresh/poster/all`, `/bangumi/refresh/poster/{bangumi_id}` | Refresh posters | +| `GET` | `/bangumi/refresh/calendar` | Refresh the broadcast calendar | +| `GET` | `/bangumi/refresh/metadata` | Refresh TMDB metadata and archive series that have ended | +| `POST` | `/bangumi/reset/all` | Delete all rules | +| `GET` | `/bangumi/needs-review` | Bangumi whose episode offset needs a check | +| `GET` | `/bangumi/suggest-offset/{bangumi_id}` | Suggest an offset from the TMDB episode counts | +| `POST` | `/bangumi/detect-offset` | Find a season or episode mismatch with TMDB | +| `POST` | `/bangumi/apply-offset/{bangumi_id}`, `/bangumi/apply-offset/many` | Apply the suggested offset and start a rename pass | +| `POST` | `/bangumi/dismiss-review/{bangumi_id}` | Clear the "needs review" flag | +| `GET` / `DELETE` | `/bangumi/{bangumi_id}/torrents` | List / delete the torrent records of the bangumi | +| `DELETE` | `/bangumi/{bangumi_id}/torrents/{torrent_id}` | Delete one torrent record | +| `GET` / `DELETE` | `/bangumi/torrents/orphans` | List / delete torrent records that belong to no bangumi | +| `GET` | `/bangumi/torrents/orphans/count` | Number of orphan torrent records | +| `DELETE` | `/bangumi/torrents/orphans/{torrent_id}` | Delete one orphan torrent record | + +## Movies + +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/movie/get/all`, `/movie/get/{movie_id}` | All movie rules / one movie rule | +| `PATCH` | `/movie/update/{movie_id}` | Change | +| `DELETE` | `/movie/delete/{movie_id}` | Delete | +| `DELETE` | `/movie/disable/{movie_id}` | Disable | +| `GET` | `/movie/enable/{movie_id}` | Enable | + +## RSS feeds + +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/rss` | All feeds | +| `POST` | `/rss/add` | Add a feed | +| `PATCH` | `/rss/update/{rss_id}` | Change a feed | +| `DELETE` | `/rss/delete/{rss_id}` | Delete a feed | +| `POST` | `/rss/delete/many` | Delete many feeds | +| `PATCH` | `/rss/disable/{rss_id}` | Disable | +| `POST` | `/rss/disable/many`, `/rss/enable/many` | Disable / enable many feeds | +| `POST` | `/rss/refresh/all`, `/rss/refresh/{rss_id}` | Refresh now | +| `GET` | `/rss/torrent/{rss_id}` | Torrents of the feed | +| `POST` | `/rss/analysis` | Analyze an RSS link and return the bangumi that AB finds | +| `POST` | `/rss/collect` | Download a full season (collect) | +| `POST` | `/rss/subscribe` | Subscribe | ## Search -### Search Bangumi (Server-Sent Events) +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/search/bangumi?site=&keywords=` | Server-Sent Events. Sends the results one by one. Separate keywords with spaces | +| `GET` | `/search/provider` | Available search sites, including sites from plugins | +| `GET` / `PUT` | `/search/provider/config` | Read / save the search sites that the user configured | -``` -GET /search/bangumi?keyword={keyword}&provider={provider} +## Downloader + +AB 4.0 can have more than one downloader instance (`plugins.instances`). The torrent list includes all instances. Each item has a `downloader_id`. + +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/downloader/instances` | Downloader instances: `{"default": , "instances": [{"id", "provider"}]}` | +| `GET` | `/downloader/torrents` | Torrents of all instances. AB skips an instance that is not available | +| `POST` | `/downloader/torrents/pause`, `/resume`, `/delete` | Pause / resume / delete torrents | +| `POST` | `/downloader/torrents/tag` | Tag a torrent with a bangumi id | +| `POST` | `/downloader/torrents/tag/auto` | Tag all untagged torrents from their name and path | +| `GET` | `/downloader/rename-conflicts` | Rename conflicts that wait for the user | +| `POST` | `/downloader/rename-conflicts/{operation_id}/retry` | Clear one conflict. The next rename pass checks it again | + +## Notification center + +| Method | Path | Description | +| --- | --- | --- | +| `GET` / `DELETE` | `/notification/messages` | List / clear the in-app notifications | +| `GET` | `/notification/messages/unread-count` | Number of unread notifications | +| `POST` | `/notification/messages/read-all` | Mark all as read | +| `POST` | `/notification/messages/{message_id}/read` | Mark one as read | +| `DELETE` | `/notification/messages/{message_id}` | Delete one | +| `POST` | `/notification/test` | Test a saved notification channel by its index | +| `POST` | `/notification/test-config` | Test a channel configuration that is not saved | + +## Event stream + +`GET /events/stream` is one Server-Sent Events connection. The WebUI uses it instead of polling. + +| `event` | When AB sends it | `data` | +| --- | --- | --- | +| `status` | Each 3 seconds | The same structure as `GET /status` | +| `downloader` | Each 5 seconds | Torrents of all instances. `null` when the downloader is not available | +| `log` | Each 10 seconds | The end of the log | +| `update` | When an online update runs and its progress changes | Update progress | +| `notification` | When the connection opens, and when the notification center changes | Notification center state, with the unread count `unread_count` | +| `bus` | When an event goes on the event bus | `{"kind": , "payload": {...}}`, for host events and plugin events | + +```bash +curl -N -H "Authorization: Bearer $AB_TOKEN" http://your-host:7892/api/v1/events/stream ``` -Search for anime torrents. Returns results as a Server-Sent Events (SSE) stream for real-time updates. +For the event names and fields in `bus` frames, refer to [Plugin development → Events](/en/dev/plugins/events). Frontend plugin components use `host.events.on(kind, callback)` to get the same frames. -**Query Parameters:** -- `keyword` — Search keyword -- `provider` — Search provider (e.g., `mikan`, `nyaa`, `dmhy`) +## Plugins -**Response:** SSE stream with parsed search results. +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/plugins` | Plugins that AB found, their state, the config form schema and the current config (masked) | +| `PUT` | `/plugins/settings` | Change `allow_unsigned` (allow unsigned plugins) | +| `PUT` | `/plugins/{plugin_id}` | Enable / disable a plugin or change its config. The change applies at once. A bad config gets `422` | +| `GET` | `/plugins/providers` | Provider ids from plugins, grouped by extension point | +| `GET` | `/plugins/ui` | Frontend slots that enabled plugins declare | +| `GET` | `/plugins/catalog` | Plugins in the signed catalog. `502` if AB cannot get the catalog | +| `POST` | `/plugins/{plugin_id}/install` | Install or upgrade a plugin from the signed catalog, then enable it | +| `DELETE` | `/plugins/{plugin_id}` | Remove a plugin that came from the signed catalog | +| `GET` | `/plugins/{plugin_id}/web/{path}` | Static frontend files in the `web/` directory of the plugin | +| Any | `/plugins/{plugin_id}/{path}` | Routes of the plugin. Refer to [REST routes](/en/dev/plugins/points/api-router) | -### List Search Providers +## Setup wizard -``` -GET /search/provider -``` +These endpoints need no authentication. `GET /setup/status` is always available and returns `need_setup` (`false` after setup). The other endpoints are available only before the first setup is complete. After that, they return `403`. `/setup/complete` also needs a browser session, or an `admin` account that still has the factory password `adminadmin`. Otherwise it returns `403`. -Get the list of available search providers. +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/setup/status` | Tells if the setup wizard is necessary | +| `POST` | `/setup/test-downloader` | Test the downloader connection | +| `POST` | `/setup/test-rss` | Test an RSS link | +| `POST` | `/setup/test-notification` | Send a test notification | +| `POST` | `/setup/complete` | Save all wizard settings and mark the setup as complete | ---- +## MCP -## Program Control +AB provides an MCP server under `/mcp` at the root path (SSE transport). A client connects to `GET /mcp/sse` and sends messages to `POST /mcp/messages/`. -### Get Status +- Access control is different from the REST API. The client IP must be in `security.mcp_whitelist`, or the request must have a token with `scope=mcp` (`Authorization: Bearer `). If `mcp_whitelist` is empty, AB refuses all IP-based access. Tokens still work. +- Built-in tools: `list_anime`, `get_anime`, `search_anime`, `subscribe_anime`, `unsubscribe_anime`, `list_downloads`, `list_rss_feeds`, `get_program_status`, `refresh_feeds`, `update_anime`. +- Built-in resources: `autobangumi://anime/list`, `autobangumi://anime/{id}`, `autobangumi://status`, `autobangumi://rss/feeds`. +- A tool from an enabled plugin has the name `__`. A resource from a plugin has the URI `autobangumi://plugins//`. Refer to [MCP tools and resources](/en/dev/plugins/points/mcp). -``` -GET /status -``` +## Responses and errors -Get program status including version, running state, and first_run flag. - -**Response:** -```json -{ - "status": "running", - "version": "3.2.0", - "first_run": false -} -``` - -### Start Program - -``` -GET /start -``` - -Start the main program (RSS checking, downloading, renaming). - -### Restart Program - -``` -GET /restart -``` - -Restart the main program. - -### Stop Program - -``` -GET /stop -``` - -Stop the main program (WebUI remains accessible). - -### Shutdown - -``` -GET /shutdown -``` - -Shutdown the entire application (restarts the Docker container). - -### Check Downloader - -``` -GET /check/downloader -``` - -Test connectivity to the configured downloader (qBittorrent). - ---- - -## Downloader Management - -Manage torrents in the downloader directly from AutoBangumi. - -### List Torrents - -``` -GET /downloader/torrents -``` - -Get all torrents in the Bangumi category. - -### Pause Torrents - -``` -POST /downloader/torrents/pause -``` - -Pause torrents by hash. - -**Request Body:** -```json -{ - "hashes": ["hash1", "hash2"] -} -``` - -### Resume Torrents - -``` -POST /downloader/torrents/resume -``` - -Resume paused torrents by hash. - -**Request Body:** -```json -{ - "hashes": ["hash1", "hash2"] -} -``` - -### Delete Torrents - -``` -POST /downloader/torrents/delete -``` - -Delete torrents with optional file deletion. - -**Request Body:** -```json -{ - "hashes": ["hash1", "hash2"], - "delete_files": false -} -``` - ---- - -## Setup Wizard - -These endpoints are only available during first-run setup (before setup is complete). They do **not** require authentication. After setup completes, all endpoints return `403 Forbidden`. - -### Check Setup Status - -``` -GET /setup/status -``` - -Check if setup wizard is needed (first run). - -**Response:** -```json -{ - "need_setup": true -} -``` - -### Test Downloader Connection - -``` -POST /setup/test-downloader -``` - -Test connection to a downloader with provided credentials. - -**Request Body:** -```json -{ - "type": "qbittorrent", - "host": "172.17.0.1:8080", - "username": "admin", - "password": "adminadmin", - "ssl": false -} -``` - -### Test RSS Feed - -``` -POST /setup/test-rss -``` - -Validate an RSS feed URL is accessible and parseable. - -**Request Body:** -```json -{ - "url": "https://mikanime.tv/RSS/MyBangumi?token=xxx" -} -``` - -### Test Notification - -``` -POST /setup/test-notification -``` - -Send a test notification with provided settings. - -**Request Body:** -```json -{ - "type": "telegram", - "token": "bot_token", - "chat_id": "chat_id" -} -``` - -### Complete Setup - -``` -POST /setup/complete -``` - -Save all configuration and mark setup as complete. Creates the sentinel file `config/.setup_complete`. - -**Request Body:** Full configuration object. - ---- - -## Logs - -### Get Logs - -``` -GET /log -``` - -Retrieve the full application log file. - -### Clear Logs - -``` -GET /log/clear -``` - -Clear the log file. - ---- - -## Response Format - -All API responses follow a consistent format: - -```json -{ - "msg_en": "Success message in English", - "msg_zh": "Success message in Chinese", - "status": true -} -``` - -Error responses include appropriate HTTP status codes (400, 401, 403, 404, 500) with error messages in both languages. +- Many action endpoints return `{"status": true, "msg_en": "...", "msg_zh": "..."}`. Query endpoints return the data directly. For the exact structure, refer to `/docs`. +- Errors use standard HTTP status codes: `401` not authenticated, `403` not permitted, `404` not found, `422` validation failed, `500` server error. diff --git a/docs/en/dev/database.md b/docs/en/dev/database.md index 5720218f..5f6c860c 100644 --- a/docs/en/dev/database.md +++ b/docs/en/dev/database.md @@ -1,394 +1,146 @@ # Database Developer Guide -This guide covers the database architecture, models, and operations in AutoBangumi. +This page describes the data layer of the AutoBangumi backend: the engine, the `Database` facade, the repositories, the session rules and the schema migrations. The code is in `backend/src/module/database/`. ## Overview -AutoBangumi uses **SQLite** as its database with **SQLModel** (Pydantic + SQLAlchemy hybrid) for ORM. The database file is located at `data/data.db`. - -### Architecture +- The database is the SQLite file `data/data.db`. The ORM is SQLModel (Pydantic + SQLAlchemy). +- The whole application uses only the async engine `sqlite+aiosqlite`. Each connection sets `PRAGMA journal_mode=WAL`, `busy_timeout=5000` and `foreign_keys=ON`. Thus, more than one connection can write at the same time, and a write that collides waits instead of failing at once with `database is locked`. +- Foreign key constraints apply. Before you delete a row that other rows refer to, handle those rows first. For example, `BangumiDatabase.delete_all` deletes the torrent records first. ``` module/database/ -├── engine.py # SQLAlchemy engine configuration -├── combine.py # Database class, migrations, session management -├── bangumi.py # Bangumi (anime subscription) operations -├── rss.py # RSS feed operations -├── torrent.py # Torrent tracking operations -└── user.py # User authentication operations +├── __init__.py # Database, get_db (FastAPI dependency) +├── engine.py # async_engine, async_session_factory, connection PRAGMAs +├── combine.py # Database: one session plus all repositories +├── migrations.py # the MIGRATIONS table and the migration runner +├── bangumi.py, rss.py, torrent.py, movie.py, user.py, auth.py, +├── inbox.py, llm_credential.py, rename_operation.py, aria2.py, +└── plugin_kv.py, passkey.py # one repository for each table ``` -## Core Components +## Database and sessions -### Database Class - -The `Database` class in `combine.py` is the main entry point. It inherits from SQLModel's `Session` and provides access to all sub-databases: +`Database` is an async context manager. It creates one `AsyncSession` and attaches the repositories to itself. When the `async with` block ends, it closes the session. ```python from module.database import Database -with Database() as db: - # Access sub-databases - bangumis = db.bangumi.search_all() - rss_items = db.rss.search_active() - torrents = db.torrent.search_all() +async with Database() as db: + rules = await db.bangumi.search_all() + feeds = await db.rss.search_all() ``` -### Sub-Database Classes +| Attribute | Repository | Table | +| --- | --- | --- | +| `db.bangumi` | `BangumiDatabase` | `bangumi` | +| `db.rss` | `RSSDatabase` | `rssitem` | +| `db.torrent` | `TorrentDatabase` | `torrent` | +| `db.movie` | `MovieDatabase` | `movie` | +| `db.user` | `UserDatabase` | `user` | +| `db.auth` | `AuthDatabase` | `auth_session`, `api_token` | +| `db.inbox` | `InboxDatabase` | notification center | +| `db.llm_credential` | `LLMCredentialDatabase` | LLM subscription credentials | +| `db.rename_operation` | `RenameOperationDatabase` | `rename_operation` (rename conflicts and progress) | +| `db.aria2` | `Aria2GidDatabase` | `aria2_gid` | +| `db.plugin_kv` | `PluginKVDatabase` | `plugin_kv` (the `ctx.kv` of plugins) | -| Class | Model | Purpose | -|-------|-------|---------| -| `BangumiDatabase` | `Bangumi` | Anime subscription rules | -| `RSSDatabase` | `RSSItem` | RSS feed sources | -| `TorrentDatabase` | `Torrent` | Downloaded torrent tracking | -| `UserDatabase` | `User` | Authentication | +`PasskeyDatabase` is not attached to `Database`. When you need it, make it with `PasskeyDatabase(db.session)`. -## Models +`Database` also forwards some session methods: `add` (synchronous, it only queues the object), `commit`, `rollback`, `refresh` and `close`. On SQLite, `begin_write()` runs `BEGIN IMMEDIATE`. Use it when you read and then write and an invariant must stay true. Call it before the first access in the session. -### Bangumi Model +### Session rules -Core model for anime subscriptions: +**Use one session for each operation.** Do not keep a session or a `Database` on an object that lives longer than one request or one loop pass. `AppContext` holds no session. + +- Routes use dependency injection: + + ```python + from fastapi import Depends + from module.database import Database, get_db + + @router.get("/example") + async def example(db: Database = Depends(get_db)): + return await db.bangumi.search_all() + ``` + +- Scheduled loops and services open their own session for each run: `async with Database() as db:`. +- Services get their dependencies in the constructor: `RSSEngine(db)`, `TorrentManager(db)`. A service uses `self.db.`. A caller that needs only one repository uses `db.` directly. +- The session factory sets `expire_on_commit=False`. After a commit, you can still read the attributes of an object, but the object is not attached to a new session. To change it, query it again in the new session. + +## Repositories + +A repository takes an `AsyncSession`. All of its methods are `async def`, and most of them commit inside the method. When you add a query: + +- Put it in the repository of its table. Do not write SQL in a route or a service. +- For many rows, get them in one query with `in_`. Do not query row by row in a loop. +- Soft delete: `bangumi` and `movie` have a `deleted` field. To "disable" a rule, `disable_rule` sets `deleted` to `True`. Only `delete_one` removes the row. When you query active rules, filter on `deleted`. + +### More than one downloader + +4.0 supports more than one downloader instance (the `plugins.instances` config). `bangumi`, `movie`, `rssitem` and `torrent` all have a `downloader_id` column: + +- On rules and feeds, an empty value means "use the default instance" (`plugins.slots.downloader`). +- On a torrent record, it is the instance that has the torrent. Torrents that came from 3.3 have `default`. + +## Schema migrations + +The migrations are in the `MIGRATIONS` tuple in `migrations.py`. The runner works from this table. The `schema_version` table in the database records the applied version. ```python -class Bangumi(SQLModel, table=True): - id: int # Primary key - official_title: str # Display name (e.g., "Mushoku Tensei") - title_raw: str # Raw title for torrent matching (indexed) - season: int = 1 # Season number - episode_offset: int = 0 # Episode numbering adjustment - season_offset: int = 0 # Season numbering adjustment - rss_link: str # Comma-separated RSS feed URLs - filter: str # Exclusion filter (e.g., "720,\\d+-\\d+") - poster_link: str # TMDB poster URL - save_path: str # Download destination path - rule_name: str # qBittorrent RSS rule name - added: bool = False # Whether rule is added to downloader - deleted: bool = False # Soft delete flag (indexed) - archived: bool = False # For completed series (indexed) - needs_review: bool = False # Offset mismatch detected - needs_review_reason: str # Reason for review - suggested_season_offset: int # Suggested season offset - suggested_episode_offset: int # Suggested episode offset - air_weekday: int # Airing day (0=Sunday, 6=Saturday) +Migration( + 26, + "add downloader_id to bangumi, movie, rssitem and torrent", + (...SQL statements...), + already_applied=..., # guard: is this migration already in effect? + guarded_statements=(...), # optional: one guard for each statement +) ``` -### RSSItem Model +At startup, AB first runs `SQLModel.metadata.create_all`, then `run_migrations()`: -RSS feed subscriptions: +1. It reads `schema_version` and skips each migration whose version is not larger. +2. For each pending migration, it inspects the database again and calls the guard. If the migration is already in effect (for example, `create_all` made a new database from the models), it only records the version. +3. Otherwise, it runs the statements in one SAVEPOINT. If a statement fails, the migration rolls back and the error goes up. The whole migration transaction rolls back and AB does not start. AB never runs on a schema that is only half migrated. +4. If a migration ran, at the end it fills `NULL` values in existing rows with the model defaults (`fill_null_with_defaults_conn`). It skips primary keys, `default_factory` fields and optional fields whose default is `None`. + +### Add a migration + +1. Change the model in `module/models/` first. A new database gets the new structure from `create_all`. +2. Append a `Migration` to the end of `MIGRATIONS`. Its version is the previous version plus 1. `CURRENT_SCHEMA_VERSION` comes from the list. Do not edit it manually. +3. Give an `already_applied` guard: `column_exists(table, column)`, `table_exists(table)` or `index_exists(table, index)`. To combine conditions, use `all_checks(...)`. If one migration changes many tables and each table can be in a different state, use `guarded_statements` to give each statement its own guard. +4. Add a test for the migration in `backend/src/test/`: start from the old schema, run the migration and check the result. ```python -class RSSItem(SQLModel, table=True): - id: int # Primary key - name: str # Display name - url: str # Feed URL (unique, indexed) - aggregate: bool = True # Whether to parse torrents - parser: str = "mikan" # Parser type: mikan, dmhy, nyaa - enabled: bool = True # Active flag - connection_status: str # "healthy" or "error" - last_checked_at: str # ISO timestamp - last_error: str # Last error message +Migration( + 27, + "add note to bangumi", + ("ALTER TABLE bangumi ADD COLUMN note TEXT DEFAULT ''",), + column_exists("bangumi", "note"), +) ``` -### Torrent Model +## Tests -Tracks downloaded torrents: +`backend/src/test/conftest.py` provides an in-memory database: + +- `db_engine`: `sqlite+aiosqlite://` with `StaticPool`. Each test gets a new one, with tables made from the models. +- `db_session`: an `AsyncSession` bound to `db_engine`. + +To test a repository directly, use `Database(engine=db_engine)`, or make one repository from `db_session`: ```python -class Torrent(SQLModel, table=True): - id: int # Primary key - name: str # Torrent name (indexed) - url: str # Torrent/magnet URL (unique, indexed) - rss_id: int # Source RSS feed ID - bangumi_id: int # Linked Bangumi ID (nullable) - qb_hash: str # qBittorrent info hash (indexed) - downloaded: bool = False # Download completed +from module.database import Database +from module.database.bangumi import BangumiDatabase +from test.factories import make_bangumi + +async def test_add_bangumi(db_engine): + async with Database(engine=db_engine) as db: + assert await db.bangumi.add(make_bangumi()) + +async def test_search(db_session): + repo = BangumiDatabase(db_session) + assert await repo.search_all() == [] ``` -## Common Operations - -### BangumiDatabase - -```python -with Database() as db: - # Create - db.bangumi.add(bangumi) # Single insert - db.bangumi.add_all(bangumi_list) # Batch insert (deduplicates) - - # Read - db.bangumi.search_all() # All records (cached, 5min TTL) - db.bangumi.search_id(123) # By ID - db.bangumi.match_torrent("torrent name") # Find by title_raw match - db.bangumi.not_complete() # Incomplete series - db.bangumi.get_needs_review() # Flagged for review - - # Update - db.bangumi.update(bangumi) # Update single record - db.bangumi.update_all(bangumi_list) # Batch update - - # Delete - db.bangumi.delete_one(123) # Hard delete - db.bangumi.disable_rule(123) # Soft delete (deleted=True) -``` - -### RSSDatabase - -```python -with Database() as db: - # Create - db.rss.add(rss_item) # Single insert - db.rss.add_all(rss_items) # Batch insert (deduplicates) - - # Read - db.rss.search_all() # All feeds - db.rss.search_active() # Enabled feeds only - db.rss.search_aggregate() # Enabled + aggregate=True - - # Update - db.rss.update(id, rss_update) # Partial update - db.rss.enable(id) # Enable feed - db.rss.disable(id) # Disable feed - db.rss.enable_batch([1, 2, 3]) # Batch enable - db.rss.disable_batch([1, 2, 3]) # Batch disable -``` - -### TorrentDatabase - -```python -with Database() as db: - # Create - db.torrent.add(torrent) # Single insert - db.torrent.add_all(torrents) # Batch insert - - # Read - db.torrent.search_all() # All torrents - db.torrent.search_by_qb_hash(hash) # By qBittorrent hash - db.torrent.search_by_url(url) # By URL - db.torrent.check_new(torrents) # Filter out existing - - # Update - db.torrent.update_qb_hash(id, hash) # Set qb_hash -``` - -## Caching - -### Bangumi Cache - -`search_all()` results are cached at the module level with a 5-minute TTL: - -```python -# Module-level cache in bangumi.py -_bangumi_cache: list[Bangumi] | None = None -_bangumi_cache_time: float = 0 -_BANGUMI_CACHE_TTL: float = 300.0 # 5 minutes - -# Cache invalidation -def _invalidate_bangumi_cache(): - global _bangumi_cache, _bangumi_cache_time - _bangumi_cache = None - _bangumi_cache_time = 0 -``` - -**Important:** The cache is automatically invalidated on: -- `add()`, `add_all()` -- `update()`, `update_all()` -- `delete_one()`, `delete_all()` -- `archive_one()`, `unarchive_one()` -- Any RSS link update operations - -### Session Expunge - -Cached objects are **expunged** from the session to prevent `DetachedInstanceError`: - -```python -for b in bangumis: - self.session.expunge(b) # Detach from session -``` - -## Migration System - -### Schema Versioning - -Migrations are tracked via a `schema_version` table: - -```python -CURRENT_SCHEMA_VERSION = 7 - -# Each migration: (version, description, [SQL statements]) -MIGRATIONS = [ - (1, "add air_weekday column", [...]), - (2, "add connection status columns", [...]), - (3, "create passkey table", [...]), - (4, "add archived column", [...]), - (5, "rename offset to episode_offset", [...]), - (6, "add qb_hash column", [...]), - (7, "add suggested offset columns", [...]), -] -``` - -### Adding a New Migration - -1. Increment `CURRENT_SCHEMA_VERSION` in `combine.py` -2. Add migration tuple to `MIGRATIONS` list: - -```python -MIGRATIONS = [ - # ... existing migrations ... - ( - 8, - "add my_new_column to bangumi", - [ - "ALTER TABLE bangumi ADD COLUMN my_new_column TEXT DEFAULT NULL", - ], - ), -] -``` - -3. Add idempotency check in `run_migrations()`: - -```python -if "bangumi" in tables and version == 8: - columns = [col["name"] for col in inspector.get_columns("bangumi")] - if "my_new_column" in columns: - needs_run = False -``` - -4. Update the corresponding Pydantic model in `module/models/` - -### Default Value Backfill - -After migrations, `_fill_null_with_defaults()` automatically fills NULL values based on model defaults: - -```python -# If model defines: -class Bangumi(SQLModel, table=True): - my_field: bool = False - -# Then existing rows with NULL will be updated to False -``` - -## Performance Patterns - -### Batch Queries - -`add_all()` uses a single query to check for duplicates instead of N queries: - -```python -# Efficient: single SELECT -keys_to_check = [(d.title_raw, d.group_name) for d in datas] -conditions = [ - and_(Bangumi.title_raw == tr, Bangumi.group_name == gn) - for tr, gn in keys_to_check -] -statement = select(Bangumi.title_raw, Bangumi.group_name).where(or_(*conditions)) -``` - -### Regex Matching - -`match_list()` compiles a single regex pattern for all title matches: - -```python -# Compile once, match many -sorted_titles = sorted(title_index.keys(), key=len, reverse=True) -pattern = "|".join(re.escape(title) for title in sorted_titles) -title_regex = re.compile(pattern) - -# O(1) lookup per torrent instead of O(n) -for torrent in torrent_list: - match = title_regex.search(torrent.name) -``` - -### Indexed Columns - -The following columns have indexes for fast lookups: - -| Table | Column | Index Type | -|-------|--------|------------| -| `bangumi` | `title_raw` | Regular | -| `bangumi` | `deleted` | Regular | -| `bangumi` | `archived` | Regular | -| `rssitem` | `url` | Unique | -| `torrent` | `name` | Regular | -| `torrent` | `url` | Unique | -| `torrent` | `qb_hash` | Regular | - -## Testing - -### Test Database Setup - -Tests use an in-memory SQLite database: - -```python -# conftest.py -@pytest.fixture -def db_engine(): - engine = create_engine("sqlite:///:memory:") - SQLModel.metadata.create_all(engine) - yield engine - engine.dispose() - -@pytest.fixture -def db_session(db_engine): - with Session(db_engine) as session: - yield session -``` - -### Factory Functions - -Use factory functions for creating test data: - -```python -from test.factories import make_bangumi, make_torrent, make_rss_item - -def test_bangumi_search(): - bangumi = make_bangumi(title_raw="Test Title", season=2) - # ... test logic -``` - -## Design Notes - -### No Foreign Keys - -SQLite foreign key enforcement is disabled by default. Relationships (like `Torrent.bangumi_id`) are managed in application logic rather than database constraints. - -### Soft Deletes - -The `Bangumi.deleted` flag enables soft deletes. Queries should filter by `deleted=False` for user-facing data: - -```python -statement = select(Bangumi).where(Bangumi.deleted == false()) -``` - -### Torrent Tagging - -Torrents are tagged in qBittorrent with `ab:{bangumi_id}` for offset lookup during rename operations. This enables fast bangumi identification without database queries. - -## Common Issues - -### DetachedInstanceError - -If you access cached objects from a different session: - -```python -# Wrong: accessing cached object in new session -bangumis = db.bangumi.search_all() # Cached -with Database() as new_db: - new_db.session.add(bangumis[0]) # Error! - -# Right: objects are expunged, work independently -bangumis = db.bangumi.search_all() -bangumis[0].title_raw = "New Title" # OK, but won't persist -``` - -### Cache Staleness - -If manual SQL updates bypass the ORM, invalidate the cache: - -```python -from module.database.bangumi import _invalidate_bangumi_cache - -with engine.connect() as conn: - conn.execute(text("UPDATE bangumi SET ...")) - conn.commit() - -_invalidate_bangumi_cache() # Important! -``` +The backend pytest config sets `asyncio_mode = "auto"`, so async test functions need no extra decorator. To run: `cd backend && uv run pytest src/test/test_xxx.py -v`. diff --git a/docs/en/dev/e2e-test-guide.md b/docs/en/dev/e2e-test-guide.md new file mode 100644 index 00000000..28ce1de6 --- /dev/null +++ b/docs/en/dev/e2e-test-guide.md @@ -0,0 +1,62 @@ +# E2E Test Guide + +The end-to-end tests run a production-built AutoBangumi image against local, deterministic upstream services (mock RSS, TMDB, player and qBittorrent). The tests do not use public services. Docker Compose publishes random ports on the loopback address. The services talk to each other on an internal network. + +The full guide (all test lanes, diagnostic artifacts, how to add a mock scenario, how to update the qBittorrent image digest) is [`e2e/README.md`](https://github.com/EstrellaXD/Auto_Bangumi/blob/main/e2e/README.md) in the repository. This page gives only the shortest path to run the tests locally. + +## Prerequisites + +- Docker Engine with Compose v2 +- Python 3.13 and `uv` +- Node.js 20 and pnpm 9.11 + +```bash +uv sync --directory backend --locked --group dev +pnpm --dir webui install --frozen-lockfile +pnpm --dir webui exec playwright install --with-deps chromium webkit firefox +``` + +## Build the test image + +```bash +pnpm --dir webui run build +python3 e2e/scripts/build_test_image.py \ + --version 3.3.999-e2e.1 \ + --image auto-bangumi:e2e \ + --dist webui/dist +``` + +The build script puts the WebUI build and the generated version module in a temporary Docker context. It does not write them into the source tree. + +## Run + +Use a new project name and a new work directory for each run. If you use a work directory again, you also use its setup state again, and the first-run coverage is lost. + +```bash +export LANE=runtime +export AB_E2E_PROJECT="ab-e2e-local-$LANE-$$" +export AB_E2E_WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ab-e2e-$LANE.XXXXXX")" +export AB_E2E_APP_IMAGE=auto-bangumi:e2e + +# Runtime API and process tests +python3 e2e/scripts/stack.py run \ + --profile browser \ + --project-name "$AB_E2E_PROJECT" \ + --work-dir "$AB_E2E_WORK_DIR" \ + -- uv run --directory backend pytest src/test/e2e/runtime -m e2e -q +``` + +| Lane | Command after `--` | `--profile` | +| --- | --- | --- | +| Runtime | `uv run --directory backend pytest src/test/e2e/runtime -m e2e -q` | `browser` | +| Chromium desktop | `pnpm --dir webui run test:e2e:chromium --retries=0` | `browser` | +| WebKit mobile | `pnpm --dir webui run test:e2e:webkit --retries=0` | `browser` | +| Real qBittorrent and packaged image | `python3 e2e/scripts/run_downloader_lane.py` | `downloader` | + +- A normal `pytest` run skips the E2E tests. You must give `-m e2e`. +- `stack.py run` collects diagnostics before it removes the Compose stack. It writes them to `$AB_E2E_WORK_DIR/artifacts`. Playwright writes screenshots and videos to `webui/test-results`, and the HTML report to `webui/playwright-report`. +- To stop a stack: `python3 e2e/scripts/stack.py stop --profile browser --project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"`. + +## CI + +`.github/workflows/e2e.yml` runs the runtime lane, the browser lanes (Chromium, WebKit mobile) and the real-downloader lane on each PR. `e2e-nightly.yml` runs the Firefox lane each night. It also runs the browser lane and the downloader lane two times each, as separate passes. The faster contract tests for endpoint structure and validation stay in `backend/src/test/test_api_*.py`. diff --git a/docs/en/dev/index.md b/docs/en/dev/index.md index 020f99db..a6633e54 100644 --- a/docs/en/dev/index.md +++ b/docs/en/dev/index.md @@ -128,7 +128,12 @@ The development team will review contributor PRs and discuss or approve merging ## Release Process -Releases are currently triggered automatically after the development team manually merges a specific "release PR". +Only a version tag that a maintainer pushes manually starts a release. A merged PR does not start a release. + +- **Beta**: a tag of the form `X.Y.Z-beta.N` (for example `4.0.0-beta.1`) on a development branch. CI builds the Docker image with two tags, the version and `dev-latest`. It also creates a pre-release on GitHub. +- **Stable**: first a PR merges the development branch into `main`. Then a maintainer puts a tag of the form `X.Y.Z` on the merge commit on `main`. CI checks the tag format and makes sure that the commit is on `main`. Then it builds the image with two tags, the version and `latest`. It creates the GitHub release with the release notes from `docs/changelog/.md`. + +The attachments of each release include the WebUI, the online update bundle, the plugin SDK wheel and the plugin-development skill (refer to [Get the SDK](/en/dev/plugins/sdk)). Bug fix PRs are typically released quickly, usually within a week. diff --git a/docs/en/dev/plugins.md b/docs/en/dev/plugins.md index be0913cc..4e973709 100644 --- a/docs/en/dev/plugins.md +++ b/docs/en/dev/plugins.md @@ -8,23 +8,25 @@ A plugin is a Python package, with optional frontend components. It runs inside ## Five-minute start -1. Install the SDK and the command line. The wheel is an attachment of each 4.0 beta and stable release on [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases). The file name is `autobangumi_sdk--py3-none-any.whl`. The wheel is not on PyPI. +1. Install the SDK and the command line. The wheel is an attachment of each 4.0 beta and stable release on [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases). The file name is `autobangumi_sdk--py3-none-any.whl`. The wheel is not on PyPI. Refer to [Get the SDK](/en/dev/plugins/sdk). ```bash uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl ``` -2. Create a skeleton and run its tests: +2. Create a skeleton and run its tests. The `pyproject.toml` of the skeleton needs `autobangumi-sdk`. This package is not on PyPI. Thus, first use `uv add` to point the dependency to the wheel that you downloaded: ```bash ab-plugin new my-rename --kind rename # or notifier, search - cd my-rename && uv run pytest # the skeleton includes a contract test + cd my-rename + uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # use the real path of the wheel + uv run pytest # the skeleton includes a contract test ``` -3. Link the plugin to your local AutoBangumi and enable hot reload. Start AB one time first, so that a config file exists in `config/`: +3. Link the plugin to your local AutoBangumi and enable hot reload. Start AB one time first, so that a config file exists in its config directory. Set `--config-dir` to this directory (`backend/src/config` when you run AB from source): ```bash - ab-plugin dev . + ab-plugin dev . --config-dir /path/to/autobangumi/config ``` Restart AB one time. After that, AB reloads the plugin when you change its files. @@ -34,6 +36,8 @@ A plugin is a Python package, with optional frontend components. It runs inside **Basics** +- [Get the SDK](/en/dev/plugins/sdk): the wheel and the plugin-author skill on the GitHub release + - [Concepts](/en/dev/plugins/concepts): `Plugin`, `ctx`, the three extension declarations, the manifest, isolation and the circuit breaker - [Config forms](/en/dev/plugins/config-forms): how `config_model` becomes a WebUI form - [Events](/en/dev/plugins/events): system events, organize events and custom events diff --git a/docs/en/dev/plugins/cli.md b/docs/en/dev/plugins/cli.md index ed5c1120..65451080 100644 --- a/docs/en/dev/plugins/cli.md +++ b/docs/en/dev/plugins/cli.md @@ -7,7 +7,7 @@ uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl # or in a project: uv add ./autobangumi_sdk-0.5.0-py3-none-any.whl ``` -The wheel is an attachment of the 4.0 beta and stable GitHub releases. It is not on PyPI. The contract tests need pytest: `uv add --dev pytest` (or install `autobangumi-sdk[test]`). +The wheel is an attachment of the 4.0 beta and stable GitHub releases. It is not on PyPI. Refer to [Get the SDK](/en/dev/plugins/sdk). The contract tests need pytest. The `pyproject.toml` that `new` creates already has pytest in its `dev` dependency group. For a project that you make yourself, use `uv add --dev pytest` (or install `autobangumi-sdk[test]`). ## new @@ -28,6 +28,8 @@ Creates the directory `/`: The `id` can contain only lowercase letters, digits and hyphens. The command refuses if the directory exists. There is no downloader skeleton. A downloader needs a real backend, so a skeleton that passes the contract has no value. +The `pyproject.toml` that `new` creates needs `autobangumi-sdk`. This package is not on PyPI. Before you run `uv run pytest`, run `uv add ` one time in the plugin directory. Then uv uses the local wheel for this dependency. + ## validate ```bash @@ -42,7 +44,7 @@ Checks the manifest fields, that the `sdk` range includes the current SDK, that ab-plugin pack [path] [-o dist] ``` -Validates, then creates `dist/-.zip`. The content is at the root of the zip, the same layout as a signed-catalog package. `tests/`, `pyproject.toml`, `uv.lock`, caches and `dist/` are excluded. File order and timestamps are fixed, so the same content gives the same sha256. +Validates, then creates `dist/-.zip`. The content is at the root of the zip, the same layout as a signed-catalog package. `tests/`, `pyproject.toml`, `uv.lock`, `dist/`, `.venv`, `.git`, `node_modules`, caches and `.pyc` files are excluded. File order and timestamps are fixed, so the same content gives the same sha256. ## dev @@ -50,7 +52,9 @@ Validates, then creates `dist/-.zip`. The content is at the root of ab-plugin dev [path] [--config-dir config] ``` -- Links the plugin directory to `/plugins/local/` with a symlink. +- Validates, then links the plugin directory to `/plugins/local/` with a symlink. +- The default `--config-dir` is `config` in the current directory. When you run the command in the plugin directory, set it to the config directory of AB (`backend/src/config` when you run AB from source). +- The symlink points to the absolute path of the plugin directory. AB must be able to open that same path, for example when you run AB from source. When AB runs in Docker, copy or mount the plugin directory to `config/plugins/local//` in the container. Then turn on "Allow unsigned plugins" in Settings → Plugins and enable the plugin. - Writes `plugins.dev_mode`, `plugins.allow_unsigned` and `plugins.enabled.` in the host config file (`config_dev.json` first, then `config.json`). - Refuses if no config file exists. Start AutoBangumi one time first. - A running AB does not re-read the config file. Restart it one time. After that, `dev_mode` checks the local plugin directories each second. When a file changes, AB reloads that plugin. AB also watches a plugin that failed to load, so it recovers after you fix the source. @@ -61,8 +65,9 @@ ab-plugin dev [path] [--config-dir config] ```bash ab-plugin new my-notify --kind notifier cd my-notify +uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # one time only uv run pytest # contract tests -ab-plugin dev . # link and enable hot reload; restart AB one time +ab-plugin dev . --config-dir /path/to/autobangumi/config # link and enable hot reload; restart AB one time # change code -> AB reloads -> check in the WebUI ab-plugin pack . ``` diff --git a/docs/en/dev/plugins/examples.md b/docs/en/dev/plugins/examples.md index 26bff16b..e630ba6f 100644 --- a/docs/en/dev/plugins/examples.md +++ b/docs/en/dev/plugins/examples.md @@ -21,10 +21,12 @@ Turn on "Allow unsigned plugins" in Settings → Plugins, then enable the plugin ## Run the tests of an example +Run the script in the repository root. It validates the manifest of each example and runs its tests, the same as CI: + ```bash -cd examples/plugins/ntfy-notifier && uv run pytest -# or run all examples (in the repository root): scripts/test_example_plugins.sh ``` +The script runs in the `backend/` environment, which has an editable install of the SDK. The `pyproject.toml` of each example needs `autobangumi-sdk`, and this package is not on PyPI. To test an example outside the repository, first run `uv add ` in the example directory. Then run `uv run pytest`. + You do not need a plugin to refresh a Jellyfin, Emby or Plex library. The built-in plugin `media-server-refresh` does it. diff --git a/docs/en/dev/plugins/points/message-template.md b/docs/en/dev/plugins/points/message-template.md index 75a1b983..ab1f260f 100644 --- a/docs/en/dev/plugins/points/message-template.md +++ b/docs/en/dev/plugins/points/message-template.md @@ -21,5 +21,5 @@ class Alerts(Plugin): - The hook affects only the external push. The frontend renders the text in the notification center from `i18n()`, so it does not change. - AB calls the hook one time for each enabled channel. `channel` is the channel type, for example `telegram`. -- If a hook fails or times out, AB keeps the text of the previous step, and the failure counts toward the breaker. If the return value has the wrong type, the whole message goes back to the default text. +- If a hook fails, times out or returns a value that is not a `RenderedMessage`, AB keeps the text of the previous step. The failure counts toward the breaker. The next hooks run as usual. - "New episode" notifications do not pass through this hook yet. They still use the single-episode template of the notification channel. diff --git a/docs/en/dev/plugins/sdk.md b/docs/en/dev/plugins/sdk.md new file mode 100644 index 00000000..09900d13 --- /dev/null +++ b/docs/en/dev/plugins/sdk.md @@ -0,0 +1,80 @@ +# Get the SDK + +The plugin SDK is not on PyPI. Each 4.0 beta and stable [GitHub release](https://github.com/EstrellaXD/Auto_Bangumi/releases) has two attachments for plugin authors: + +| File | Content | +| --- | --- | +| `autobangumi_sdk--py3-none-any.whl` | The `ab_sdk` package and the `ab-plugin` command | +| `autobangumi-plugin-skill-.zip` | A plugin-development skill for AI coding assistants | + +The wheel file name contains the SDK version (`ab_sdk.SDK_VERSION`, for example `0.5.0`). It does not contain the AB version. Download the wheel from the release of your target AB version, so that the two SDK versions are the same. The `sdk` range in the plugin manifest (for example `">=0.5,<1"`) must include this version. + +## Install the wheel + +The wheel needs Python 3.13 or later. It depends on `pydantic`, `httpx` and `packaging`. You do not need to install AutoBangumi. + +To use only the command line, install the wheel as a global tool: + +```bash +uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl +ab-plugin --help +``` + +To write a plugin and run its tests, add the wheel as a dependency of the plugin project: + +```bash +cd my-plugin +uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl +uv run pytest +``` + +`uv add` writes the local path of the wheel in `[tool.uv.sources]` of `pyproject.toml`. `ab-plugin pack` does not pack `pyproject.toml` or `uv.lock`, so this path does not go into the plugin package. The `pyproject.toml` that `ab-plugin new` creates already needs `autobangumi-sdk`, but it gives no source. Thus, run `uv add` one time in a new skeleton too. + +The contract tests need pytest. The `dev` dependency group of the skeleton already has pytest. You can also install the optional dependency `autobangumi-sdk[test]`. + +To upgrade the SDK, download the wheel of the new release. Then run `uv tool install --force ` or `uv add ` again. + +## Develop in the AB repository + +The SDK source is in `backend/src/ab_sdk`. The package configuration is in `backend/sdk/pyproject.toml`. The `dev` dependency group of `backend` installs it in editable mode, so you do not need the wheel in the repository: + +```bash +cd backend +uv sync --group dev +uv run ab-plugin validate ../examples/plugins/ntfy-notifier +``` + +To build the wheel yourself (the same command as CI): + +```bash +uv build --wheel backend/sdk --out-dir sdk-dist +``` + +## The plugin-development skill + +The skill is a set of Markdown instructions. An AI coding assistant reads them to write, test and pack AB plugins. The zip contains the directory `autobangumi-plugin/`: + +``` +autobangumi-plugin/ +├── SKILL.md # workflow, extension point table, frequent errors +└── references/ + ├── extension-points.md # signature and fields of each extension point + ├── frontend.md # frontend slots + └── testing.md # contract tests and create_plugin +``` + +Unpack it in the skills directory of your assistant. For Claude Code, the personal skills directory is `~/.claude/skills/`: + +```bash +unzip autobangumi-plugin-skill-.zip -d ~/.claude/skills/ +``` + +For other assistants, use their skills directory. The source of the skill is `skills/autobangumi-plugin/` in the repository. + +## How CI builds the files + +The `release` job of `.github/workflows/build.yml` runs only when a maintainer pushes a version tag: + +1. `uv build --wheel backend/sdk --out-dir sdk-dist` builds the wheel. The version comes from `ab_sdk.SDK_VERSION`. +2. The job zips `skills/autobangumi-plugin` into `autobangumi-plugin-skill-.zip`. +3. The job attaches the two files to the GitHub release of that version, together with the WebUI, the online update bundle and other files. Beta (pre-release) and stable releases both have the two files. diff --git a/docs/en/dev/plugins/signing.md b/docs/en/dev/plugins/signing.md index 124192b3..bbac7816 100644 --- a/docs/en/dev/plugins/signing.md +++ b/docs/en/dev/plugins/signing.md @@ -49,11 +49,7 @@ After these checks, AB unpacks the plugin to `config/plugins///`, w A catalog plugin can depend only on the standard library and on packages that AB already has. AB does not run pip. Put third-party pure-Python libraries in `vendor/`. -::: warning The WebUI has no catalog page yet -During the 4.0 preview, you can browse and install the catalog with the API only. The Settings → Plugins page has no buttons for it. -::: - -API: +In the WebUI, the "Plugin catalog" section of **Settings → Plugins** lets you browse the catalog and install, update and remove plugins. API: | Request | Description | | --- | --- | @@ -86,4 +82,4 @@ A user sees the new version through `GET /api/v1/plugins/catalog`. ## Publish the SDK -The SDK is a wheel in the attachments of the 4.0 beta and stable GitHub releases. CI builds it with `uv build --wheel backend/sdk`. The same release has `autobangumi-plugin-skill-.zip`. It is a plugin-development skill for AI coding assistants. Unpack it in the skills directory of the assistant. +CI builds the SDK wheel and the plugin-author skill. It attaches them to each 4.0 beta and stable GitHub release. Maintainers do not publish them manually. To download and install them, refer to [Get the SDK](/en/dev/plugins/sdk). diff --git a/docs/ja/api/index.md b/docs/ja/api/index.md index 52b6cbde..b5208c6b 100644 --- a/docs/ja/api/index.md +++ b/docs/ja/api/index.md @@ -1,619 +1,221 @@ -# REST APIリファレンス +# REST API リファレンス -AutoBangumiは`/api/v1`でREST APIを公開しています。すべてのエンドポイント(ログインとセットアップを除く)はJWT認証が必要です。 +AutoBangumi は `/api/v1` 以下で REST API を提供します。WebUI も同じ API を使います。 -**ベースURL:** `http://your-host:7892/api/v1` +**ベース URL:** `http://your-host:7892/api/v1` -**認証:** JWTトークンをCookieまたは`Authorization: Bearer `ヘッダーとして含めてください。 - -**インタラクティブドキュメント:** 開発モードで実行している場合、Swagger UIは`http://your-host:7892/docs`で利用可能です。 - ---- +**対話式ドキュメント:** このページにはエンドポイントと用途だけを載せています。リクエストとレスポンスのフィールドは、動作中のインスタンスの `http://your-host:7892/docs`(FastAPI がコードから生成する Swagger UI)を参照してください。プラグインのルートは `/docs` に表示されません。 ## 認証 -### ログイン +AB は 2 種類の認証情報を受け付けます。 -``` -POST /auth/login +- **ブラウザセッション**:`POST /auth/login` にフォームフィールド `username`、`password` を送ってログインします。成功すると AB は HttpOnly Cookie `token` を設定します。WebUI はこの方法を使います。 +- **API トークン**:設定 → ユーザーとアクセス制御 → API トークン で `scope=api` のトークンを作成します(または `POST /tokens` を呼びます)。平文は一度だけ表示されます。リクエストには `Authorization: Bearer <トークン>` を付けます。 + +```bash +curl -H "Authorization: Bearer $AB_TOKEN" http://your-host:7892/api/v1/status ``` -ユーザー名とパスワードで認証します。 +- リクエストに `Authorization` ヘッダーがある場合、AB はそのトークンだけを検証し、Cookie は見ません。 +- 認証情報がない、または無効な場合は `401` を返します。 +- アカウント管理のエンドポイント(`/auth/update`、`/users`、`/tokens`、Passkey の登録と管理)はブラウザセッションだけを受け付けます。API トークンでは `403` を返します。 +- 次のエンドポイントは認証不要です:`/auth/login`、`/passkey/auth/*`、セットアップウィザード `/setup/*`(`GET /setup/status` は常に使えます。その他はセットアップ完了後 `403`)、ルートパスの `/health`。 +- `/auth/refresh_token` と `/auth/logout` は `Authorization` ヘッダーを見ず、セッション Cookie だけを使います。`refresh_token` は有効な Cookie がないと `401` を返します。ログインと Passkey ログインは `security.login_whitelist` で制限されます。 +- ローカル開発では、環境変数 `AB_DEV_NO_AUTH=1` を設定するとすべての認証を省略します。本番環境では設定しないでください。 -**リクエストボディ:** -```json -{ - "username": "string", - "password": "string" -} -``` +## 認証とアカウント -**レスポンス:** JWTトークン付きの認証Cookieを設定します。 +| メソッド | パス | 説明 | +| --- | --- | --- | +| `POST` | `/auth/login` | ユーザー名とパスワードでログインし、セッション Cookie を設定 | +| `POST` | `/auth/refresh_token` | 現在のセッションを延長 | +| `POST` | `/auth/logout` | 現在のセッションを終了し、Cookie を削除 | +| `GET` | `/auth/me` | 現在のユーザー | +| `POST` | `/auth/update` | 現在のアカウントを変更し、そのすべてのセッションを更新 | +| `GET` / `POST` | `/users` | ユーザーの一覧 / 作成 | +| `PATCH` / `DELETE` | `/users/{user_id}` | ユーザーの変更 / 削除 | +| `GET` / `POST` | `/tokens` | API トークンの一覧 / 作成(`scope` は `api` または `mcp`、`expires_at` は任意) | +| `DELETE` | `/tokens/{token_id}` | トークンを無効化 | +| `POST` | `/passkey/register/options`、`/passkey/register/verify` | Passkey を登録 | +| `POST` | `/passkey/auth/options`、`/passkey/auth/verify` | Passkey でログイン | +| `GET` | `/passkey/list` | 現在のユーザーの Passkey 一覧 | +| `POST` | `/passkey/delete` | Passkey を削除 | -### トークンのリフレッシュ +## プログラム -``` -GET /auth/refresh_token -``` - -現在の認証トークンをリフレッシュします。 - -### ログアウト - -``` -GET /auth/logout -``` - -認証Cookieをクリアしてログアウトします。 - -### 認証情報の更新 - -``` -POST /auth/update -``` - -ユーザー名および/またはパスワードを更新します。 - -**リクエストボディ:** -```json -{ - "username": "string", - "password": "string" -} -``` - ---- - -## Passkey / WebAuthn - -WebAuthn/FIDO2 Passkeysを使用したパスワードレス認証。 - -### Passkeyの登録 - -``` -POST /passkey/register/options -``` - -WebAuthn登録オプション(チャレンジ、リライングパーティ情報)を取得します。 - -``` -POST /passkey/register/verify -``` - -ブラウザからのPasskey登録レスポンスを検証して保存します。 - -### Passkeyで認証 - -``` -POST /passkey/auth/options -``` - -WebAuthn認証チャレンジオプションを取得します。 - -``` -POST /passkey/auth/verify -``` - -Passkey認証レスポンスを検証し、JWTトークンを発行します。 - -### Passkeyの管理 - -``` -GET /passkey/list -``` - -現在のユーザーの登録済みPasskeyをすべてリストします。 - -``` -POST /passkey/delete -``` - -クレデンシャルIDで登録済みPasskeyを削除します。 - ---- +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/status` | バージョン、実行状態、初回起動かどうか | +| `POST` | `/start`、`/stop`、`/restart` | バックグラウンドタスクの開始 / 停止 / 再起動 | +| `POST` | `/shutdown` | プログラムを終了 | +| `GET` | `/check/downloader` | 既定のダウンローダーが使えるか確認 | +| `GET` | `/log` | ログを読む | +| `POST` | `/log/clear` | ログを消去 | +| `GET` | `/update/check` | 最新バージョンとオンライン更新の状態 | +| `POST` | `/update/apply` | 最新の更新をダウンロードして適用し、再起動 | +| `POST` | `/update/rollback` | 前の更新に戻す。バックアップがなければイメージ付属のバージョンに戻す | +| `GET` | `/health`(ルートパス。`/api/v1` の下ではありません) | 死活監視。常に `200` を返します:`{"status", "version", "db_ok"}` | ## 設定 -### 設定の取得 - -``` -GET /config/get -``` - -現在のアプリケーション設定を取得します。 - -**レスポンス:** `program`、`downloader`、`rss_parser`、`bangumi_manager`、`notification`、`proxy`、`llm` などのセクションを含む完全な設定オブジェクト。 - -### 設定の更新 - -``` -PATCH /config/update -``` - -アプリケーション設定を部分的に更新します。変更したいフィールドのみを含めてください。 - -**リクエストボディ:** 部分的な設定オブジェクト。 - ---- - -## 番組(アニメルール) - -### すべての番組をリスト - -``` -GET /bangumi/get/all -``` - -すべてのアニメダウンロードルールを取得します。 - -### IDで番組を取得 - -``` -GET /bangumi/get/{bangumi_id} -``` - -IDで特定のアニメルールを取得します。 - -### 番組の更新 - -``` -PATCH /bangumi/update/{bangumi_id} -``` - -アニメルールのメタデータ(タイトル、シーズン、エピソードオフセットなど)を更新します。 - -### 番組の削除 - -``` -DELETE /bangumi/delete/{bangumi_id} -``` - -単一のアニメルールと関連するトレントを削除します。 - -``` -DELETE /bangumi/delete/many/ -``` - -複数のアニメルールを一括削除します。 - -**リクエストボディ:** -```json -{ - "bangumi_ids": [1, 2, 3] -} -``` - -### 番組の無効化 / 有効化 - -``` -DELETE /bangumi/disable/{bangumi_id} -``` - -アニメルールを無効化します(ファイルは保持、ダウンロード停止)。 - -``` -DELETE /bangumi/disable/many/ -``` - -複数のアニメルールを一括無効化します。 - -``` -GET /bangumi/enable/{bangumi_id} -``` - -以前に無効化されたアニメルールを再有効化します。 - -### ポスターのリフレッシュ - -``` -GET /bangumi/refresh/poster/all -``` - -すべてのアニメのポスター画像をTMDBからリフレッシュします。 - -``` -GET /bangumi/refresh/poster/{bangumi_id} -``` - -特定のアニメのポスター画像をリフレッシュします。 - -### カレンダー - -``` -GET /bangumi/refresh/calendar -``` - -Bangumi.tvからアニメ放送カレンダーデータをリフレッシュします。 - -### すべてリセット - -``` -GET /bangumi/reset/all -``` - -すべてのアニメルールを削除します。注意して使用してください。 - ---- - -## RSSフィード - -### すべてのフィードをリスト - -``` -GET /rss -``` - -設定されたすべてのRSSフィードを取得します。 - -### フィードの追加 - -``` -POST /rss/add -``` - -新しいRSSフィード購読を追加します。 - -**リクエストボディ:** -```json -{ - "url": "string", - "aggregate": true, - "parser": "mikan" -} -``` - -### フィードの有効化 / 無効化 - -``` -POST /rss/enable/many -``` - -複数のRSSフィードを有効化します。 - -``` -PATCH /rss/disable/{rss_id} -``` - -単一のRSSフィードを無効化します。 - -``` -POST /rss/disable/many -``` - -複数のRSSフィードを一括無効化します。 - -### フィードの削除 - -``` -DELETE /rss/delete/{rss_id} -``` - -単一のRSSフィードを削除します。 - -``` -POST /rss/delete/many -``` - -複数のRSSフィードを一括削除します。 - -### フィードの更新 - -``` -PATCH /rss/update/{rss_id} -``` - -RSSフィードの設定を更新します。 - -### フィードのリフレッシュ - -``` -GET /rss/refresh/all -``` - -すべてのRSSフィードのリフレッシュを手動でトリガーします。 - -``` -GET /rss/refresh/{rss_id} -``` - -特定のRSSフィードをリフレッシュします。 - -### フィードからトレントを取得 - -``` -GET /rss/torrent/{rss_id} -``` - -特定のRSSフィードから解析されたトレントのリストを取得します。 - -### 分析と購読 - -``` -POST /rss/analysis -``` - -RSS URLを分析し、購読せずにアニメのメタデータを抽出します。 - -**リクエストボディ:** -```json -{ - "url": "string" -} -``` - -``` -POST /rss/collect -``` - -RSSフィードからすべてのエピソードをダウンロードします(完結したアニメ用)。 - -``` -POST /rss/subscribe -``` - -自動継続ダウンロード用にRSSフィードを購読します。 - ---- +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/config/get` | 現在の設定。秘密フィールドはマスク済み | +| `PATCH` | `/config/update` | 設定を保存して再読み込み。マスクのまま送られた秘密フィールドは元の値を保持 | +| `POST` | `/config/llm/models` | 選択した LLM プロバイダーのモデル一覧 | +| `GET` | `/config/llm/providers` | LLM プロバイダー一覧 | +| `POST` | `/config/llm/providers/{provider_id}/install` | LLM プロバイダープラグインをインストール | +| `DELETE` | `/config/llm/providers/{provider_id}` | LLM プロバイダープラグインを削除 | +| `POST` | `/config/llm/providers/{provider_id}/auth/begin`、`/auth/complete` | サブスクリプション型プロバイダーの認可フロー | +| `GET` | `/config/llm/providers/{provider_id}/auth/status` | 認可の状態 | +| `DELETE` | `/config/llm/providers/{provider_id}/auth` | 認可を解除 | + +## 番組 + +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/bangumi/get/all` | すべての番組ルール | +| `GET` | `/bangumi/get/{bangumi_id}` | 1 件のルール | +| `PATCH` | `/bangumi/update/{bangumi_id}` | ルールを変更 | +| `DELETE` | `/bangumi/delete/{bangumi_id}` | ルールを削除 | +| `POST` | `/bangumi/delete/many` | 一括削除 | +| `POST` | `/bangumi/disable/{bangumi_id}`、`/bangumi/enable/{bangumi_id}` | ルールの無効化 / 有効化 | +| `POST` | `/bangumi/disable/many` | 一括無効化 | +| `PATCH` | `/bangumi/archive/{bangumi_id}`、`/bangumi/unarchive/{bangumi_id}` | アーカイブ / アーカイブ解除 | +| `PATCH` | `/bangumi/{bangumi_id}/weekday` | 放送曜日を手動で設定 | +| `GET` | `/bangumi/refresh/poster/all`、`/bangumi/refresh/poster/{bangumi_id}` | ポスターを更新 | +| `GET` | `/bangumi/refresh/calendar` | 放送カレンダーを更新 | +| `GET` | `/bangumi/refresh/metadata` | TMDB メタデータを更新し、放送終了した番組を自動でアーカイブ | +| `POST` | `/bangumi/reset/all` | すべてのルールを削除 | +| `GET` | `/bangumi/needs-review` | 話数オフセットの確認が必要な番組 | +| `GET` | `/bangumi/suggest-offset/{bangumi_id}` | TMDB の話数からオフセットを提案 | +| `POST` | `/bangumi/detect-offset` | シーズン / 話数と TMDB の不一致を検出 | +| `POST` | `/bangumi/apply-offset/{bangumi_id}`、`/bangumi/apply-offset/many` | 提案されたオフセットを適用し、リネームを 1 回実行 | +| `POST` | `/bangumi/dismiss-review/{bangumi_id}` | 「要確認」の印を消す | +| `GET` / `DELETE` | `/bangumi/{bangumi_id}/torrents` | その番組のトレント記録の一覧 / 削除 | +| `DELETE` | `/bangumi/{bangumi_id}/torrents/{torrent_id}` | トレント記録を 1 件削除 | +| `GET` / `DELETE` | `/bangumi/torrents/orphans` | どの番組にも属さないトレント記録の一覧 / 削除 | +| `GET` | `/bangumi/torrents/orphans/count` | 孤立したトレント記録の件数 | +| `DELETE` | `/bangumi/torrents/orphans/{torrent_id}` | 孤立したトレント記録を 1 件削除 | + +## 映画 + +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/movie/get/all`、`/movie/get/{movie_id}` | すべて / 1 件の映画ルール | +| `PATCH` | `/movie/update/{movie_id}` | 変更 | +| `DELETE` | `/movie/delete/{movie_id}` | 削除 | +| `DELETE` | `/movie/disable/{movie_id}` | 無効化 | +| `GET` | `/movie/enable/{movie_id}` | 有効化 | + +## RSS フィード + +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/rss` | すべてのフィード | +| `POST` | `/rss/add` | フィードを追加 | +| `PATCH` | `/rss/update/{rss_id}` | フィードを変更 | +| `DELETE` | `/rss/delete/{rss_id}` | フィードを削除 | +| `POST` | `/rss/delete/many` | 一括削除 | +| `PATCH` | `/rss/disable/{rss_id}` | 無効化 | +| `POST` | `/rss/disable/many`、`/rss/enable/many` | 一括無効化 / 有効化 | +| `POST` | `/rss/refresh/all`、`/rss/refresh/{rss_id}` | すぐに更新 | +| `GET` | `/rss/torrent/{rss_id}` | そのフィードのトレント | +| `POST` | `/rss/analysis` | RSS リンクを解析し、見つかった番組を返す | +| `POST` | `/rss/collect` | シーズン全体をダウンロード(収集) | +| `POST` | `/rss/subscribe` | 購読 | ## 検索 -### 番組検索(Server-Sent Events) +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/search/bangumi?site=<サイト>&keywords=<キーワード>` | Server-Sent Events で結果を 1 件ずつ送信。複数のキーワードは空白で区切る | +| `GET` | `/search/provider` | 使える検索サイト(プラグインが提供するサイトを含む) | +| `GET` / `PUT` | `/search/provider/config` | ユーザーが設定した検索サイトの読み込み / 保存 | -``` -GET /search/bangumi?keyword={keyword}&provider={provider} +## ダウンローダー + +AB 4.0 ではダウンローダーのインスタンスを複数設定できます(`plugins.instances`)。トレント一覧はすべてのインスタンスをまとめたもので、各項目に `downloader_id` が付きます。 + +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/downloader/instances` | ダウンローダーのインスタンス:`{"default": <既定のインスタンス id>, "instances": [{"id", "provider"}]}` | +| `GET` | `/downloader/torrents` | すべてのインスタンスのトレント。使えないインスタンスは省略 | +| `POST` | `/downloader/torrents/pause`、`/resume`、`/delete` | トレントの一時停止 / 再開 / 削除 | +| `POST` | `/downloader/torrents/tag` | トレントに番組 id のタグを付ける | +| `POST` | `/downloader/torrents/tag/auto` | タグのないトレントに、名前とパスから自動でタグを付ける | +| `GET` | `/downloader/rename-conflicts` | ユーザーの対応を待つリネームの競合 | +| `POST` | `/downloader/rename-conflicts/{operation_id}/retry` | 競合を 1 件消し、次のリネームで再確認させる | + +## 通知センター + +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` / `DELETE` | `/notification/messages` | アプリ内通知の一覧 / 全削除 | +| `GET` | `/notification/messages/unread-count` | 未読の件数 | +| `POST` | `/notification/messages/read-all` | すべて既読にする | +| `POST` | `/notification/messages/{message_id}/read` | 既読にする | +| `DELETE` | `/notification/messages/{message_id}` | 1 件削除 | +| `POST` | `/notification/test` | 保存済みの通知チャンネルを番号で指定してテスト | +| `POST` | `/notification/test-config` | 保存前の通知チャンネル設定をテスト | + +## イベントストリーム + +`GET /events/stream` は 1 本の Server-Sent Events 接続です。WebUI はポーリングの代わりにこれを使います。 + +| `event` | 送信のタイミング | `data` | +| --- | --- | --- | +| `status` | 3 秒ごと | `GET /status` と同じ構造 | +| `downloader` | 5 秒ごと | すべてのインスタンスのトレント。ダウンローダーが使えないときは `null` | +| `log` | 10 秒ごと | ログの末尾 | +| `update` | オンライン更新の実行中で、進捗が変わったとき | 更新の進捗 | +| `notification` | 接続したとき、および通知センターが変わったとき | 通知センターの状態(未読数 `unread_count` を含む) | +| `bus` | イベントバスにイベントが流れたとき | `{"kind": <イベント名>, "payload": {...}}`。ホストのイベントとプラグインのイベントを含む | + +```bash +curl -N -H "Authorization: Bearer $AB_TOKEN" http://your-host:7892/api/v1/events/stream ``` -アニメのトレントを検索します。リアルタイム更新のためにServer-Sent Events(SSE)ストリームとして結果を返します。 +`bus` フレームのイベント名とフィールドは [プラグイン開発 → イベント](/ja/dev/plugins/events) を参照してください。フロントエンドのプラグインコンポーネントは `host.events.on(kind, callback)` で同じフレームを受け取ります。 -**クエリパラメータ:** -- `keyword` — 検索キーワード -- `provider` — 検索プロバイダー(例:`mikan`、`nyaa`、`dmhy`) +## プラグイン -**レスポンス:** 解析された検索結果を含むSSEストリーム。 +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/plugins` | 見つかったプラグイン、状態、設定フォームの schema、現在の設定(マスク済み) | +| `PUT` | `/plugins/settings` | `allow_unsigned`(未署名プラグインを許可)を変更 | +| `PUT` | `/plugins/{plugin_id}` | プラグインの有効化 / 無効化、または設定の変更。保存するとすぐに反映。不正な設定は `422` | +| `GET` | `/plugins/providers` | プラグインが提供する Provider id(拡張ポイントごと) | +| `GET` | `/plugins/ui` | 有効なプラグインが宣言するフロントエンドスロット | +| `GET` | `/plugins/catalog` | 署名付きカタログのプラグイン。カタログを取得できないときは `502` | +| `POST` | `/plugins/{plugin_id}/install` | 署名付きカタログからプラグインをインストールまたは更新し、有効化 | +| `DELETE` | `/plugins/{plugin_id}` | 署名付きカタログからインストールしたプラグインを削除 | +| `GET` | `/plugins/{plugin_id}/web/{path}` | プラグインの `web/` ディレクトリにあるフロントエンドの静的ファイル | +| 任意 | `/plugins/{plugin_id}/{path}` | プラグイン独自のルート。[REST ルート](/ja/dev/plugins/points/api-router) を参照 | -### 検索プロバイダーのリスト +## セットアップウィザード -``` -GET /search/provider -``` +これらのエンドポイントは認証不要です。`GET /setup/status` は常に使え、`need_setup`(セットアップ完了後は `false`)を返します。その他のエンドポイントは初回セットアップが完了するまでだけ使え、完了後は `403` を返します。`/setup/complete` はさらに、ブラウザーセッションか、`admin` アカウントが出荷時のパスワード `adminadmin` のままであることが必要です。それ以外は `403` を返します。 -利用可能な検索プロバイダーのリストを取得します。 +| メソッド | パス | 説明 | +| --- | --- | --- | +| `GET` | `/setup/status` | セットアップウィザードが必要か | +| `POST` | `/setup/test-downloader` | ダウンローダーへの接続をテスト | +| `POST` | `/setup/test-rss` | RSS リンクをテスト | +| `POST` | `/setup/test-notification` | テスト通知を送信 | +| `POST` | `/setup/complete` | ウィザードの設定をすべて保存し、セットアップ完了とする | ---- +## MCP -## プログラム制御 +AB はルートパスの `/mcp` 以下で MCP サーバー(SSE トランスポート)を提供します。クライアントは `GET /mcp/sse` に接続し、`POST /mcp/messages/` にメッセージを送ります。 -### ステータスの取得 +- アクセス制御は REST API とは別です。クライアントの IP が `security.mcp_whitelist` に含まれるか、リクエストに `scope=mcp` のトークン(`Authorization: Bearer <トークン>`)が必要です。`mcp_whitelist` が空の場合、IP によるアクセスはすべて拒否されます。トークンは引き続き使えます。 +- 組み込みツール:`list_anime`、`get_anime`、`search_anime`、`subscribe_anime`、`unsubscribe_anime`、`list_downloads`、`list_rss_feeds`、`get_program_status`、`refresh_feeds`、`update_anime`。 +- 組み込みリソース:`autobangumi://anime/list`、`autobangumi://anime/{id}`、`autobangumi://status`、`autobangumi://rss/feeds`。 +- 有効なプラグインが提供するツールの名前は `<プラグイン id>__`、リソースの URI は `autobangumi://plugins/<プラグイン id>/` です。[MCP ツールとリソース](/ja/dev/plugins/points/mcp) を参照してください。 -``` -GET /status -``` +## レスポンスとエラー -バージョン、実行状態、first_runフラグを含むプログラムステータスを取得します。 - -**レスポンス:** -```json -{ - "status": "running", - "version": "3.2.0", - "first_run": false -} -``` - -### プログラムの開始 - -``` -GET /start -``` - -メインプログラム(RSSチェック、ダウンロード、リネーム)を開始します。 - -### プログラムの再起動 - -``` -GET /restart -``` - -メインプログラムを再起動します。 - -### プログラムの停止 - -``` -GET /stop -``` - -メインプログラムを停止します(WebUIはアクセス可能なまま)。 - -### シャットダウン - -``` -GET /shutdown -``` - -アプリケーション全体をシャットダウンします(Dockerコンテナを再起動します)。 - -### ダウンローダーのチェック - -``` -GET /check/downloader -``` - -設定されたダウンローダー(qBittorrent)への接続をテストします。 - ---- - -## ダウンローダー管理 - -AutoBangumiから直接ダウンローダー内のトレントを管理します。 - -### トレントのリスト - -``` -GET /downloader/torrents -``` - -Bangumiカテゴリ内のすべてのトレントを取得します。 - -### トレントの一時停止 - -``` -POST /downloader/torrents/pause -``` - -ハッシュでトレントを一時停止します。 - -**リクエストボディ:** -```json -{ - "hashes": ["hash1", "hash2"] -} -``` - -### トレントの再開 - -``` -POST /downloader/torrents/resume -``` - -ハッシュで一時停止したトレントを再開します。 - -**リクエストボディ:** -```json -{ - "hashes": ["hash1", "hash2"] -} -``` - -### トレントの削除 - -``` -POST /downloader/torrents/delete -``` - -オプションでファイル削除を伴うトレントを削除します。 - -**リクエストボディ:** -```json -{ - "hashes": ["hash1", "hash2"], - "delete_files": false -} -``` - ---- - -## セットアップウィザード - -これらのエンドポイントは、初回実行セットアップ中(セットアップ完了前)にのみ利用可能です。認証は**不要**です。セットアップ完了後、すべてのエンドポイントは`403 Forbidden`を返します。 - -### セットアップステータスの確認 - -``` -GET /setup/status -``` - -セットアップウィザードが必要かどうか(初回実行)を確認します。 - -**レスポンス:** -```json -{ - "need_setup": true -} -``` - -### ダウンローダー接続のテスト - -``` -POST /setup/test-downloader -``` - -提供された認証情報でダウンローダーへの接続をテストします。 - -**リクエストボディ:** -```json -{ - "type": "qbittorrent", - "host": "172.17.0.1:8080", - "username": "admin", - "password": "adminadmin", - "ssl": false -} -``` - -### RSSフィードのテスト - -``` -POST /setup/test-rss -``` - -RSS URLがアクセス可能で解析可能か検証します。 - -**リクエストボディ:** -```json -{ - "url": "https://mikanime.tv/RSS/MyBangumi?token=xxx" -} -``` - -### 通知のテスト - -``` -POST /setup/test-notification -``` - -提供された設定でテスト通知を送信します。 - -**リクエストボディ:** -```json -{ - "type": "telegram", - "token": "bot_token", - "chat_id": "chat_id" -} -``` - -### セットアップの完了 - -``` -POST /setup/complete -``` - -すべての設定を保存し、セットアップを完了としてマークします。センチネルファイル`config/.setup_complete`を作成します。 - -**リクエストボディ:** 完全な設定オブジェクト。 - ---- - -## ログ - -### ログの取得 - -``` -GET /log -``` - -完全なアプリケーションログファイルを取得します。 - -### ログのクリア - -``` -GET /log/clear -``` - -ログファイルをクリアします。 - ---- - -## レスポンス形式 - -すべてのAPIレスポンスは一貫した形式に従います: - -```json -{ - "msg_en": "Success message in English", - "msg_zh": "Success message in Chinese", - "status": true -} -``` - -エラーレスポンスには、両方の言語でのエラーメッセージとともに適切なHTTPステータスコード(400、401、403、404、500)が含まれます。 +- 多くの操作系エンドポイントは `{"status": true, "msg_en": "...", "msg_zh": "..."}` を返します。取得系エンドポイントはデータをそのまま返します。正確な構造は `/docs` を参照してください。 +- エラーには標準の HTTP ステータスコードを使います:`401` 未認証、`403` 権限なし、`404` 存在しない、`422` 検証エラー、`500` サーバーエラー。 diff --git a/docs/ja/dev/database.md b/docs/ja/dev/database.md index 35d94696..05af5742 100644 --- a/docs/ja/dev/database.md +++ b/docs/ja/dev/database.md @@ -1,394 +1,146 @@ -# データベース開発者ガイド +# データベース開発ガイド -このガイドでは、AutoBangumiのデータベースアーキテクチャ、モデル、および操作について説明します。 +このページでは、AutoBangumi バックエンドのデータ層(エンジン、`Database` ファサード、リポジトリ、セッションの規約、スキーマのマイグレーション)を説明します。コードは `backend/src/module/database/` にあります。 ## 概要 -AutoBangumiはデータベースとして**SQLite**を使用し、ORMには**SQLModel**(Pydantic + SQLAlchemyハイブリッド)を使用しています。データベースファイルは`data/data.db`にあります。 - -### アーキテクチャ +- データベースは SQLite ファイル `data/data.db`、ORM は SQLModel(Pydantic + SQLAlchemy)です。 +- アプリケーション全体で非同期エンジン `sqlite+aiosqlite` だけを使います。各接続で `PRAGMA journal_mode=WAL`、`busy_timeout=5000`、`foreign_keys=ON` を設定します。複数の接続が同時に書き込め、競合した書き込みはすぐに `database is locked` を出さずに待ちます。 +- 外部キー制約は有効です。ほかの行から参照されている行を削除する前に、参照している行を先に処理してください(例えば `BangumiDatabase.delete_all` は先にトレント記録を削除します)。 ``` module/database/ -├── engine.py # SQLAlchemyエンジン設定 -├── combine.py # Databaseクラス、マイグレーション、セッション管理 -├── bangumi.py # Bangumi(アニメ購読)操作 -├── rss.py # RSSフィード操作 -├── torrent.py # トレント追跡操作 -└── user.py # ユーザー認証操作 +├── __init__.py # Database、get_db(FastAPI の依存) +├── engine.py # async_engine、async_session_factory、接続ごとの PRAGMA +├── combine.py # Database:1 つのセッションとすべてのリポジトリ +├── migrations.py # MIGRATIONS テーブルとマイグレーションの実行 +├── bangumi.py、rss.py、torrent.py、movie.py、user.py、auth.py、 +├── inbox.py、llm_credential.py、rename_operation.py、aria2.py、 +└── plugin_kv.py、passkey.py # テーブルごとのリポジトリ ``` -## コアコンポーネント +## Database とセッション -### Databaseクラス - -`combine.py`の`Database`クラスがメインエントリーポイントです。SQLModelの`Session`を継承し、すべてのサブデータベースへのアクセスを提供します: +`Database` は非同期コンテキストマネージャーです。`AsyncSession` を 1 つ作り、リポジトリを自身の属性として持ちます。`async with` ブロックを出るとセッションを閉じます。 ```python from module.database import Database -with Database() as db: - # サブデータベースへのアクセス - bangumis = db.bangumi.search_all() - rss_items = db.rss.search_active() - torrents = db.torrent.search_all() +async with Database() as db: + rules = await db.bangumi.search_all() + feeds = await db.rss.search_all() ``` -### サブデータベースクラス +| 属性 | リポジトリ | テーブル | +| --- | --- | --- | +| `db.bangumi` | `BangumiDatabase` | `bangumi` | +| `db.rss` | `RSSDatabase` | `rssitem` | +| `db.torrent` | `TorrentDatabase` | `torrent` | +| `db.movie` | `MovieDatabase` | `movie` | +| `db.user` | `UserDatabase` | `user` | +| `db.auth` | `AuthDatabase` | `auth_session`、`api_token` | +| `db.inbox` | `InboxDatabase` | 通知センター | +| `db.llm_credential` | `LLMCredentialDatabase` | LLM のサブスクリプション認証情報 | +| `db.rename_operation` | `RenameOperationDatabase` | `rename_operation`(リネームの競合と進捗) | +| `db.aria2` | `Aria2GidDatabase` | `aria2_gid` | +| `db.plugin_kv` | `PluginKVDatabase` | `plugin_kv`(プラグインの `ctx.kv`) | -| クラス | モデル | 目的 | -|-------|-------|------| -| `BangumiDatabase` | `Bangumi` | アニメ購読ルール | -| `RSSDatabase` | `RSSItem` | RSSフィードソース | -| `TorrentDatabase` | `Torrent` | ダウンロードしたトレントの追跡 | -| `UserDatabase` | `User` | 認証 | +`PasskeyDatabase` は `Database` に含まれません。必要なときに `PasskeyDatabase(db.session)` で作ります。 -## モデル +`Database` はいくつかのセッションメソッドも転送します:`add`(同期。キューに入れるだけ)、`commit`、`rollback`、`refresh`、`close`。`begin_write()` は SQLite では `BEGIN IMMEDIATE` を実行します。「読んでから書き、その間に不変条件を保つ」場合に使い、そのセッションで最初にアクセスする前に呼び出します。 -### Bangumiモデル +### セッションの規約 -アニメ購読のコアモデル: +**1 つの操作に 1 つのセッション。** 1 回のリクエストや 1 回のループより長く生きるオブジェクトに、セッションや `Database` を保存しないでください。`AppContext` はセッションを持ちません。 + +- ルートでは依存性注入を使います。 + + ```python + from fastapi import Depends + from module.database import Database, get_db + + @router.get("/example") + async def example(db: Database = Depends(get_db)): + return await db.bangumi.search_all() + ``` + +- 定期ループとサービスは、実行のたびに自分で開きます:`async with Database() as db:`。 +- サービスは依存をコンストラクタで受け取ります:`RSSEngine(db)`、`TorrentManager(db)`。サービス内では `self.db.<リポジトリ>` を使い、リポジトリを 1 つだけ使う呼び出し側は `db.<リポジトリ>` を直接使います。 +- セッションファクトリは `expire_on_commit=False` です。コミット後もオブジェクトの属性は読めますが、そのオブジェクトは新しいセッションには関連付いていません。変更するときは、新しいセッションでもう一度取得してください。 + +## リポジトリ + +リポジトリは `AsyncSession` を受け取り、メソッドはすべて `async def` で、多くはメソッドの中でコミットします。クエリを追加するときは: + +- そのテーブルのリポジトリに置きます。ルートやサービスに SQL を直接書かないでください。 +- 複数行は `in_` で 1 回で取得します。ループの中で 1 行ずつ問い合わせないでください。 +- 論理削除:`bangumi` と `movie` には `deleted` フィールドがあります。ルールの「無効化」(`disable_rule`)は `deleted` を `True` にするだけで、行を本当に削除するのは `delete_one` です。有効なルールを問い合わせるときは `deleted` で絞り込んでください。 + +### 複数のダウンローダー + +4.0 はダウンローダーのインスタンスを複数持てます(設定 `plugins.instances`)。`bangumi`、`movie`、`rssitem`、`torrent` には `downloader_id` 列があります。 + +- ルールとフィードでは、空は「既定のインスタンスを使う」(`plugins.slots.downloader`)という意味です。 +- トレント記録では、そのトレントが実際にあるインスタンスです。3.3 から引き継いだトレントは `default` です。 + +## スキーマのマイグレーション + +マイグレーションは `migrations.py` の `MIGRATIONS` タプルにあり、テーブル駆動で実行されます。データベースの `schema_version` テーブルが適用済みのバージョンを記録します。 ```python -class Bangumi(SQLModel, table=True): - id: int # 主キー - official_title: str # 表示名(例:"無職転生") - title_raw: str # トレントマッチング用の生タイトル(インデックス付き) - season: int = 1 # シーズン番号 - episode_offset: int = 0 # エピソード番号調整 - season_offset: int = 0 # シーズン番号調整 - rss_link: str # カンマ区切りRSSフィードURL - filter: str # 除外フィルター(例:"720,\\d+-\\d+") - poster_link: str # TMDBポスターURL - save_path: str # ダウンロード先パス - rule_name: str # qBittorrent RSSルール名 - added: bool = False # ルールがダウンローダーに追加されたかどうか - deleted: bool = False # ソフト削除フラグ(インデックス付き) - archived: bool = False # 完結シリーズ用(インデックス付き) - needs_review: bool = False # オフセット不一致検出 - needs_review_reason: str # レビューの理由 - suggested_season_offset: int # 提案されたシーズンオフセット - suggested_episode_offset: int # 提案されたエピソードオフセット - air_weekday: int # 放送日(0=日曜日、6=土曜日) +Migration( + 26, + "add downloader_id to bangumi, movie, rssitem and torrent", + (...SQL 文...), + already_applied=..., # ガード:このマイグレーションがすでに有効か + guarded_statements=(...), # 任意:文ごとのガード +) ``` -### RSSItemモデル +起動時、AB はまず `SQLModel.metadata.create_all` を実行し、次に `run_migrations()` を実行します。 -RSSフィード購読: +1. `schema_version` を読み、そのバージョン以下のマイグレーションを飛ばします。 +2. 未適用のマイグレーションごとにデータベースの構造を調べ直し、ガードを呼びます。すでに有効な場合(例えば新しいデータベースを `create_all` がモデルから作った場合)は、バージョンだけを記録します。 +3. そうでなければ、1 つの SAVEPOINT の中で文を実行します。どれかが失敗すると、そのマイグレーションをロールバックして例外を上に投げ、マイグレーションのトランザクション全体がロールバックされ、AB は起動に失敗します。AB は途中までしか移行していないスキーマでは動きません。 +4. マイグレーションを実行した場合は、最後に既存の行の `NULL` をモデルの既定値で埋めます(`fill_null_with_defaults_conn`)。主キー、`default_factory` のフィールド、既定値が `None` の Optional フィールドは対象外です。 + +### マイグレーションを追加する + +1. 先に `module/models/` のモデルを変更します。新しいデータベースは `create_all` で新しい構造になります。 +2. `MIGRATIONS` の末尾に `Migration` を追加します。バージョンは直前の値に 1 を足したものです。`CURRENT_SCHEMA_VERSION` はリストから求まるので、手で変更しないでください。 +3. `already_applied` ガードを指定します:`column_exists(テーブル, 列)`、`table_exists(テーブル)`、`index_exists(テーブル, インデックス)`。条件を組み合わせるには `all_checks(...)` を使います。1 つのマイグレーションで複数のテーブルを変更し、テーブルごとに状態が違う可能性がある場合は、`guarded_statements` で文ごとにガードを付けます。 +4. `backend/src/test/` にマイグレーションのテストを追加します。古いスキーマから始め、マイグレーションを実行して結果を確認します。 ```python -class RSSItem(SQLModel, table=True): - id: int # 主キー - name: str # 表示名 - url: str # フィードURL(ユニーク、インデックス付き) - aggregate: bool = True # トレントを解析するかどうか - parser: str = "mikan" # パーサータイプ:mikan、dmhy、nyaa - enabled: bool = True # アクティブフラグ - connection_status: str # "healthy"または"error" - last_checked_at: str # ISOタイムスタンプ - last_error: str # 最後のエラーメッセージ +Migration( + 27, + "add note to bangumi", + ("ALTER TABLE bangumi ADD COLUMN note TEXT DEFAULT ''",), + column_exists("bangumi", "note"), +) ``` -### Torrentモデル - -ダウンロードしたトレントを追跡: - -```python -class Torrent(SQLModel, table=True): - id: int # 主キー - name: str # トレント名(インデックス付き) - url: str # トレント/マグネットURL(ユニーク、インデックス付き) - rss_id: int # ソースRSSフィードID - bangumi_id: int # リンクされたBangumi ID(nullable) - qb_hash: str # qBittorrentインフォハッシュ(インデックス付き) - downloaded: bool = False # ダウンロード完了 -``` - -## 一般的な操作 - -### BangumiDatabase - -```python -with Database() as db: - # 作成 - db.bangumi.add(bangumi) # 単一挿入 - db.bangumi.add_all(bangumi_list) # バッチ挿入(重複排除) - - # 読み取り - db.bangumi.search_all() # 全レコード(キャッシュ、5分TTL) - db.bangumi.search_id(123) # IDで検索 - db.bangumi.match_torrent("torrent name") # title_rawマッチで検索 - db.bangumi.not_complete() # 未完了シリーズ - db.bangumi.get_needs_review() # レビューフラグ付き - - # 更新 - db.bangumi.update(bangumi) # 単一レコード更新 - db.bangumi.update_all(bangumi_list) # バッチ更新 - - # 削除 - db.bangumi.delete_one(123) # ハード削除 - db.bangumi.disable_rule(123) # ソフト削除(deleted=True) -``` - -### RSSDatabase - -```python -with Database() as db: - # 作成 - db.rss.add(rss_item) # 単一挿入 - db.rss.add_all(rss_items) # バッチ挿入(重複排除) - - # 読み取り - db.rss.search_all() # 全フィード - db.rss.search_active() # 有効なフィードのみ - db.rss.search_aggregate() # 有効 + aggregate=True - - # 更新 - db.rss.update(id, rss_update) # 部分更新 - db.rss.enable(id) # フィード有効化 - db.rss.disable(id) # フィード無効化 - db.rss.enable_batch([1, 2, 3]) # バッチ有効化 - db.rss.disable_batch([1, 2, 3]) # バッチ無効化 -``` - -### TorrentDatabase - -```python -with Database() as db: - # 作成 - db.torrent.add(torrent) # 単一挿入 - db.torrent.add_all(torrents) # バッチ挿入 - - # 読み取り - db.torrent.search_all() # 全トレント - db.torrent.search_by_qb_hash(hash) # qBittorrentハッシュで検索 - db.torrent.search_by_url(url) # URLで検索 - db.torrent.check_new(torrents) # 既存のものをフィルター - - # 更新 - db.torrent.update_qb_hash(id, hash) # qb_hashを設定 -``` - -## キャッシング - -### Bangumiキャッシュ - -`search_all()`の結果はモジュールレベルで5分のTTLでキャッシュされます: - -```python -# bangumi.pyのモジュールレベルキャッシュ -_bangumi_cache: list[Bangumi] | None = None -_bangumi_cache_time: float = 0 -_BANGUMI_CACHE_TTL: float = 300.0 # 5分 - -# キャッシュ無効化 -def _invalidate_bangumi_cache(): - global _bangumi_cache, _bangumi_cache_time - _bangumi_cache = None - _bangumi_cache_time = 0 -``` - -**重要:** キャッシュは以下で自動的に無効化されます: -- `add()`、`add_all()` -- `update()`、`update_all()` -- `delete_one()`、`delete_all()` -- `archive_one()`、`unarchive_one()` -- 任意のRSSリンク更新操作 - -### セッションExpunge - -キャッシュされたオブジェクトは`DetachedInstanceError`を防ぐためにセッションから**expunge**されます: - -```python -for b in bangumis: - self.session.expunge(b) # セッションから切り離す -``` - -## マイグレーションシステム - -### スキーマバージョニング - -マイグレーションは`schema_version`テーブルを介して追跡されます: - -```python -CURRENT_SCHEMA_VERSION = 7 - -# 各マイグレーション:(バージョン、説明、[SQLステートメント]) -MIGRATIONS = [ - (1, "add air_weekday column", [...]), - (2, "add connection status columns", [...]), - (3, "create passkey table", [...]), - (4, "add archived column", [...]), - (5, "rename offset to episode_offset", [...]), - (6, "add qb_hash column", [...]), - (7, "add suggested offset columns", [...]), -] -``` - -### 新しいマイグレーションの追加 - -1. `combine.py`の`CURRENT_SCHEMA_VERSION`をインクリメント -2. `MIGRATIONS`リストにマイグレーションタプルを追加: - -```python -MIGRATIONS = [ - # ... 既存のマイグレーション ... - ( - 8, - "add my_new_column to bangumi", - [ - "ALTER TABLE bangumi ADD COLUMN my_new_column TEXT DEFAULT NULL", - ], - ), -] -``` - -3. `run_migrations()`に冪等性チェックを追加: - -```python -if "bangumi" in tables and version == 8: - columns = [col["name"] for col in inspector.get_columns("bangumi")] - if "my_new_column" in columns: - needs_run = False -``` - -4. `module/models/`の対応するPydanticモデルを更新 - -### デフォルト値のバックフィル - -マイグレーション後、`_fill_null_with_defaults()`がモデルのデフォルトに基づいてNULL値を自動的に埋めます: - -```python -# モデルが定義している場合: -class Bangumi(SQLModel, table=True): - my_field: bool = False - -# NULLの既存行はFalseに更新されます -``` - -## パフォーマンスパターン - -### バッチクエリ - -`add_all()`は、Nクエリの代わりに単一のクエリを使用して重複をチェックします: - -```python -# 効率的:単一SELECT -keys_to_check = [(d.title_raw, d.group_name) for d in datas] -conditions = [ - and_(Bangumi.title_raw == tr, Bangumi.group_name == gn) - for tr, gn in keys_to_check -] -statement = select(Bangumi.title_raw, Bangumi.group_name).where(or_(*conditions)) -``` - -### 正規表現マッチング - -`match_list()`は、すべてのタイトルマッチ用に単一の正規表現パターンをコンパイルします: - -```python -# 一度コンパイル、多くマッチ -sorted_titles = sorted(title_index.keys(), key=len, reverse=True) -pattern = "|".join(re.escape(title) for title in sorted_titles) -title_regex = re.compile(pattern) - -# トレントごとにO(n)ではなくO(1)ルックアップ -for torrent in torrent_list: - match = title_regex.search(torrent.name) -``` - -### インデックス付きカラム - -以下のカラムには高速ルックアップ用のインデックスがあります: - -| テーブル | カラム | インデックスタイプ | -|---------|--------|------------------| -| `bangumi` | `title_raw` | 通常 | -| `bangumi` | `deleted` | 通常 | -| `bangumi` | `archived` | 通常 | -| `rssitem` | `url` | ユニーク | -| `torrent` | `name` | 通常 | -| `torrent` | `url` | ユニーク | -| `torrent` | `qb_hash` | 通常 | - ## テスト -### テストデータベースセットアップ +`backend/src/test/conftest.py` にインメモリのデータベースがあります。 -テストはインメモリSQLiteデータベースを使用します: +- `db_engine`:`sqlite+aiosqlite://` と `StaticPool`。テストごとに新しく作り、モデルからテーブルを作成します。 +- `db_session`:`db_engine` に結び付いた `AsyncSession`。 + +リポジトリを直接テストするときは、`Database(engine=db_engine)` を使うか、`db_session` からリポジトリを 1 つ作ります。 ```python -# conftest.py -@pytest.fixture -def db_engine(): - engine = create_engine("sqlite:///:memory:") - SQLModel.metadata.create_all(engine) - yield engine - engine.dispose() +from module.database import Database +from module.database.bangumi import BangumiDatabase +from test.factories import make_bangumi -@pytest.fixture -def db_session(db_engine): - with Session(db_engine) as session: - yield session +async def test_add_bangumi(db_engine): + async with Database(engine=db_engine) as db: + assert await db.bangumi.add(make_bangumi()) + +async def test_search(db_session): + repo = BangumiDatabase(db_session) + assert await repo.search_all() == [] ``` -### ファクトリ関数 - -テストデータ作成にはファクトリ関数を使用: - -```python -from test.factories import make_bangumi, make_torrent, make_rss_item - -def test_bangumi_search(): - bangumi = make_bangumi(title_raw="Test Title", season=2) - # ... テストロジック -``` - -## 設計ノート - -### 外部キーなし - -SQLite外部キー強制はデフォルトで無効になっています。リレーションシップ(`Torrent.bangumi_id`など)はデータベース制約ではなくアプリケーションロジックで管理されます。 - -### ソフト削除 - -`Bangumi.deleted`フラグはソフト削除を可能にします。クエリはユーザー向けデータには`deleted=False`でフィルターする必要があります: - -```python -statement = select(Bangumi).where(Bangumi.deleted == false()) -``` - -### トレントタグ付け - -トレントはリネーム操作中のオフセットルックアップ用にqBittorrentで`ab:{bangumi_id}`でタグ付けされます。これにより、データベースクエリなしで高速な番組識別が可能になります。 - -## 一般的な問題 - -### DetachedInstanceError - -キャッシュされたオブジェクトを別のセッションからアクセスする場合: - -```python -# 間違い:新しいセッションでキャッシュされたオブジェクトにアクセス -bangumis = db.bangumi.search_all() # キャッシュ済み -with Database() as new_db: - new_db.session.add(bangumis[0]) # エラー! - -# 正しい:オブジェクトはexpungeされ、独立して動作 -bangumis = db.bangumi.search_all() -bangumis[0].title_raw = "New Title" # OK、ただし永続化されない -``` - -### キャッシュの古さ - -手動SQLアップデートがORMをバイパスする場合、キャッシュを無効化: - -```python -from module.database.bangumi import _invalidate_bangumi_cache - -with engine.connect() as conn: - conn.execute(text("UPDATE bangumi SET ...")) - conn.commit() - -_invalidate_bangumi_cache() # 重要! -``` +バックエンドの pytest は `asyncio_mode = "auto"` なので、非同期のテスト関数に追加のデコレーターは不要です。実行するには:`cd backend && uv run pytest src/test/test_xxx.py -v`。 diff --git a/docs/ja/dev/e2e-test-guide.md b/docs/ja/dev/e2e-test-guide.md new file mode 100644 index 00000000..d640a53a --- /dev/null +++ b/docs/ja/dev/e2e-test-guide.md @@ -0,0 +1,62 @@ +# E2E テストガイド + +エンドツーエンドテストは、本番ビルドの AutoBangumi イメージを、ローカルの決定的な上流サービス(モックの RSS、TMDB、プレーヤー、qBittorrent)に対して実行します。テストは公開サービスにアクセスしません。Docker Compose はループバックアドレスにランダムなポートを公開し、サービス同士は内部ネットワークで通信します。 + +完全な説明(各テストレーン、診断用の成果物、モックシナリオの追加、qBittorrent イメージのダイジェスト更新)はリポジトリの [`e2e/README.md`](https://github.com/EstrellaXD/Auto_Bangumi/blob/main/e2e/README.md) にあります。このページでは、ローカルで実行する最短の手順だけを示します。 + +## 準備 + +- Docker Engine と Compose v2 +- Python 3.13 と `uv` +- Node.js 20 と pnpm 9.11 + +```bash +uv sync --directory backend --locked --group dev +pnpm --dir webui install --frozen-lockfile +pnpm --dir webui exec playwright install --with-deps chromium webkit firefox +``` + +## テストイメージのビルド + +```bash +pnpm --dir webui run build +python3 e2e/scripts/build_test_image.py \ + --version 3.3.999-e2e.1 \ + --image auto-bangumi:e2e \ + --dist webui/dist +``` + +ビルドスクリプトは、WebUI のビルド成果物と生成したバージョンモジュールを一時的な Docker コンテキストに置きます。ソースツリーには書き込みません。 + +## 実行 + +実行のたびに新しいプロジェクト名と作業ディレクトリを使います。作業ディレクトリを使い回すとセットアップ状態も使い回すことになり、初回起動のテストが意味を失います。 + +```bash +export LANE=runtime +export AB_E2E_PROJECT="ab-e2e-local-$LANE-$$" +export AB_E2E_WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ab-e2e-$LANE.XXXXXX")" +export AB_E2E_APP_IMAGE=auto-bangumi:e2e + +# ランタイムの API とプロセスのテスト +python3 e2e/scripts/stack.py run \ + --profile browser \ + --project-name "$AB_E2E_PROJECT" \ + --work-dir "$AB_E2E_WORK_DIR" \ + -- uv run --directory backend pytest src/test/e2e/runtime -m e2e -q +``` + +| レーン | `--` の後のコマンド | `--profile` | +| --- | --- | --- | +| ランタイム | `uv run --directory backend pytest src/test/e2e/runtime -m e2e -q` | `browser` | +| Chromium デスクトップ | `pnpm --dir webui run test:e2e:chromium --retries=0` | `browser` | +| WebKit モバイル | `pnpm --dir webui run test:e2e:webkit --retries=0` | `browser` | +| 実際の qBittorrent とパッケージ済みイメージ | `python3 e2e/scripts/run_downloader_lane.py` | `downloader` | + +- 通常の `pytest` は E2E テストを飛ばします。`-m e2e` を明示する必要があります。 +- `stack.py run` は Compose を片付ける前に診断情報を集め、`$AB_E2E_WORK_DIR/artifacts` に書き出します。Playwright のスクリーンショットと動画は `webui/test-results`、HTML レポートは `webui/playwright-report` にあります。 +- スタックを止めるには:`python3 e2e/scripts/stack.py stop --profile browser --project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"`。 + +## CI + +`.github/workflows/e2e.yml` は PR ごとに、ランタイムのレーン、ブラウザのレーン(Chromium、WebKit モバイル)、実際のダウンローダーのレーンを実行します。`e2e-nightly.yml` は毎晩 Firefox のレーンを実行し、ブラウザのレーンとダウンローダーのレーンをそれぞれ独立して 2 回実行します。エンドポイントの構造や検証など、より速い契約テストは `backend/src/test/test_api_*.py` にあります。 diff --git a/docs/ja/dev/index.md b/docs/ja/dev/index.md index ab20a56c..b4ce2a31 100644 --- a/docs/ja/dev/index.md +++ b/docs/ja/dev/index.md @@ -128,7 +128,12 @@ Minor開発ブランチ(例:`3.1-dev`)が機能開発を完了し、**最 ## リリースプロセス -リリースは現在、開発チームが特定の「リリースPR」を手動でマージした後に自動的にトリガーされます。 +リリースは、メンテナーがバージョンタグを手動で push したときだけ実行されます。PR をマージしてもリリースされません。 + +- **beta**:開発ブランチに `X.Y.Z-beta.N` 形式のタグ(例:`4.0.0-beta.1`)を付けます。CI が Docker イメージをビルドし、バージョン番号と `dev-latest` のタグを付け、プレリリースの GitHub Release を作成します。 +- **正式版**:開発ブランチを PR で `main` にマージしたあと、`main` のマージコミットに `X.Y.Z` 形式のタグを付けます。CI はタグの形式と、そのコミットが `main` 上にあることを確認してから、イメージをビルドし(タグはバージョン番号と `latest`)、`docs/changelog/.md` のリリースノートで GitHub Release を作成します。 + +各 Release には、WebUI、オンライン更新バンドル、プラグイン SDK の wheel とプラグイン開発 skill が添付されます([SDK の入手](/ja/dev/plugins/sdk) を参照)。 バグ修正PRは通常、迅速にリリースされ、通常は1週間以内です。 diff --git a/docs/ja/dev/plugins.md b/docs/ja/dev/plugins.md index 17e8058e..d9a8c736 100644 --- a/docs/ja/dev/plugins.md +++ b/docs/ja/dev/plugins.md @@ -8,23 +8,25 @@ ## 5 分で始める -1. SDK とコマンドラインをインストールします。wheel は 4.0 の各 beta / 正式版の [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases) に添付されます(ファイル名は `autobangumi_sdk-<バージョン>-py3-none-any.whl`)。PyPI には公開しません。 +1. SDK とコマンドラインをインストールします。wheel は 4.0 の各 beta / 正式版の [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases) に添付されます(ファイル名は `autobangumi_sdk--py3-none-any.whl`)。PyPI には公開しません。詳しくは [SDK の入手](/ja/dev/plugins/sdk) を参照してください。 ```bash uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl ``` -2. ひな形を作ってテストします。 +2. ひな形を作ってテストします。ひな形の `pyproject.toml` は `autobangumi-sdk` に依存しますが、このパッケージは PyPI にありません。先に `uv add` でダウンロードした wheel を指定します。 ```bash ab-plugin new my-rename --kind rename # notifier、search も選べます - cd my-rename && uv run pytest # ひな形にはコントラクトテストが付いています + cd my-rename + uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # wheel の実際のパスに置き換えます + uv run pytest # ひな形にはコントラクトテストが付いています ``` -3. ローカルの AutoBangumi にリンクし、ホットリロードを有効にします。先に AB を一度起動して、`config/` に設定ファイルを作っておきます。 +3. ローカルの AutoBangumi にリンクし、ホットリロードを有効にします。先に AB を一度起動して、設定ディレクトリに設定ファイルを作っておきます。`--config-dir` にはこのディレクトリを指定します(ソースから実行する場合は `backend/src/config`)。 ```bash - ab-plugin dev . + ab-plugin dev . --config-dir /path/to/autobangumi/config ``` AB を一度再起動します。以後は、プラグインのファイルを変更すると自動でリロードされます。 @@ -34,6 +36,8 @@ **基本** +- [SDK の入手](/ja/dev/plugins/sdk):GitHub Release の wheel とプラグイン開発 skill + - [基本概念](/ja/dev/plugins/concepts):`Plugin`、`ctx`、3 種類の拡張宣言、マニフェスト、分離とサーキットブレーカー - [設定フォーム](/ja/dev/plugins/config-forms):`config_model` が WebUI のフォームになる仕組み - [イベント](/ja/dev/plugins/events):システムイベント、整理イベント、独自イベント diff --git a/docs/ja/dev/plugins/cli.md b/docs/ja/dev/plugins/cli.md index fa868d3f..c859cb6f 100644 --- a/docs/ja/dev/plugins/cli.md +++ b/docs/ja/dev/plugins/cli.md @@ -7,7 +7,7 @@ uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl # プロジェクト内の場合:uv add ./autobangumi_sdk-0.5.0-py3-none-any.whl ``` -wheel は 4.0 beta / 正式版の GitHub Release の添付ファイルです。PyPI には公開しません。コントラクトテストには pytest が必要です:`uv add --dev pytest`(または `autobangumi-sdk[test]` をインストール)。 +wheel は 4.0 beta / 正式版の GitHub Release の添付ファイルです。PyPI には公開しません。[SDK の入手](/ja/dev/plugins/sdk) を参照してください。コントラクトテストには pytest が必要です。`new` が作る `pyproject.toml` には `dev` 依存グループに pytest が入っています。自分で作ったプロジェクトでは `uv add --dev pytest`(または `autobangumi-sdk[test]` をインストール)を使います。 ## new @@ -28,6 +28,8 @@ ab-plugin new [--kind rename|notifier|search] [--dir .] `id` に使えるのは小文字、数字、ハイフンだけです。ディレクトリが既にあると実行を拒否します。ダウンローダーのひな形はありません。ダウンローダーには実際のバックエンドが必要で、そのままコントラクトを通るひな形には意味がないためです。 +生成される `pyproject.toml` は `autobangumi-sdk` に依存します。このパッケージは PyPI にないため、`uv run pytest` の前にプラグインディレクトリで一度 `uv add ` を実行します。uv はこの依存にローカルの wheel を使うようになります。 + ## validate ```bash @@ -42,7 +44,7 @@ ab-plugin validate [path] ab-plugin pack [path] [-o dist] ``` -検証してから `dist/-<バージョン>.zip` を作成します。内容は zip のルートに置かれ、署名付きカタログのパッケージと同じ構成です。`tests/`、`pyproject.toml`、`uv.lock`、キャッシュ、`dist/` は除外されます。ファイルの順序とタイムスタンプは固定なので、同じ内容からは同じ sha256 が得られます。 +検証してから `dist/-<バージョン>.zip` を作成します。内容は zip のルートに置かれ、署名付きカタログのパッケージと同じ構成です。`tests/`、`pyproject.toml`、`uv.lock`、`dist/`、`.venv`、`.git`、`node_modules`、各種キャッシュ、`.pyc` は除外されます。ファイルの順序とタイムスタンプは固定なので、同じ内容からは同じ sha256 が得られます。 ## dev @@ -50,7 +52,9 @@ ab-plugin pack [path] [-o dist] ab-plugin dev [path] [--config-dir config] ``` -- プラグインディレクトリを `/plugins/local/` にシンボリックリンクします。 +- 検証してから、プラグインディレクトリを `/plugins/local/` にシンボリックリンクします。 +- `--config-dir` の既定値はカレントディレクトリの `config` です。プラグインディレクトリで実行する場合は、AB の設定ディレクトリ(ソースから実行する場合は `backend/src/config`)を指定します。 +- シンボリックリンクはプラグインディレクトリの絶対パスを指します。AB が同じパスでそこを開ける必要があります(ソースから実行する AB など)。AB を Docker で動かす場合は、プラグインディレクトリをコンテナの `config/plugins/local//` にコピーまたはマウントし、設定 → プラグイン で「未署名プラグインを許可」をオンにしてから有効にします。 - ホストの設定ファイル(`config_dev.json`、なければ `config.json`)に `plugins.dev_mode`、`plugins.allow_unsigned`、`plugins.enabled.` を書き込みます。 - 設定ファイルがないと実行を拒否します。先に AutoBangumi を一度起動してください。 - 動作中の AB は設定ファイルを再読み込みしません。一度再起動してください。以後、`dev_mode` が 1 秒ごとにローカルプラグインのディレクトリを確認し、ファイルが変わるとそのプラグインをリロードします。読み込みに失敗したプラグインも監視されるので、ソースを直すと自動で復旧します。 @@ -61,8 +65,9 @@ ab-plugin dev [path] [--config-dir config] ```bash ab-plugin new my-notify --kind notifier cd my-notify +uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # 一度だけ uv run pytest # コントラクトテスト -ab-plugin dev . # リンクしてホットリロードを有効化。AB を一度再起動 +ab-plugin dev . --config-dir /path/to/autobangumi/config # リンクしてホットリロードを有効化。AB を一度再起動 # コードを変更 → AB が自動でリロード → WebUI で確認 ab-plugin pack . ``` diff --git a/docs/ja/dev/plugins/examples.md b/docs/ja/dev/plugins/examples.md index dce14abd..98c32303 100644 --- a/docs/ja/dev/plugins/examples.md +++ b/docs/ja/dev/plugins/examples.md @@ -21,10 +21,12 @@ cp -r examples/plugins/ntfy-notifier config/plugins/local/ ## サンプルのテストを実行する +リポジトリのルートで実行します。スクリプトは各サンプルのマニフェストを検証し、そのテストを実行します(CI と同じ)。 + ```bash -cd examples/plugins/ntfy-notifier && uv run pytest -# すべてのサンプルを実行する場合(リポジトリのルートで): scripts/test_example_plugins.sh ``` +スクリプトは `backend/` の環境で動き、そこには SDK が編集可能モードでインストールされています。サンプルの `pyproject.toml` は `autobangumi-sdk` に依存しますが、このパッケージは PyPI にありません。サンプルをリポジトリの外にコピーして単独でテストする場合は、先にサンプルのディレクトリで `uv add ` を実行してから `uv run pytest` を実行します。 + Jellyfin / Emby / Plex のライブラリ更新にプラグインを書く必要はありません。組み込みプラグイン `media-server-refresh` が対応しています。 diff --git a/docs/ja/dev/plugins/points/message-template.md b/docs/ja/dev/plugins/points/message-template.md index 30f49fd2..992134df 100644 --- a/docs/ja/dev/plugins/points/message-template.md +++ b/docs/ja/dev/plugins/points/message-template.md @@ -21,5 +21,5 @@ class Alerts(Plugin): - フックが影響するのは外部へのプッシュだけです。通知センターの文面は、フロントエンドが `i18n()` から描画するので変わりません。 - フックは、有効な各チャンネルに対して 1 回ずつ呼ばれます。`channel` はチャンネルの種類で、例えば `telegram` です。 -- フックが失敗またはタイムアウトした場合は、前の段階の文面を使い、失敗はブレーカーに数えられます。戻り値の型が違うと、メッセージ全体が既定の文面に戻ります。 +- フックが失敗した、タイムアウトした、または `RenderedMessage` 以外を返した場合は、前の段階の文面を使い、失敗はブレーカーに数えられます。後続のフックは通常どおり実行されます。 - 「新しい話」の通知は、まだこのフックを通りません。通知チャンネルに設定した 1 話用のテンプレートを使います。 diff --git a/docs/ja/dev/plugins/sdk.md b/docs/ja/dev/plugins/sdk.md new file mode 100644 index 00000000..7199e967 --- /dev/null +++ b/docs/ja/dev/plugins/sdk.md @@ -0,0 +1,80 @@ +# SDK の入手 + +プラグイン SDK は PyPI に公開しません。4.0 の各 beta / 正式版の [GitHub Release](https://github.com/EstrellaXD/Auto_Bangumi/releases) に、プラグイン作者向けのファイルが 2 つ添付されます。 + +| ファイル | 内容 | +| --- | --- | +| `autobangumi_sdk--py3-none-any.whl` | `ab_sdk` パッケージと `ab-plugin` コマンド | +| `autobangumi-plugin-skill-.zip` | AI コーディングアシスタント向けのプラグイン開発 skill | + +wheel のファイル名に入るのは SDK のバージョン(`ab_sdk.SDK_VERSION`、例えば `0.5.0`)で、AB のバージョンではありません。対象とする AB バージョンの Release から wheel をダウンロードしてください。両者の SDK バージョンが一致します。プラグインのマニフェストの `sdk` 範囲(例えば `">=0.5,<1"`)は、このバージョンを含む必要があります。 + +## wheel のインストール + +wheel には Python 3.13 以上が必要です。依存は `pydantic`、`httpx`、`packaging` で、AutoBangumi 本体のインストールは不要です。 + +コマンドだけを使う場合は、グローバルなツールとしてインストールします。 + +```bash +uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl +ab-plugin --help +``` + +プラグインを書いてテストを実行する場合は、wheel をプラグインプロジェクトの依存に追加します。 + +```bash +cd my-plugin +uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl +uv run pytest +``` + +`uv add` は `pyproject.toml` の `[tool.uv.sources]` に wheel のローカルパスを記録します。`ab-plugin pack` は `pyproject.toml` と `uv.lock` をパッケージに含めないため、このパスはプラグインパッケージに入りません。`ab-plugin new` が作る `pyproject.toml` は `autobangumi-sdk` に依存していますが、入手元を指定していません。新しいひな形でも、先に一度 `uv add` を実行してください。 + +コントラクトテストには pytest が必要です。ひな形の `dev` 依存グループには pytest が入っています。オプション依存 `autobangumi-sdk[test]` をインストールすることもできます。 + +SDK を更新するときは、新しい Release の wheel をダウンロードし、`uv tool install --force <新しい wheel>` または `uv add <新しい wheel>` をもう一度実行します。 + +## AB のリポジトリで開発する + +SDK のソースは `backend/src/ab_sdk`、パッケージ設定は `backend/sdk/pyproject.toml` にあります。`backend` の `dev` 依存グループが SDK を編集可能モードでインストールするため、リポジトリ内では wheel は不要です。 + +```bash +cd backend +uv sync --group dev +uv run ab-plugin validate ../examples/plugins/ntfy-notifier +``` + +wheel を自分でビルドする場合(CI と同じコマンド): + +```bash +uv build --wheel backend/sdk --out-dir sdk-dist +``` + +## プラグイン開発 skill + +skill は Markdown の説明の集まりです。AI コーディングアシスタントはこれを読んで、AB プラグインを書き、テストし、パッケージ化します。zip には `autobangumi-plugin/` ディレクトリが入っています。 + +``` +autobangumi-plugin/ +├── SKILL.md # 作業手順、拡張ポイント一覧、よくある間違い +└── references/ + ├── extension-points.md # 各拡張ポイントのシグネチャとフィールド + ├── frontend.md # フロントエンドスロット + └── testing.md # コントラクトテストと create_plugin +``` + +アシスタントの skills ディレクトリに展開します。Claude Code の場合、個人用の skills ディレクトリは `~/.claude/skills/` です。 + +```bash +unzip autobangumi-plugin-skill-.zip -d ~/.claude/skills/ +``` + +ほかのアシスタントでは、それぞれの skills ディレクトリに置いてください。skill のソースはリポジトリの `skills/autobangumi-plugin/` です。 + +## CI でのビルド + +`.github/workflows/build.yml` の `release` ジョブは、メンテナーがバージョンタグを push したときだけ実行されます。 + +1. `uv build --wheel backend/sdk --out-dir sdk-dist` で wheel をビルドします。バージョンは `ab_sdk.SDK_VERSION` から取ります。 +2. `skills/autobangumi-plugin` を `autobangumi-plugin-skill-.zip` に圧縮します。 +3. 2 つのファイルを、WebUI やオンライン更新バンドルなどと一緒に、そのバージョンの GitHub Release に添付します。beta(プレリリース)と正式版のどちらにも添付されます。 diff --git a/docs/ja/dev/plugins/signing.md b/docs/ja/dev/plugins/signing.md index d904e044..10b1b604 100644 --- a/docs/ja/dev/plugins/signing.md +++ b/docs/ja/dev/plugins/signing.md @@ -49,11 +49,7 @@ AB は GitHub release `plugins` から `catalog.json` をダウンロードし カタログのプラグインが依存できるのは、標準ライブラリと AB に既にあるパッケージだけです。AB は pip を実行しません。サードパーティの純 Python ライブラリは `vendor/` に入れてください。 -::: warning WebUI にはまだカタログの画面がありません -4.0 のプレビュー期間中、カタログの閲覧とインストールは API だけです。設定 → プラグインのページには対応するボタンがありません。 -::: - -API: +WebUI では、**設定 → プラグイン** の「Plugin catalog」(插件目录)セクションでカタログを閲覧し、プラグインをインストール・更新・削除できます。API: | リクエスト | 説明 | | --- | --- | @@ -86,4 +82,4 @@ API: ## SDK の公開 -SDK は、4.0 beta / 正式版の GitHub Release の添付ファイルとして wheel で配布します。CI が `uv build --wheel backend/sdk` でビルドします。同じ release に `autobangumi-plugin-skill-<バージョン>.zip` も添付されます。AI コーディングアシスタント向けのプラグイン開発 skill です。アシスタントの skills ディレクトリに展開してください。 +SDK の wheel とプラグイン開発 skill は CI がビルドし、4.0 の各 beta / 正式版の GitHub Release に添付します。メンテナーが手動で公開する必要はありません。ダウンロードとインストールは [SDK の入手](/ja/dev/plugins/sdk) を参照してください。