ryot v10.5.6 API 使用详解
ryot 唯一程序化接口 GraphQL API 的 curl 实测详解:端点与认证、46 查询/70 变更字段规模、搜索/评论/进度/集合/用户五域逐条请求回显,含联合类型、异步作业与排障 FAQ。
ryot v10.5.6 API 使用详解
目标:讲清 ryot v10.5.6 唯一的程序化接口——GraphQL API 的端点、认证、核心查询/变更域与通用约定。本文全部请求与响应均为对
http://127.0.0.1:8141运行实例的 curl 实测回显(含报错实录),覆盖搜索、评论、进度、集合、用户管理五个数据域。
一、环境信息
- 操作系统:Ubuntu 24.04.5 LTS(x86_64)
- ryot 版本:v10.5.6(
coreDetails.version实测) - API 端点:
POST http://127.0.0.1:8141/backend/graphql - 演示账号:
admin/q1w2e3r4(注册于本文实验环境) - 客户端工具:curl 8.5.0 + python3(JSON 格式化)
- 服务部署方式:见 《ryot Docker Compose 部署指南》
二、前置条件
- 服务健康检查通过:
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8141/health
# HTTP 200
- GraphQL 端点免认证可达(用于探测):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-d '{"query":"query { __typename }"}'
# {"data":{"__typename":"QueryRoot"}}
- 已创建管理员账号(见第四章)。业务查询与变更均要求携带
Authorization: Bearer <apiKey>。
三、API 总览
| 维度 | 结论(实测/实查) |
|---|---|
| 协议 | GraphQL(单一 POST 端点),无 REST 管理 API |
| 端点路径 | /backend/graphql(反向代理按 /backend* 前缀转发并剥前缀,误用 /graphql 返回前端 404 页,见第七章 Q1) |
| 版本 | 随服务版本 v10.5.6,经 coreDetails.version 查询 |
| 认证 | loginUser 换取不透明 apiKey,请求头 Authorization: Bearer <apiKey>;有效期由 USERS_TOKEN_VALID_FOR_DAYS 控制(默认 90 天) |
| Schema 规模 | QueryRoot 46 个查询字段、MutationRoot 70 个变更字段(__type introspection 实测计数) |
| 错误风格 | 业务错误走联合类型(如 `LoginResult = LoginError |
| 限流 | 自托管实例无内置限流约定,按部署侧(反向代理/防火墙)自行控制 |
核心枚举(introspection 实测全量值):
MediaLot(11):BOOK SHOW MOVIE ANIME MANGA MUSIC PODCAST AUDIO_BOOK VIDEO_GAME COMIC_BOOK VISUAL_NOVELMediaSource(19):IGDB TMDB TVDB VNDB CUSTOM METRON ITUNES ANILIST AUDIBLE SPOTIFY MUSIC_BRAINZ GIANT_BOMB HARDCOVER MYANIMELIST LISTENNOTES GOOGLE_BOOKS OPENLIBRARY MANGA_UPDATES YOUTUBE_MUSICEntityLot(10):GENRE PERSON REVIEW WORKOUT METADATA EXERCISE COLLECTION METADATA_GROUP WORKOUT_TEMPLATE USER_MEASUREMENTSeenState(4):DROPPED ON_A_HOLD COMPLETED IN_PROGRESS
四、认证与首次调用
4.1 创建首个管理员(registerUser)
ryot 无内置默认账号,首个管理员用 registerUser 创建,adminAccessToken 必须与服务环境变量 SERVER_ADMIN_ACCESS_TOKEN 一致:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-d '{"query":"mutation { registerUser(input: {data: {password: {username: \"admin\", password: \"q1w2e3r4\"}}, adminAccessToken: \"q1w2e3r4\"}) { __typename ... on StringIdObject { id } ... on RegisterError { error } } }"}'
# {"data":{"registerUser":{"__typename":"StringIdObject","id":"usr_0MfPIsb1mhEh"}}}
⚠️ 注意:
registerUser返回联合类型RegisterResult,必须用内联片段(... on StringIdObject)选取字段,直接{ id }会报Unknown field "id" on type "RegisterResult"(实测)。
4.2 登录换取令牌(loginUser)
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-d '{"query":"mutation { loginUser(input: {password: {username: \"admin\", password: \"q1w2e3r4\"}}) { __typename ... on ApiKeyResponse { apiKey } ... on LoginError { error } } }"}'
# {"data":{"loginUser":{"__typename":"ApiKeyResponse","apiKey":"UFPP7KKBIKI4P4NXZXVFX2Q2MNGWXODSEQ7TIOIKLWD7RNB6TVPQ===="}}}
每次登录返回新的 apiKey(实测两次登录值不同),旧令牌在有效期内仍可用。后续示例统一用:
TOKEN=UFPP7KKBIKI4P4NXZXVFX2Q2MNGWXODSEQ7TIOIKLWD7RNB6TVPQ====
4.3 认证调用示例
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { coreDetails { version pageSize signupAllowed } }"}'
# {"data":{"coreDetails":{"version":"v10.5.6","pageSize":20,"signupAllowed":true}}}
五、API 域详解
5.1 元数据搜索域(metadataSearch / metadataDetails)
metadataSearch 按外部数据源搜索并返回 ryot 内部实体 ID 列表。入参 MetadataSearchInput{lot, source, search: SearchInput{query,take,page}};返回 CachedSearchIdResponse{cacheId, response: IdResults{items: [String!]!, details{totalItems, nextPage}}}。实测搜索 AniList 源的《葬送的芙莉莲》:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { metadataSearch(input: {lot: ANIME, source: ANILIST, search: {query: \"Frieren\", take: 3}}) { cacheId response { items details { totalItems nextPage } } } }"}'
实测响应:
{"data":{"metadataSearch":{
"cacheId":"8f89fc3d-b2ff-4773-b2a4-53e4c22da86e",
"response":{
"items":["met_6E_SB2LoFh5M","met_FW4DT0WOgaIf","met_vBMNiC1G0Sng","met_XgNcNA2u4zCu","met_Qx6652sTAUkf","met_wG-YztoFAK8i"],
"details":{"totalItems":6,"nextPage":null}}}}}
💡 说明:
take: 3实返 6 条——上游源单页返回多于请求数时 ryot 原样透传,分页以details.nextPage为准。ANILIST源免第三方密钥即可搜索(本实例未配置任何数据源密钥)。
metadataDetails(metadataId: String!) 拉取详情(返回 CachedGraphqlMetadataDetailsResponse,内层 GraphqlMetadataDetails 共 30 个字段):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { metadataDetails(metadataId: \"met_6E_SB2LoFh5M\") { response { id title lot identifier source publishYear } } }"}'
# {"data":{"metadataDetails":{"response":{"id":"met_6E_SB2LoFh5M","title":"Sousou no Frieren","lot":"ANIME","identifier":"154587","source":"ANILIST","publishYear":2023}}}}
5.2 评论域(createOrUpdateReview / userMetadataDetails)
createOrUpdateReview(input: CreateOrUpdateReviewInput!):必填 entityId + entityLot(媒体实体用 METADATA),可选 rating(Decimal)、text、date、visibility,以及按媒体类型的进度字段(animeEpisodeNumber、showSeasonNumber/showEpisodeNumber、mangaChapterNumber 等)。返回 StringIdObject:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { createOrUpdateReview(input: {entityId: \"met_6E_SB2LoFh5M\", entityLot: METADATA, rating: 9.2, text: \"Frieren - a gentle story about time and memory.\"}) { id } }"}'
# {"data":{"createOrUpdateReview":{"id":"rev_YYRZRCrqERPX"}}}
回查用 userMetadataDetails(metadataId)(返回 UserMetadataDetails,含 hasInteracted/reviews/history/averageRating/seenByUserCount/inProgress/collections 等 13 个字段):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { userMetadataDetails(metadataId: \"met_6E_SB2LoFh5M\") { response { hasInteracted seenByUserCount averageRating reviews { id rating textOriginal postedOn } } } }"}'
实测响应:
{"data":{"userMetadataDetails":{"response":{
"hasInteracted":true,"seenByUserCount":1,"averageRating":"9.2",
"reviews":[{"id":"rev_YYRZRCrqERPX","rating":"9.2",
"textOriginal":"Frieren - a gentle story about time and memory.",
"postedOn":"2026-11-03T02:06:53.089932+00:00"}]}}}}
💡 说明:
rating等 Decimal 标量在 JSON 中序列化为字符串("9.2"),客户端解析需注意。
5.3 观看进度域(deployBulkMetadataProgressUpdate / updateSeenItem)
新建观看记录没有独立 create 变更,统一走批量进度变更 deployBulkMetadataProgressUpdate(input: [MetadataProgressUpdateInput!]!)。入参 {metadataId, change},change 为 Oneof 枚举:changeLatestState(SeenState) / changeLatestInProgress(Decimal) / createNewInProgress{startedOn + 进度字段} / createNewCompleted{withoutDates | startedOnDate | finishedOnDate | startedAndFinishedOnDate}(后三者携带 timestamp 与进度字段)。实测记一条「2026-11-03 完结 28 集」:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { deployBulkMetadataProgressUpdate(input: [{metadataId: \"met_6E_SB2LoFh5M\", change: {createNewCompleted: {finishedOnDate: {timestamp: \"2026-11-03T12:00:00Z\", animeEpisodeNumber: 28}}}}]) }"}'
# {"data":{"deployBulkMetadataProgressUpdate":true}}
回查 history 确认落库:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { userMetadataDetails(metadataId: \"met_6E_SB2LoFh5M\") { response { history { id state progress finishedOn } } } }"}'
# {"data":{"userMetadataDetails":{"response":{"history":[{"id":"see_EBlvS6dwO4Om","state":"COMPLETED","progress":"100","finishedOn":"2026-11-03T12:00:00+00:00"}]}}}}
updateSeenItem(input: UpdateSeenItemInput!) 仅更新既有记录(入参无实体字段,靠 seenId 定位),对不存在的 seenId 实测报错:
{"data":null,"errors":[{"message":"No seen found for this user and metadata","path":["updateSeenItem"]}]}
删除记录用 deleteSeenItem(seenId: String!)。
5.4 集合域(createOrUpdateCollection / deployAddEntitiesToCollectionJob / collectionContents)
创建集合(返回 StringIdObject,id 形如 col_*):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { createOrUpdateCollection(input: {name: \"Anime Favorites\", description: \"Top anime tracked with ryot API\"}) { id } }"}'
# {"data":{"createOrUpdateCollection":{"id":"col_6AvxdIXxODeS"}}}
userCollectionsList 实测返回 8 个集合:自定义的 Anime Favorites 之外,新用户自动带有 7 个默认集合(Completed / In Progress / Monitoring / Reminders / Watchlist / Custom / Owned)。
向集合加实体是异步作业 deployAddEntitiesToCollectionJob(input: ChangeCollectionToEntitiesInput{creatorUserId, collectionName, entities: [EntityWithLotInput!]!})(注意按集合名而非 id 引用):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { deployAddEntitiesToCollectionJob(input: {creatorUserId: \"usr_dXbDkWHbULeE\", collectionName: \"Anime Favorites\", entities: [{entityId: \"met_6E_SB2LoFh5M\", entityLot: METADATA}]}) }"}'
# {"data":{"deployAddEntitiesToCollectionJob":true}}
等待约 5 秒(高频调度周期)后用 collectionContents 验证:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { collectionContents(input: {collectionId: \"col_6AvxdIXxODeS\"}) { response { totalItems results { items { entityId entityLot } } } } }"}'
# {"data":{"collectionContents":{"response":{"totalItems":1,"results":{"items":[{"entityId":"met_6E_SB2LoFh5M","entityLot":"METADATA"}]}}}}}
5.5 用户与系统域(usersList / generateAuthToken)
usersList 为管理员能力,返回 BasicUserDetails{id, name, lot, isDisabled}(name 即用户 ID 字符串):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { usersList { id name lot } }"}'
实测响应(节选):
{"data":{"usersList":[
{"id":"usr_dXbDkWHbULeE","name":"usr_dXbDkWHbULeE","lot":"NORMAL"},
{"id":"usr_YIesAvHsO5E7","name":"usr_YIesAvHsO5E7","lot":"ADMIN"},
{"id":"usr_R-xjh1FQ5Vwa","name":"usr_R-xjh1FQ5Vwa","lot":"NORMAL"},
{"id":"usr_0MfPIsb1mhEh","name":"usr_0MfPIsb1mhEh","lot":"NORMAL"}]}}
generateAuthToken(无入参)为当前用户再签发一个令牌,实测:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { generateAuthToken }"}'
# {"data":{"generateAuthToken":"6A5JUWLFC4WH4UDH3MFDFGVEZTZAQDIFD7AYG2EUO7COPYXYESNA===="}}
六、分页/过滤/排序通用约定
- 分页:列表类入参统一嵌
SearchInput{query, take, page};响应统一带details{totalItems, nextPage},nextPage: null表示无下一页(实测见 5.1)。 - 过滤:
userMetadataList(input: UserMetadataListInput{lot, filter, search, sort}),filter为MediaFilter{general, source, dateRange, collections},其中MediaGeneralFilter实测枚举:ALL RATED UNRATED DROPPED ON_A_HOLD UNFINISHED。实测按「已评分」过滤:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { userMetadataList(input: {lot: ANIME, filter: {general: RATED}, search: {take: 5, page: 1}}) { response { items details { totalItems } } } }"}'
# {"data":{"userMetadataList":{"response":{"items":["met_6E_SB2LoFh5M"],"details":{"totalItems":1}}}}}
- 排序:
sort: MediaSortInput{by: MediaSortBy, order: GraphqlSortOrder};MediaSortBy实测枚举:TITLE RANDOM USER_RATING RELEASE_DATE LAST_UPDATED LAST_CONSUMED TIMES_CONSUMED PROVIDER_RATING,GraphqlSortOrder为ASC/DESC。带排序复测同 5.1 查询:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { userMetadataList(input: {lot: ANIME, filter: {general: RATED}, search: {take: 5, page: 1}, sort: {by: USER_RATING, order: DESC}}) { response { items details { totalItems } } } }"}'
# {"data":{"userMetadataList":{"response":{"items":["met_6E_SB2LoFh5M"],"details":{"totalItems":1}}}}}
- 缓存层:
metadataSearch、metadataDetails、collectionContents等返回体外层带cacheId(UUID),ryot 用它做结果缓存失效(配套变更expireCacheKey)。
七、常见问题(FAQ)
Q1:向 /graphql 发请求返回 404 HTML?
实测:
curl -s http://127.0.0.1:8141/graphql -H 'Content-Type: application/json' \
-d '{"query":"query { __typename }"}'
# <!DOCTYPE html>...<h1>404 Not Found</h1>...(输出节选,实测首尾;react-router 页面,日志报 No route matches URL "/graphql")
该路径落到前端路由而非 API。正确端点为 /backend/graphql(见第三章)。
Q2:registerUser 直接选 id 报 Unknown field "id" on type "RegisterResult"?
返回类型是联合类型,必须用内联片段并带 __typename(见 4.1 的 ⚠️ 说明)。loginUser 同理(LoginResult)。
Q3:登录报 USERNAME_DOES_NOT_EXIST / CREDENTIALS_MISMATCH?
实测回显:
{"data":{"loginUser":{"__typename":"LoginError","error":"USERNAME_DOES_NOT_EXIST"}}}
{"data":{"loginUser":{"__typename":"LoginError","error":"CREDENTIALS_MISMATCH"}}}
前者表示从未注册(先走 4.1),后者为密码错误。注册重名报 IDENTIFIER_ALREADY_EXISTS(实测)。
Q4:带正确 adminAccessToken 注册的新用户为什么不是 ADMIN?
实测:系统已存在管理员后,再次携带正确 adminAccessToken 注册,新用户 lot 仍为 NORMAL(见 5.5 响应)。ADMIN 角色仅由「首管创建」获得,后续用户角色变更需走 updateUser(管理员)。
Q5:deployAddEntitiesToCollectionJob 返回 true 但 collectionContents 查不到?
该变更只是部署后台作业(高频调度默认 every 5 minutes),实测等待约 5 秒后作业即完成;若长期为空,检查是否关闭了后台任务(SERVER_DISABLE_BACKGROUND_JOBS=true 会使所有 deploy*Job 类变更失效)。