P.H.M. API

Phira / Phigros 谱面难度参考接口。查询已缓存的定数,或分析一张谱并写入共享缓存。 免费;匿名可用,API Key 获得更高额度。

返回的是「点估计 + 不确定带」:ref_const 是社区共识标度, ref_official 是官谱标度。两者不可混比,细节见首页说明。

快速开始

API 是普通的 HTTPS + JSON,不需要 SDK。先拿到一个 API Key(开放平台),或者直接用匿名额度试:

# base_url https://phm.app.workbuddy.host/api/v1 # 匿名也可以,只是额度低 curl https://phm.app.workbuddy.host/api/v1/charts/22681

带 Key 的请求(推荐 x-phm-key):

curl https://phm.app.workbuddy.host/api/v1/charts/22681 \ -H "x-phm-key: sk-你的key"

JavaScript:

const res = await fetch( "https://phm.app.workbuddy.host/api/v1/charts/22681", { headers: { "x-phm-key": "sk-你的key" } } ); const chart = await res.json();

Python:

import requests r = requests.get( "https://phm.app.workbuddy.host/api/v1/charts/22681", headers={"x-phm-key": "sk-你的key"}, timeout=30, ) print(r.json())

在线试一下

不用装任何东西,在这里直接打一次真实的请求。Key 可以留空 —— 留空就走匿名额度。

点「发送请求」看结果。
Key 只存在这个输入框里,不写入 localStorage、不进 URL。 请求头用的是 x-phm-key —— 这也是你该用的那个头。

认证

两种身份:

为什么必须用 x-phm-key:本站部署在一个会改写鉴权头的网关后面, 实测两种常见写法都不可靠 ——
① Authorization:网关会写入它自己的值,把你的盖掉;
② x-api-key:网关把它当成自己的鉴权头消费掉,服务端收不到;
③ x-phm-key:私有名字,网关不碰,稳定生效。
失效时的表现很隐蔽:不报错,只是静默降级成匿名(1000 次/小时 → 100 次/小时)。
自检方法:把 Key 换成一把不存在的 sk-(格式仍要对), 正常应返回 401 invalid_api_key;如果仍返回 200, 说明 Key 没送达,你正在以匿名身份被限流。
更直接:打开 /api/v1/meta?diag=1, 看 diag 里各头的长度 —— 0 表示这个头没穿透到服务端。

Key 的明文只会在创建时返回一次;服务端只存 sha256 哈希与掩码。每个账号最多 100 个有效 Key,可随时吊销。

开放平台登录使用站点账号;API Key 与账号密码相互独立,不要把 Key 写进前端代码或公开仓库。

限流

身份额度说明
匿名100 次/小时真实对端 IP + 全局桶
API Key1000 次/小时分析另有 200 次/天

超限返回 429,并带 retry-after;服务端限流器不可用时返回 503。

查询定数

GET /charts/{chart_id}

读共享缓存。未分析过的谱返回 analyzed:false(不是 404)。

{ "chart_id": 22681, "name": "Pandemic", "level": "IN.16", "difficulty": 16.6, "ref_const": 15.6, "ref_official": 16.1, "ps_score": 13.4, "engine_ver": "server-verified", "engine_build": "com-trim15-v0.5.0", "analyzed": true, "cached": true }

列出谱面

GET /charts

分页浏览共享缓存。这是唯一能"不知道 id 也能用"的接口 —— /charts/{id} 需要你先知道是哪张谱,而这里可以搜索、排序、翻页。

参数默认说明
page1页码,从 1 开始
pageSize50每页条数,上限 200
sortchart_id排序字段,白名单:chart_id / name / level / difficulty / ref_const / ref_official / ps_score / nps / notes。传别的值返回 400
orderascasc 或 desc
q—按谱面名做子串匹配(最多 60 字符)
level—难度前缀过滤,如 IN / AT / SP
only—official = 只要算得出官谱标度的;stale = 引擎版本与基准不一致的
build—判过期用的基准引擎版本,仅在 only=stale 时有意义
curl "https://phm.app.workbuddy.host/api/v1/charts?sort=ref_const&order=desc&pageSize=3" \ -H "x-phm-key: sk-你的key"

响应(total 是真实总数,不是本页条数,可以直接算页数):

{ "rows": [ { "chart_id": 80464, "name": "QuomodocunquizE", "level": "AT Lv.19", "ref_const": 17.834, "ref_official": 16.9645, "ps_score": 16.623, "nps": 19.3434, "notes": 4000, "engine_build": "com-trim15-v0.5.0", "analyzed": true, "cached": true } ], "total": 661, "page": 1, "pageSize": 3, "pages": 221, "sort": "ref_const", "order": "desc" }

搜索:

curl "https://phm.app.workbuddy.host/api/v1/charts?q=Pandemic" \ -H "x-phm-key: sk-你的key"

