Kyoo 5.2.1 API 使用详解
Kyoo 5.2.1 API 对接手册:四服务 81 条路由全景、会话/API Key/OIDC 三通道认证全流程实测(JWT payload 结构与登出)、seed 手动注册视频、info→direct 播放链路 302/206 实测回显、Python 与 Bash 集成封装、401/403/422/500 错误语义与五个集成坑实录。
Kyoo 5.2.1 API 使用详解
目标:给需要对接或二次开发 Kyoo 的读者一份可落地的 API 手册——认证三通道(会话 token / API Key / OIDC)全流程实测、81 条路由全景表、媒体库/播放/用户管理等核心端点逐一实测并附真实回显、错误码语义与 SDK 封装建议;全部请求在 Ubuntu 24.04 实测环境以 curl 完成并逐行照抄回显。
一、环境信息
- 操作系统:Ubuntu 24.04.5 LTS,Kyoo 5.2.1 Docker Compose 全栈(7 服务)
- API 入口:
http://localhost:8136(Traefik 宿主映射端口,容器内 8901) - 客户端工具:curl 8.9.1、python3.12(requests 库仅在集成示例使用)
- 演示账号:admin / q1w2e3r4(首个注册用户,管理员权限);另注册普通用户 demo1 用于权限对照
- 实测样本:Big Buck Bunny (2008).mp4(已通过 seed API 注册,进入未匹配队列)
- API 文档探查方式:在线 OpenAPI 实测抓取(
/swagger页面内嵌配置,见 2.1)+ 官方源码路由定义双重印证
二、前置条件
- 已完成部署且
http://localhost:8136/api/ready返回 healthy(见部署指南篇 4.5); - 已注册管理员账号(首个注册用户自动获得全量权限);
- 明确 Kyoo 的 API 服务边界:业务功能分散在四个服务,统一经 Traefik 单入口(8136)暴露——api(媒体库)、auth(认证)、scanner(扫描触发)、transcoder(视频流);本文所有路径都基于该单入口,无需直连容器端口;
- 理解「phantom-token」机制(后文 3.1):客户端凭证只在 Traefik 认证中间件处使用一次,转发到业务服务前已被换成正式 JWT,业务服务只见 JWT。
三、参数说明
3.1 认证参数:三种凭证的传递方式
| 凭证类型 | 获取方式 | 请求头形态 | 有效期 | 适用场景 |
|---|---|---|---|---|
| 会话 token | POST /auth/sessions 登录 | Authorization: Bearer <token> | 至少 24h(auth 默认) | 浏览器/个人脚本 |
| API Key | POST /auth/keys 创建 | X-Api-Key: <key> | 长期(删前有效) | 服务端集成/自动化 |
| OIDC | 外部 IdP 登录后回 Kyoo 换会话 | 同会话 token | 随会话 | 企业 SSO |
三个细节实测确认:
- 凭证放 Cookie 也可以:Traefik 的 authRequestHeaders 实测为
Authorization,Cookie,X-Api-Key,Sec-WebSocket-Protocol,即四种头都可携带凭证; - JWT 是 RS256 签名:公钥经
GET /.well-known/jwks.json发布(实测可访问),第三方网关可自行验签; - 会话与 API Key 的权限同源:都来自 JWT 的 claims(
permissions数组),api 服务只认 claim 不认凭证形态。
3.2 请求参数:路径/查询/分页三要素
- 路径参数:媒体 id 为 UUID v4(实测
adea946d-2258-4a9f-832a-b28d2e8f287a),条目 slug 为字符串; - 查询参数:媒体库列表类接口统一为
?after=<id>&first=<n>形态(游标分页,官方 OpenAPI 一致声明),不是 page/offset; - 响应信封:列表接口实测统一返回
{items, this, next}三字段——next为下一页 URL 或 null。
3.3 响应参数:统一错误结构
业务错误实测统一为四字段 JSON(status/message/details),HTTP 状态码承载主语义(4.x 节有 401/404 实测样例;正确场景返回资源 JSON,无信封包裹)。
四、基础操作
4.1 认证流程 A:账号密码登录换会话 token
注册与登录两步走(注册接口首次调用即建管理员):
# 1. 注册(仅首个用户为管理员,重复注册为普通用户)
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/auth/users \
-H 'Content-Type: application/json' \
-d '{"username":"admin","email":"admin@example.com","password":"q1w2e3r4"}'
{"id":"3dcefdeb-254b-40f9-beeb-e1d7dd547241","createdDate":"2026-10-29T10:13:23.469473Z","lastUsed":"2026-10-29T10:13:23.469473Z","device":"curl","token":"W5zY5ejXbN56Ugj5goFkNkWjNapfZxfJ6_BnQ77EBzjeFWmWoO-Ygs74MGidjqOfuU8md1n2V-G4m7Rfd25_Rw"}
[201]
# 2. 登录已有账号
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/auth/sessions \
-H 'Content-Type: application/json' \
-d '{"login":"admin","password":"q1w2e3r4"}'
{"id":"78e78078-6c80-419a-986c-f1ac01a56f6d","createdDate":"2026-10-29T10:34:18.100827Z","lastUsed":"2026-10-29T10:34:18.100827Z","device":"curl","token":"VA-pPK1MRZVcyu6GqELk4yP4AcxXeXpAxtU3Rui2svv3kqQAy3Fjidw5l06IuiTfsfjYyQD9F2jKx5vK4AoZUg"}
[201]
字段说明:token 即会话凭证(后续所有 Bearer 的值);device 是登录客户端标识;id 为会话 id(可在会话管理接口精确登出,见 4.3)。注意登录接口的用户名字段是 login 不是 username——实测传错字段返回 422(见 8.1 Q1),登录 DTO 定义在官方源码 auth/sessions.go 的 LoginDto 结构体(json tag 为 login)。
4.2 认证流程 B:API Key 创建与使用
实测(正确 DTO 为 name + claims,顶层 permissions 会触发 500 空报错,见排障节):
# 创建(管理员会话)
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/auth/keys \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"docdemo","claims":{"permissions":["core.read","core.play"]}}'
{"id":"ee1691bb-0946-4e90-81a3-8b176121282a","name":"docdemo","claims":{"permissions":["core.read","core.play"]},"token":"y8WgTX_qztDG-uJQbW8vZCp2aVN4g9Y82DE9Ml8HnPsNggaR-DAvPUNnJ_3mIqlagUBDUXwjwxpbwAxqAv3E9w"}
[201]
# 使用(X-Api-Key 调媒体库)
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8136/api/shows \
-H "X-Api-Key: y8WgTX_qztDG-uJQbW8vZCp2aVN4g9Y82DE9Ml8HnPsNggaR-DAvPUNnJ_3mIqlagUBDUXwjwxpbwAxqAv3E9w"
200
API Key 的 token 只在创建响应出现一次(后续 GET /auth/keys 列表不含 token,实测确认),务必落库保存。
4.3 认证流程 C:JWT 自验与登出
# 用会话换正式 JWT(Traefik 内部做的事,手动可复现)
$ curl -s http://localhost:8136/auth/jwt -H "Authorization: Bearer $TOKEN" | cut -d. -f2 | python3 -c "import sys,base64,json;s=sys.stdin.read().strip();s+='='*(-len(s)%4);print(json.dumps(json.loads(base64.urlsafe_b64decode(s)),ensure_ascii=False))" | head -c 300
{"exp": 1793273658, "iat": 1793270058, "iss": "http://localhost:8136", "jti": "b32056cb-91cd-4e81-9d06-9aa52d28f7ba", "permissions": ["users.read", "users.write", "users.delete", "apikeys.read", "apikeys.write", "core.read", "core.write", "core.play", "scanner.trigger", "scanner.guess", "scanner.sea
(回显经 head -c 300 截断,permissions 数组完整内容与 FIRST_USER_CLAIMS 一致。)
JWT payload 实测结构:exp(过期)、iat(签发时间)、iss(= PUBLIC_URL)、jti(token 唯一 id)、顶层平铺的 permissions 数组——权限直接以 claim 形式内嵌,第三方集成据此判断即可,无需回查数据库。
# 登出指定会话(按会话 id 删除,id 取登录响应的 id 字段)
$ curl -s -w "\n[%{http_code}]\n" -X DELETE http://localhost:8136/auth/sessions/78e78078-6c80-419a-986c-f1ac01a56f6d \
-H "Authorization: Bearer $TOKEN"
{"id":"78e78078-6c80-419a-986c-f1ac01a56f6d","createdDate":"2026-10-29T10:34:18.100827Z","lastUsed":"2026-10-29T10:34:18.100827Z","device":"curl"}
[200]
五、API 全景清单
5.1 路由全景:四服务 81 条
实测统计口径:api 服务 OpenAPI 56 条 + transcoder 7 条 + auth 源码 18 条(scanner 3 条经源码确认)。按服务分组的端点总览(完整方法×路径见 /swagger 页面,此处给功能分组表):
| 服务 | 分组 | 代表端点(方法 省略前缀) | 权限要求 |
|---|---|---|---|
| api | 媒体库 | GET /shows、GET /shows/{slug}、GET /movies、GET /entries | core.read |
| api | 条目内嵌 | GET /entries/{slug}/episodes、/entries/{slug}/next-entry | core.read |
| api | 播放记录 | GET /profiles/me/history、POST /profiles/me/history、GET /profiles/me/nextup | core.play |
| api | 观看清单 | GET/POST/DELETE /profiles/me/watchlist | core.play |
| api | 视频源 | GET /videos/{id}/info、/videos/{id}/direct、/videos/{id}/master.m3u8 | core.play |
| api | 视频 seed | POST /videos、GET /videos/unmatched、GET /videos/guesses | core.write |
| api | 人员/工作室/流派 | GET /staff、/studios、/genres/{id}/entries | core.read |
| api | 通知与设置 | GET /events、GET/PUT /profiles/me/settings | core.read |
| api | 文档 | GET /swagger | 公开 |
| transcoder | 视频流 | GET /video/{slug}/direct、/master.m3u8、/{slug}/{audio}/{chapter}.avc... | core.play |
| transcoder | 元数据 | GET /video/{slug}/info、/thumb.png、/thumbnails.png、/thumbnails.vtt | core.play |
| transcoder | 分段流 | GET /video/{slug}/{audio}/{chapter}.vtt | core.play |
| auth | 注册登录 | POST /users、POST /sessions、DELETE /sessions、PATCH /users/me/password | 公开/用户 |
| auth | 会话管理 | GET /users/me/sessions、GET /users/{id}/sessions、DELETE /sessions/{id} | 用户/管理员 |
| auth | API Key | GET/POST /keys、DELETE /keys/{id} | apikeys.write |
| auth | 用户管理 | GET/PATCH/DELETE /users、/users/{id}、POST /users/me/logo | users.* |
| auth | OIDC | GET /oidc/login/{provider}、/oidc/callback/{provider}、DELETE /oidc/login/{provider} | 公开/用户 |
| auth | 公钥与发现 | GET /.well-known/jwks.json、/.well-known/openid-configuration、GET /info | 公开 |
| scanner | 扫描 | POST /scanner/run、GET /scanner/refresh、GET /scanner/openapi.json | scanner.trigger |
5.2 OpenAPI 文档获取
Kyoo 的 API 文档页在 http://localhost:8136/swagger(实测 200,Scalar UI),页面配置内嵌了三份 spec 地址——四服务各有自己的 OpenAPI 文档:
$ curl -s http://localhost:8136/swagger/ | grep -oE '/[a-z]+/(swagger/doc\.json|openapi\.json)'
/api/swagger/doc.json
/auth/swagger/doc.json
/scanner/openapi.json
/video/swagger/doc.json
实测注意:auth 的 spec 直连返回 500({"status":500,"message":"Error on load swagger:...","details":null}),api 的 56 条路由实测可用:
$ curl -s http://localhost:8136/api/swagger/doc.json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(d['info'])
print('paths:', len(d['paths']))"
{'title': 'Kyoo API', 'description': 'Kyoo media browser API (unofficial doc)', 'version': '5.0.0'}
paths: 56
六、核心 API 实测
6.1 媒体库:列表与详情
# 空库列表(游客可读)
$ curl -s http://localhost:8136/api/shows
{"items":[],"this":"http://127.0.0.1:8136/api/shows","next":null}
# 详情:不存在的 slug
$ curl -s -w "\n[%{http_code}]\n" http://localhost:8136/api/shows/not-exist-slug
{"status":404,"message":"No show found with the slug or id: not-exist-slug","details":null}
[404]
返回信封解说:items 数组 + this 当前页 URL + next 游标(空库或末页为 null)。实测环境影库为空(scanner 外网受限未跑扫描),404 文案确认了 slug/id 双定位语义。
6.2 视频 seed:手动注册媒体文件(不依赖 scanner)
这是 Kyoo 最适合集成的口子——绕过目录扫描直接把视频写入媒体库。实测三连(先登录拿管理员 token):
# 1. 注册视频(rendering 为容器内路径的 sha1,part=null 单文件)
$ RENDER=$(printf '/video/Big Buck Bunny (2008).mp4' | sha1sum | cut -d' ' -f1)
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/api/videos \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "[{\"path\":\"/video/Big Buck Bunny (2008).mp4\",\"rendering\":\"$RENDER\",\"part\":null,\"version\":1,\"guess\":{\"title\":\"Big Buck Bunny\",\"kind\":\"movie\",\"years\":[2008],\"from\":\"kyoo-doc-demo\",\"history\":[]}}]"
{"id":"adea946d-2258-4a9f-832a-b28d2e8f287a","path":"/video/Big Buck Bunny (2008).mp4","rendering":"9f784aa9c03cbeaaf95ff595519ea7265c6e040d","part":null,"version":1,"guess":{"title":"Big Buck Bunny","kind":"movie","years":[2008],"from":"kyoo-doc-demo","history":[]}}
[201]
# 2. 未匹配队列
$ curl -s http://localhost:8136/api/videos/unmatched -H "Authorization: Bearer $TOKEN"
{"items":[{"id":"adea946d-2258-4a9f-832a-b28d2e8f287a","path":"/video/Big Buck Bunny (2008).mp4","rendering":"9f784aa9c03cbeaaf95ff595519ea7265c6e040d","part":null,"version":1,"guess":{"from":"kyoo-doc-demo","kind":"movie","title":"Big Buck Bunny","years":[2008],"history":[]},"createdAt":"2026-10-29T10:15:40.044Z","updatedAt":"2026-10-29T10:15:40.044Z"}],"this":"http://127.0.0.1:8136/api/videos/unmatched","next":null}
# 3. 文件名识别结果
$ curl -s http://localhost:8136/api/videos/guesses -H "Authorization: Bearer $TOKEN"
{"paths":["/video/Big Buck Bunny (2008).mp4"],"guesses":{},"unmatched":["/video/Big Buck Bunny (2008).mp4"]}
入参解说:POST /api/videos 接受 SeedVideo 数组(一次可批量注册多个文件);rendering 为 transcoder 内的渲染 id(算法为容器内路径的 sha1,实测 printf '/video/Big Buck Bunny (2008).mp4' | sha1sum 结果与响应逐字一致:9f784aa9c03cbeaaf95ff595519ea7265c6e040d);guess 为预填的识别结果(kind 支持 movie 等类别值)。未匹配队列的每条记录带服务端生成的 id 与 createdAt/updatedAt 时间戳;guesses 端点返回 paths/guesses/unmatched 三段结构——guesses 为空表示尚无 scanner 识别历史(本环境 scanner 受外网限制未运行)。
另一个实测发现:GET /api/videos/unmatched 不带凭证也可访问(返回 200,游客权限 core.read 放行),批量写操作才需要 core.write。
6.3 观看进度与继续观看
播放记录类接口实测(当前库内无已识别条目,用不存在的 slug 触发校验器观察入参契约):
# 上报观看进度(载荷缺 videoId,用 slug 字段触达校验)
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/api/profiles/me/history \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '[{"slug":"some-episode","percent":45.5,"addedDate":"2026-10-29T12:00:00Z","playedDate":"2026-10-29T12:00:00Z"}]'
{"status":422,"message":"Validation error.","details":{"type":"body","validator":{"schema":{"type":"array","items":{"type":"object","allOf":[{"type":"object","required":["percent","time","playedDate","videoId"],"properties":{"percent":{"minimum":0,"maximum":100,"anyOf":[{"format":"integer","default":0,"type":"string"},{"minimum":0,"maximum":100,"type":"integer"}]},"time":{"minimum":0,"description":"When this episode was stopped (in seconds since the start). This value is null if the entry was never watched or is finished.","anyOf":[{"format":"integer","default":0,"type":"string"},{"minimum":0,"description":"When this episode was stopped (in seconds since the start). This value is null if the entry was never watched or is finished."
[422]
(回显经截断,details.validator.schema 为校验器原文节选。)
实测要点:合法载荷必须包含 percent、time、playedDate、videoId 四个字段(校验器 required 原文),videoId 指向 videos 表记录 id 而非条目 slug——集成方上报进度前需先通过 6.2/6.4 拿到视频 id。
# 空历史的继续观看
$ curl -s http://localhost:8136/api/profiles/me/nextup -H "Authorization: Bearer $TOKEN"
{"items":[],"this":"http://127.0.0.1:8136/api/profiles/me/nextup","next":null}
6.4 播放链路:info → direct 分片直连
完整播放链路实测(这是浏览器播放器的实际工作方式):
# 1. 询问播放信息 → 302 重定向
$ curl -s -I http://localhost:8136/api/videos/adea946d-2258-4a9f-832a-b28d2e8f287a/info \
-H "Authorization: Bearer $TOKEN" | grep -iE 'HTTP/|location'
HTTP/1.1 302 Found
Location: /video/L3ZpZGVvL0JpZyBCdWNrIEJ1bm55ICgyMDA4KS5tcDQ/info
# 2. 跟随重定向拿 ffprobe 元数据
$ curl -s -L http://localhost:8136/api/videos/adea946d-2258-4a9f-832a-b28d2e8f287a/info \
-H "Authorization: Bearer $TOKEN" | head -c 600
{"path":"/video/Big Buck Bunny (2008).mp4","container":"mov,mp4,m4a,3gp,3g2,mj2","duration":10.0,"video":{"codec":"h264","language":"und","duration":10.0,"bitrate":3997620,"width":1280,"height":720,"aspectRatio":1.7777778,"frameRate":"25/1","pixelFormat":"yuv420p"},"audio":[{"codec":"aac","language":"eng","duration":10.0,"bitrate":69392,"channels":2,"sampleRate":"44100","default":true,"forced":false}],"fonts":[],"chapters":[]}
# 3. 原画质直连(Range 请求分片)
$ curl -s -o /dev/null -L -w "final:%{http_code} size:%{size_download} type:%{content_type}\n" \
http://localhost:8136/api/videos/adea946d-2258-4a9f-832a-b28d2e8f287a/direct \
-H "Authorization: Bearer $TOKEN" -r 0-1000
final:206 size:1001 type:video/mp4
链路解说:
Location头里的 base64url 串L3ZpZGVv...解码即/video/Big Buck Bunny (2008).mp4——transcoder 以「媒体路径 base64url」为资源定位,与 6.2 的 rendering sha1 是并存的两种寻址(API 层用 id,流层用路径);info返回 ffprobe 结果的结构化 JSON:视频轨 h264 1280×720、音轨 aac 双声道,前端据此决定直连还是转码(能直接播的容器/编码走 direct,否则请求 master.m3u8 HLS 多码率);direct支持 HTTP Range(实测 206 + 1001 字节分片),前端 mp4 渐进式加载。
6.5 用户管理:管理员与普通用户对照
# 用户列表(管理员)
$ curl -s http://localhost:8136/auth/users -H "Authorization: Bearer $TOKEN" | head -c 300
{"items":[{"id":"b1d2fc8d-1f73-4c8a-a132-b966450e5e38","username":"admin","email":"admin@example.com","hasPassword":true,"createdDate":"2026-10-29T10:13:23.467403Z","lastSeen":"2026-10-29T10:34:18.28068Z","claims":{"permissions":["users.read","users.write","users.delete","apikeys.read","apikeys.writ
(回显经 head -c 300 截断;claims 字段即该用户的权限 JSON,lastSeen 为最近活跃时间。)
再注册一个普通用户 demo1 做权限对照(非首个注册,默认 claims 为 EXTRA_CLAIMS):
# 注册
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/auth/users \
-H 'Content-Type: application/json' \
-d '{"username":"demo1","email":"demo1@example.com","password":"q1w2e3r4"}'
{"id":"2192ac55-9d65-4032-883b-61bf3bbcb893","createdDate":"2026-10-29T10:36:01.467754Z","lastUsed":"2026-10-29T10:36:01.467754Z","device":"curl","token":"FoABpbFSgtSO2DURyuFXX-UHJUF9ssUDRYRT8Lcex-dThxSLbUKD6NEBWS8ieUuFoEm473Db94pCBVh30ywHPg"}
[201]
# demo1 会话访问用户列表 → 权限不足
$ curl -s -w "\n[%{http_code}]\n" http://localhost:8136/auth/users \
-H "Authorization: Bearer FoABpbFSgtSO2DURyuFXX-UHJUF9ssUDRYRT8Lcex-dThxSLbUKD6NEBWS8ieUuFoEm473Db94pCBVh30ywHPg"
{"status":403,"message":"Missing permissions: users.read.","details":null}
[403]
实测权限语义:/auth/users 需要 users.read 权限(缺失时 403 报错文案会精确指出缺哪个权限,便于集成方诊断);self 侧接口(/users/me*)任何登录用户可用。改密走 PATCH /users/me/password(载荷 oldPassword + newPassword,路由定义见官方源码 auth/main.go)。
6.6 其他常用端点速查(附实测状态码)
| 端点 | 实测状态码 | 说明 |
|---|---|---|
| GET /auth/info | 200 | 返回 publicUrl/allowRegister/oidc(无需认证) |
| GET /.well-known/openid-configuration | 200 | OIDC 发现文档(无需认证) |
| GET /api/health | 200 | {"status":"healthy"} |
| GET /api/ready | 200 | 含 database 字段的就绪探针 |
| GET /api/videos/unmatched | 200 | 无凭证也可读(游客 core.read 放行,见 6.2) |
| POST /scanner/run | 502 | scanner 未存活时的网关错误(排障见部署指南篇) |
| GET /auth/swagger/doc.json | 500 | auth spec 生成报错(已知问题,见 8.2) |
七、SDK 与代码集成示例
官方只提供 TypeScript 版官方应用与 Android 客户端(API 调用代码在 front 仓库内),无官方 Python/Go SDK,集成建议直接按 OpenAPI 或本文实测样例封装。
7.1 Python:requests 会话封装(面向媒体库只读集成)
import requests
BASE = "http://localhost:8136"
class Kyoo:
def __init__(self, base=BASE, apikey=None):
self.base, self.apikey = base, apikey
def _h(self):
return {"X-Api-Key": self.apikey} if self.apikey else {}
def shows(self, first=50):
r = requests.get(f"{self.base}/api/shows", params={"first": first},
headers=self._h(), timeout=10)
r.raise_for_status()
return r.json()["items"]
def play_info(self, video_id: str):
# 302 会携带 /video/<base64url>/info,跟随即得 ffprobe JSON
r = requests.get(f"{self.base}/api/videos/{video_id}/info",
headers=self._h(), timeout=10, allow_redirects=True)
return r.json()
if __name__ == "__main__":
kyoo = Kyoo(apikey="y8WgTX_...") # 换成你自己的 key(/auth/keys 创建)
print(kyoo.shows())
示例说明:服务端集成推荐 API Key(长期有效、权限可控),浏览器内嵌页面走 OIDC 会话;allow_redirects=True 让 requests 自动跟随 302 拿到 transcoder 的 info JSON。
7.2 Bash:登录 + 调用一条龙脚本
#!/usr/bin/env bash
# kyoo-call.sh —— 用账号密码调 Kyoo API 的最小脚本
set -euo pipefail
BASE="${BASE:-http://localhost:8136}"
TOKEN=$(curl -s -X POST "$BASE/auth/sessions" \
-H 'Content-Type: application/json' \
-d "{\"login\":\"${KYOO_USER:?}\",\"password\":\"${KYOO_PASS:?}\"}" \
| python3 -c "import json,sys;print(json.load(sys.stdin)['token'])")
curl -s "$BASE$1" -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 用法:KYOO_USER=admin KYOO_PASS=q1w2e3r4 ./kyoo-call.sh /api/shows
脚本解说:KYOO_USER/KYOO_PASS 从环境读取(避免密码落盘);token 只在脚本进程内使用;结束无登出步骤,会话可复用(用 DELETE /auth/sessions/{会话id} 可精确登出,见 4.3)。
八、常见问题
8.1 Q1:登录接口报 422,密码明明是对的
根因(实测踩坑):请求体用户名字段必须是 login,不是 username(注册接口才是 username)。解决:登录载荷改为 {"login":"admin","password":"..."}。实测错误回显:
$ curl -s -w "\n[%{http_code}]\n" -X POST http://localhost:8136/auth/sessions \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}'
{"status":422,"message":"Key: 'LoginDto.Login' Error:Field validation for 'Login' failed on the 'required' tag","details":null}
[422]
报错原文直接点名 LoginDto.Login(官方源码 auth/sessions.go 的登录 DTO),字段名对上即恢复 201。
8.2 Q2:GET /auth/swagger/doc.json 返回 500
现象(实测回显):{"status":500,"message":"Error on load swagger:...","details":null}。
定位与解决:auth 服务的 swagger 文档生成故障(已知问题),不影响其 API 本身——auth 路由以本文 5.1 表与官方源码 auth/main.go 383~427 行为准;媒体库 API 文档用 /api/swagger/doc.json 正常。
8.3 Q3:POST /auth/keys 返回 500 且 message 为空
根因:请求体 DTO 是 name + claims,把权限数组写到顶层 permissions 会触发 500。解决:{"name":"x","claims":{"permissions":["core.read"]}}(完整实测见部署指南篇 9.2)。
8.4 Q4:带 token 请求 /api 返回 403 而不是预期数据
排查顺序(实测归纳,两类错误要区分):
- 伪造/过期 token → 403 Invalid JWT(Traefik 认证中间件验签失败):
$ curl -s -w "\n[%{http_code}]\n" http://localhost:8136/api/entries/some-slug -H "Authorization: Bearer bad-token"
{"status":403,"message":"Invalid JWT","details":null}
[403]
- 凭证形态放错 → 401 Invalid bearer format(API Key 塞进 Bearer 头、或头格式不对):
$ curl -s -w "\n[%{http_code}]\n" http://localhost:8136/auth/users -H "Authorization: Bearer <空值>"
{"status":401,"message":"Invalid bearer format.","details":null}
[401]
- 改过 PUBLIC_URL 后旧 token 的 issuer 校验失败——重新登录(见配置详解篇 7.3);
- 401/403 之外还有第三种:有凭证但权限不够(如普通用户访问 /auth/users),报
Missing permissions: xxx(见 6.5)。
8.5 Q5:POST /api/videos 注册成功但页面看不到条目
根因:seed 进来的视频先落在 unmatched 队列(GET /api/videos/unmatched 可见),要成为媒体库条目需 scanner 完成识别入库(或继续调用后续匹配端点)。scanner 未存活时(外网依赖问题)视频会一直待在队列——这是 Kyoo「文件→条目」流水线的正常形态,非 bug。
九、参考链接
- 官方 API 文档(Scalar):https://kyoo.zoriya.dev/swagger
- OpenAPI spec(媒体库):http://localhost:8136/api/swagger/doc.json(本地实测,生产部署同理)
- JWKS 公钥:http://localhost:8136/.well-known/jwks.json
- 官方前端仓库(TS 调用参考实现):https://github.com/zoriya/Kyoo/tree/master/front
- 本系列篇目:部署见《Kyoo 5.2.1 Docker Compose 部署指南》,配置见《Kyoo 5.2.1 配置参数详解》,选型见《Kyoo 5.2.1 介绍与选型指南》。