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 错误语义与五个集成坑实录。

KyooKyoo媒体服务器流媒体自托管2026/10/29

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)+ 官方源码路由定义双重印证

二、前置条件

  1. 已完成部署且 http://localhost:8136/api/ready 返回 healthy(见部署指南篇 4.5);
  2. 已注册管理员账号(首个注册用户自动获得全量权限);
  3. 明确 Kyoo 的 API 服务边界:业务功能分散在四个服务,统一经 Traefik 单入口(8136)暴露——api(媒体库)、auth(认证)、scanner(扫描触发)、transcoder(视频流);本文所有路径都基于该单入口,无需直连容器端口;
  4. 理解「phantom-token」机制(后文 3.1):客户端凭证只在 Traefik 认证中间件处使用一次,转发到业务服务前已被换成正式 JWT,业务服务只见 JWT。

三、参数说明

3.1 认证参数:三种凭证的传递方式

凭证类型获取方式请求头形态有效期适用场景
会话 tokenPOST /auth/sessions 登录Authorization: Bearer <token>至少 24h(auth 默认)浏览器/个人脚本
API KeyPOST /auth/keys 创建X-Api-Key: <key>长期(删前有效)服务端集成/自动化
OIDC外部 IdP 登录后回 Kyoo 换会话同会话 token随会话企业 SSO

三个细节实测确认:

  1. 凭证放 Cookie 也可以:Traefik 的 authRequestHeaders 实测为 Authorization,Cookie,X-Api-Key,Sec-WebSocket-Protocol,即四种头都可携带凭证;
  2. JWT 是 RS256 签名:公钥经 GET /.well-known/jwks.json 发布(实测可访问),第三方网关可自行验签;
  3. 会话与 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 /entriescore.read
api条目内嵌GET /entries/{slug}/episodes、/entries/{slug}/next-entrycore.read
api播放记录GET /profiles/me/history、POST /profiles/me/history、GET /profiles/me/nextupcore.play
api观看清单GET/POST/DELETE /profiles/me/watchlistcore.play
api视频源GET /videos/{id}/info、/videos/{id}/direct、/videos/{id}/master.m3u8core.play
api视频 seedPOST /videos、GET /videos/unmatched、GET /videos/guessescore.write
api人员/工作室/流派GET /staff、/studios、/genres/{id}/entriescore.read
api通知与设置GET /events、GET/PUT /profiles/me/settingscore.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.vttcore.play
transcoder分段流GET /video/{slug}/{audio}/{chapter}.vttcore.play
auth注册登录POST /users、POST /sessions、DELETE /sessions、PATCH /users/me/password公开/用户
auth会话管理GET /users/me/sessions、GET /users/{id}/sessions、DELETE /sessions/{id}用户/管理员
authAPI KeyGET/POST /keys、DELETE /keys/{id}apikeys.write
auth用户管理GET/PATCH/DELETE /users、/users/{id}、POST /users/me/logousers.*
authOIDCGET /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.jsonscanner.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

链路解说:

  1. Location 头里的 base64url 串 L3ZpZGVv... 解码即 /video/Big Buck Bunny (2008).mp4——transcoder 以「媒体路径 base64url」为资源定位,与 6.2 的 rendering sha1 是并存的两种寻址(API 层用 id,流层用路径);
  2. info 返回 ffprobe 结果的结构化 JSON:视频轨 h264 1280×720、音轨 aac 双声道,前端据此决定直连还是转码(能直接播的容器/编码走 direct,否则请求 master.m3u8 HLS 多码率);
  3. 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/info200返回 publicUrl/allowRegister/oidc(无需认证)
GET /.well-known/openid-configuration200OIDC 发现文档(无需认证)
GET /api/health200{"status":"healthy"}
GET /api/ready200含 database 字段的就绪探针
GET /api/videos/unmatched200无凭证也可读(游客 core.read 放行,见 6.2)
POST /scanner/run502scanner 未存活时的网关错误(排障见部署指南篇)
GET /auth/swagger/doc.json500auth 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 而不是预期数据

排查顺序(实测归纳,两类错误要区分):

  1. 伪造/过期 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]
  1. 凭证形态放错 → 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]
  1. 改过 PUBLIC_URL 后旧 token 的 issuer 校验失败——重新登录(见配置详解篇 7.3);
  2. 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。

九、参考链接