全库统计

GET /stats

共收录多少谱、覆盖多少、活跃用户多少。与首页展示的数字同源。

curl https://phm.app.workbuddy.host/api/v1/stats \ -H "x-phm-key: sk-你的key" # → {"charts":661,"scores":81,"profiles":1,"active_1d":1,…}

谱师作品

GET /charters/{uid}

某个谱师(Phira 用户 id)上传过的谱面,并自动贴上本站已算好的定数。

参数默认说明
uid(路径)必填Phira 用户 id,纯数字
maxPages3向上游读几页(每页 30 张),上限 5 —— 硬上限,避免这个接口变成爬虫入口
curl "https://phm.app.workbuddy.host/api/v1/charters/257272?maxPages=2" \ -H "x-phm-key: sk-你的key" # → { "uid":257272, "count":2, "returned":2, "maxPages":2, # "charts":[ { "chart_id":48099, "name":"Future Downloader", # "analyzed":false, … } ] }
两个必须知道的行为:
① analyzed:false 表示本站还没算过这张谱 —— 此时 ref_const 等字段不存在。缺字段和"算出来是 0"是两件事,别用 0 兜底。
② 上游挂了返回 502,不会返回空数组。空数组会被读成"这位谱师没有作品", 那是最坏的错误答案 —— 宁可报错让你重试。

玩家资料

GET /players/{uid}

公开资料 + 统计。不含成绩明细:把几百条记录塞进一个响应,既慢又容易被批量抓。

curl https://phm.app.workbuddy.host/api/v1/players/257272 \ -H "x-phm-key: sk-你的key" # → { "id":257272, "name":"平方秒和立方吨", "rks":3.5926006, # "bio":"…", "avatar":"https://…", "joined":"2023-07-25T…", # "stats":{ "numRecords":850, "avgAccuracy":0.9449… } }

uid 不存在返回 404 player_not_found。 stats 是可选增强:拿不到时为 null,请求本身仍然成功。

分析谱面

POST /analyses

服务端从 Phira 下载谱面、复算并写入共享缓存。v1 只接受 chart_id;分析结果对之后所有查询直接可用。

curl -X POST https://phm.app.workbuddy.host/api/v1/analyses \ -H "x-phm-key: sk-你的key" \ -H "content-type: application/json" \ -d '{"chart_id": 22681}'

已缓存时直接返回(cached:true);未缓存时会下载 ≤60MB 谱面并运行引擎,可能需要几秒。并发上限 2,忙时返回 503 analysis_busy。

用量

GET /usage

用 API Key 调用时返回该 Key 所属账号的用量;开放平台用账号 token 调用也可以。

{ "today": 12, "week": 87, "month": 312, "by_endpoint": { "/charts": 280, "/analyses": 32 }, "quota": { "hourly": 1000, "analysisDaily": 200 } }

Key 管理

在 API 开放平台 登录后创建、改名、设置权限/到期、轮换、吊销。删除(吊销)后 Key 会立即从列表消失,历史用量仍保留。

Scopes:charts:read(查询)、analyses:write(分析)、usage:read(用量)。 缺 scope 返回 403 insufficient_scope;过期返回 401 expired_api_key。 创建时可设 30/90/365 天或永不过期;轮换会新建同权限 Key 并立即吊销旧 Key。
方法路径说明
GET/keys列出当前账号的有效 Key(吊销后不再返回;不含明文)
POST/keys创建,返回一次明文;body 可带 scopes[] / expires_in_days
PATCH/keys/{id}改名 / 改 scopes / 改到期
POST/keys/{id}/rotate轮换:新 Key + 吊销旧 Key
DELETE/keys/{id}吊销

这些接口要求网页账号 token(x-hm-token),不是 sk- key。

错误码

错误统一为:

{ "error": { "type": "invalid_request_error", "code": "invalid_chart_id", "message": "..." } }
HTTPcode含义
400invalid_chart_idchart_id 不是正整数
400invalid_uid谱师 / 玩家的 uid 不是正整数
400invalid_sort_fieldsort 不在白名单里(响应头 x-allowed-sort 给出全部可选值)
404player_not_found该 Phira 用户不存在
404not_found端点不存在
401invalid_api_keyKey 不存在、格式错或已吊销
401account_token_requiredKey 管理需要登录
401expired_api_keyKey 已过期
403insufficient_scopeKey 缺少该接口所需 scope
409key_limit_reached账号已有 100 个有效 Key
429rate_limit_exceeded超出小时额度
502analysis_failed / cache_unavailable数据库暂时不可用
502upstream_unavailablePhira 上游暂时不可用(谱师 / 玩家接口)。不是「没有数据」,请重试而不是当作空结果
502stats_unavailable统计服务暂时不可用
503analysis_busy / rate_limit_unavailable分析并发已满 / 限流器故障

更新日志