# 公开音乐 API

[文档首页](./README.md) · [数据模型及通用错误](./protocol.md)

本文件全部接口为 GET。除音频接口外均返回 JSON，不要求管理员登录。全局设备/IP封禁和访问策略可能使任意 `/api/*` 返回 403/503；单接口下方只列主要分支，不重复全部通用错误。

## 1. 健康与目录

### 1.1 `GET /api/health`

无查询参数。

```json
{"status":"ok","app":"Wristhyme","version":"2.1.1","access":"public"}
```

200 只表示该网关处理了健康请求，不验证上游搜索、插件、音频或完整播放。`version` 示例来自网关快照，不是当前 Android 版本。启用访问策略后健康接口仍可能被封禁/策略错误拦截。

### 1.2 `GET /api/sources`

无查询参数。200：

```json
{
  "sources":[
    {"id":"qq","name":"QQ音乐","backend":"qq","plugin":""},
    {"id":"netease","name":"网易云音乐","backend":"netease","plugin":""}
  ],
  "catalog_revision":1790812800,
  "default_plugin":"0123456789abcdef"
}
```

- 只返回启用目录项，且自定义别名的基础平台也必须启用。
- 关联 `plugin` 禁用时返回空；关联插件只是推荐信息，不代表客户端必须或已经切换。
- `default_plugin` 是当前有效默认，空字符串表示没有有效默认。客户端使用稳定别名进行解析，避免保存一个随后变化的真实默认 ID。
- `catalog_revision` 是策略投影的 Unix 秒时间戳，不是严格递增事务序号；同秒修改可能相同，不能作为唯一变更检测依据。
- 基础七平台有特定展示顺序；不要要求响应永远恰好七条。
- 无启用项时可以正常返回 `sources: []`；客户端不得硬编码补回禁用项。

## 2. 通用搜索与歌单

### 2.1 `GET /api/search`

| 查询参数 | 类型 | 必需 | 默认/范围 |
| --- | --- | --- | --- |
| `source` | string | 是 | `/api/sources` 返回的 ID；可为自定义别名 |
| `q` | string | 是 | 空白折叠后1..100字符，不能以http:/https:开头 |
| `page` | integer字符串 | 否 | 网易云默认1，范围1..20；其他平台当前通用接口忽略此字段 |
| `refresh` | string | 否 | 仅 `1` 绕过搜索缓存；其他值按非刷新处理 |

网易云每次最多20首，其他普通平台最多60首；哔哩哔哩最多20个视频首分P结果。自定义 source 先映射到 backend，响应歌曲和顶层 source 保持真实平台。

普通平台 200：

```json
{
  "source":"qq",
  "songs":[
    {"key":"11111111111111111111111111111111","ref":"<SIGNED_REF>","name":"示例歌曲","artist":"示例歌手","album":"示例专辑","duration":"240","source":"qq","original":false,"cover":""}
  ],
  "note":"",
  "query":"示例歌曲",
  "state":"ok"
}
```

网易云 200 在上述结构上增加：

```json
{
  "page":1,
  "page_size":20,
  "total":120,
  "has_more":true,
  "search_adapter":"netease-signals-221",
  "ranking_scope":"current_page",
  "ranking_note":"原版和热度缺乏已验证字段时为未知；版本仅依标题/别名线索，平台次序只作稳定兜底。",
  "dedup":{
    "duplicate_ids_removed":0,
    "duplicate_reissues_collapsed":1,
    "policy":"same-title-artist-version-duration-span-1000ms",
    "scope":"current-page",
    "raw_count":20,
    "display_count":19
  }
}
```

示例是扩展字段片段，完整响应仍包含 songs/source/query/state/note。`total` 可为 null；去重后歌曲数可以少于 page_size。分页依据上游总量，而非返回列表长度；第20页可能仍报告上游有更多，但本接口不能继续请求第21页。

空结果为200、`songs:[]`、`state:empty`，可能附平台限制提示。平台搜索错误不能视为空结果，网易云或哔哩哔哩显式错误分支为502、`state:error`、`songs:[]`、`error`。

主要错误：400参数不合法；403禁用；429繁忙；502上游失败。非空搜索缓存90秒。没有通用跨源聚合搜索参数，也没有可自定义 page_size 的公开合同。

### 2.2 `GET /api/playlist_search`

| 参数 | 类型 | 必需 | 默认/范围 |
| --- | --- | --- | --- |
| `source` | string | 否 | 默认 `netease`；可用目录平台/别名 |
| `q` | string | 是 | 空白折叠后1..100字符，不接受http:/https:开头 |

200：`{playlists: Playlist[], source: string}`，最多20条。没有公开分页、刷新、排序或个性化参数；某个平台有适配器不等于它一定支持歌单搜索。空数组表示当前未读取到歌单。

主要错误：400参数无效；403禁用；429繁忙；502上游失败。

### 2.3 `GET /api/playlist`

两种输入方式：`source + id`，或者 `link`。非空 link 存在时，解析后的平台和ID覆盖 source/id。

| 参数 | 类型 | 必需 | 默认/范围 |
| --- | --- | --- | --- |
| `source` | string | source/id模式需要 | 启用平台/目录别名 |
| `id` | string | source/id模式需要 | `[A-Za-z0-9_-]{1,180}` |
| `link` | string | 否 | 下述受支持完整URL，不支持短链 |
| `page` | integer字符串 | 否 | 默认1，转换整数后钳制到1..100；非整数返回400 |

200：

```json
{"songs":[],"source":"netease","id":"123456789","page":1,"has_more":false,"note":"该歌单无可读取曲目，可能为空、私密或平台受限"}
```

每页最多100首；不会连续抓取全歌单。`has_more` 优先取上游页数；没有页数标记时按本页是否达到100首估算，所以不是绝对准确的全量计数。

**完整链接合同：**

| 平台 | 允许主机 | ID提取形式 |
| --- | --- | --- |
| 网易云 | `music.163.com`、`y.music.163.com` | 路径含playlist，query或fragment query中数字id；如 `https://music.163.com/#/playlist?id=123456789` |
| QQ | `y.qq.com`、`i.y.qq.com` | `/playlist/数字`、`/taoge/数字`，或disstid/id数字查询参数 |
| 酷狗 | `www.kugou.com`、`m.kugou.com` | `/songlist/gcid_字母数字`、`/special/数字`、`/special/single/数字`，或specialid数字参数 |

链接可用HTTP/HTTPS，但禁止用户名/密码及80/443以外端口；服务器只提取ID，不抓取任意用户链接、不展开跳转。整个链接必须 URL 编码，尤其是 `#`、`?` 和 `&`。

主要错误：400链接/ID/分页无效；403平台禁用；429繁忙；502上游失败。

## 3. 轻量列表

这组路径名带 watch，但也可供手机等客户端调用。固定每页10项，无自动后台预取；共用2并发槽和300秒缓存。

### 3.1 `GET /api/watch/search`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `source` | 是 | 启用平台/目录别名 |
| `q` | 是 | 与通用搜索相同的1..100字符约束 |
| `page` | 否 | 默认1，必须1..100，越界返回400 |
| `refresh` | 否 | `1` 刷新请求页；首页刷新会清理相应搜索页缓存 |

200包含 `songs: Song[]`、`source`、`page`、`page_size:10`、`has_more`、`query`。

网易云还可包含 total/dedup/search_adapter/ranking_scope/ranking_note/note；使用独立10首的上游分页，**不能混用通用20首分页的page偏移**。网易云第20页强制has_more=false，第21..100页正常返回空和has_more=false。

哔哩哔哩从有界的最多20条快照切页，并非继续搜索后续平台页。其他平台依上游分页标记；若上游当前页与请求页不符，返回空页且has_more=false，防止无限重复加载。

主要错误：400参数无效；部分目录ValueError也被该模块包装为400；403禁用；429加载繁忙；502其他上游异常。因此轻量网易云失败的HTTP包装与通用搜索不完全一致。

### 3.2 `GET /api/watch/recommend`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `source` | 是 | 只支持基础平台QQ/网易云，别名映射后同样适用 |
| `page` | 否 | 默认1，范围1..100 |
| `refresh` | 否 | 仅page=1且refresh=1重新获取推荐快照 |

200：`{playlists: Playlist[], source, page, page_size:10, has_more, note}`。

服务器读取最多100个去重的公开推荐歌单，客户端分页。不是登录账号个性化日推；没有账号、历史听歌、推荐模型参数。平台没有可读推荐时当前模块抛ValueError并返回400，不是必然200空数组。

主要错误：400不支持平台/页码/推荐不可读；403禁用；429繁忙；502其他上游失败。

### 3.3 `GET /api/watch/playlist`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `source` | 是 | 只支持QQ/网易云 |
| `id` | 是 | `[A-Za-z0-9_-]{1,180}` |
| `page` | 否 | 默认1，范围1..100 |
| `refresh` | 否 | `1`刷新当前页，首页刷新同时清理同歌单其他页缓存 |

200：`{songs: Song[], has_more, source, page, page_size:10}`。没有 link输入合同，响应也不保证包含id/note。过滤基础平台不符的歌曲；发现分页标记错位时返回空列表。

主要错误：400参数无效；403禁用；429繁忙；502上游失败。

## 4. 热歌榜

### `GET /api/charts/hot`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `source` | 是 | `qq`或`netease`；音源别名经公共策略映射后可用 |
| `page` | 否 | 默认1，1..100；不钳制，越界400 |
| `refresh` | 否 | 默认0，只接受0/1；仅首页的1才清理该平台热榜缓存 |

200：

```json
{"songs":[],"total":0,"has_more":false,"period":"2026-10-01","source":"qq","title":"QQ音乐热歌榜","chart_id":"26","page":1,"page_size":10,"note":"官方公开热歌榜 · 2026-10-01"}
```

此为结构示例；真实有效榜单首页通常有歌曲。QQ固定榜单26，网易云固定3778678，每页10首。`period`是平台更新标识/日期，不应自行当作完整精确时间戳；缺失时空字符串。

QQ按平台offset拉取并校验rank与页码；网易云最多读取1000个有效去重条目后本地切页。`total` 在QQ来自平台截断到0..1000，在网易云是已读有效行数。某页无数据不一定是错误；首页/应有数据的位置返回异常内容则502。

每平台独立加载锁，第二个并发请求可能429。缓存300秒；没有任意榜单ID、topid、page_size、登录个性化或媒体下载参数。禁用检查在慢请求前后都执行。

主要错误：400不支持平台/页码/refresh；403禁用；429榜单加载中；502平台内容异常/超时。

## 5. 恢复与原平台音频

### 5.1 `GET /api/restore`

查询参数 `ref: string` 必需，长度最多12000字符。200直接返回一个 [Song](./protocol.md)，不包在song字段中。

400：引用为空/损坏/签名不匹配、来源无效或其平台已禁用。虽然文案可能写“签名过期”，ref没有内置TTL。签名密钥轮换会使旧ref失效；访问策略异常还可能被外层包装为其他错误。

### 5.2 `GET /api/inspect`

查询参数 `key: string` 必需。检查**原平台**，不检测后台默认插件音频。

200可能为：

```json
{"valid":true,"size":"8.00 MB","bitrate":"267 kbps","format":"MP3","quality_note":"格式由上游地址标识；码率为估算值，不代表无损认证"}
```

上游早期失败可能只给valid=false，网关仍补format/quality_note；size和bitrate不保证存在。`size`是人类可读字符串，不是字节整数。`format`为FLAC/MP3/M4A/AAC/OGG/WAV/未知，根据地址后缀推断；url字段被移除。估算码率与文件扩展名不能证明无损或授权。

主要错误：410key不可用；429繁忙；502上游失败。valid=false可正常200。

### 5.3 `GET /api/media`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `key` | 是 | 服务器已知歌曲 |
| `download` | 否 | 仅1增加附件响应头；否则普通音频流 |

请求可带单一Range。成功为200/206二进制音频，常见MIME：`audio/mpeg`、`audio/flac`、`audio/mp4`、`audio/aac`、`audio/ogg`、`audio/wav`。实际类型结合字节验证，不能仅依据平台名或Content-Type。

可能透传 `Content-Range`、`Accept-Ranges`、`ETag`、`Last-Modified`、`Content-Length`，以真实上游存在为条件。没有保证一定提供长度、范围或校验标记。

`download=1` 的附件名由标题和歌手清理后组成，头格式包含ASCII后备名及UTF-8 filename*；不会创建服务器下载任务、写服务器音乐库，也不切换后台默认插件。

**Range与续传：**

- 只接受正则 `bytes=\d*-\d*`：典型 `bytes=0-`、`bytes=1024-2047`、`bytes=-1024`；多段范围拒绝。
- 这是格式检查，不代表上游支持每种范围；非音频、续传资源变化也会失败。
- 请求了Range但响应为200时表示不能把body直接追加到旧文件，应按下载器策略从头处理。
- 上游416可能被包装为外层502 + upstream_status=416；本地格式检查416与上游不支持要区分。
- 仅Range被网关显式转发；不要依靠发送If-Range来保证服务端条件续传。客户端自行核对响应范围、ETag、长度和已保存资源身份。

主要错误：410key不可用；416本地Range不支持；429繁忙；502平台拒绝/非有效音频/资源变化；504连接失败或超时。

### 5.4 `GET /api/playback_status`

查询参数 `key` 必需。200示例：

```json
{"state":"streaming","content_type":"audio/mpeg","source":"qq"}
```

其他状态为 `{state:"unknown"}` 或 `{state:"error",error,source,upstream_status?}`。状态有效观察窗口180秒，最多1200条进程内记录。

该状态只反映原平台 `/api/media` 部分取流分支，不代表客户端解码、已播放时长或完整下载。插件媒体不更新此表，部分媒体验证失败也不会写error，所以unknown/streaming不能作为最终播放验收。

主要错误：410key不可用；全局403/503。该接口不走普通六槽慢请求限流。

## 6. 歌词与快照

### 6.1 `GET /api/lyric`

查询参数 `key` 必需。200：

```json
{"lyric":"[00:00.00]示例歌词","source":"netease","key":"11111111111111111111111111111111","lyric_adapter":"square-karaoke-1"}
```

上游请求format=lrc，读取最多128KiB并规范化换行/部分方括号逐字时间到增强LRC。适配不会创造缺失的逐字时间；相邻行首时间戳表示重复行，不应误当逐字标签。

此接口不是结构化逐字数组或翻译专用API；普通LRC、增强LRC和客户端支持的其他解码能力应区分。空字符串允许200，不能据此生成歌词。旧部署可能没有lyric_adapter字段；上游格式质量也不由适配标识保证。

主要错误：410key不可用；429繁忙；502上游失败。该路径调用原平台歌词，不自动跟随后台默认插件。

### 6.2 `GET /api/snapshot`

查询参数 `key` 必需。用于下载时取得曲目信息和歌词，不下载音频、不保存服务器文件。

200：

```json
{"name":"示例歌曲","artist":"示例歌手","album":"示例专辑","duration":"240","source":"qq","key":"11111111111111111111111111111111","captured_at":1790812800000,"lyric":"","lyric_state":"unavailable","lyric_note":"未保存歌词：下载时平台未提供"}
```

`captured_at`为Unix毫秒。`lyric_state`为saved/unavailable；正常取得可接受歌词时saved，其余通过lyric_note说明超限、不可用、失败或超时。歌词抓取失败通常仍返回200及歌曲信息，不能把unavailable当作整个快照请求失败。

最多128KiB歌词，快照会检测多1字节的超限，并排除常见HTML/JSON错误内容。返回不包含ref、cover或search_signals；需要长期恢复引用时另存原Song.ref。

主要错误：410key不可用；429繁忙；外层502异常。快照的原平台歌词不保证等于插件播放来源歌词。

## 7. LX插件

支持平台与默认别名见 [协议](./protocol.md)。第三方可能获得服务器IP及当前在线歌曲元数据；不会因为此接口取得设备本地整个音乐库。`consent=1`是接口请求合同，不由管理员会话代替。

### 7.1 `GET /api/plugins`

无查询参数。200：

```json
{
  "plugins":[
    {"id":"0000000000000000","name":"跟随后台默认 · 示例插件","version":"1.0.0","sha256":"<SHA256>","imported":true,"status":"imported","detail":"已导入服务器，尚未运行检测","sources":{},"is_default":true,"target_plugin":"0123456789abcdef"},
    {"id":"0123456789abcdef","name":"示例插件","version":"1.0.0","sha256":"<SHA256>","imported":true,"status":"imported","detail":"已导入服务器，尚未运行检测","sources":{}}
  ],
  "runtime":"quickjs-isolated-process",
  "notice":"启用后第三方将获得服务器IP和当前歌曲信息；不读取或上传设备本地音乐库。",
  "default_plugin":"0123456789abcdef"
}
```

只显示当前启用项；默认有效时首项可能为别名，复制真实插件元数据并增加is_default/target_plugin。别名不满足“ID等于SHA256前16位”，客户端应明确识别。

状态包括imported/initialized/failed；`checked_at`可能存在，单位Unix秒；网关最后取流结果可能覆盖status/detail。sources来自插件初始化声明，未检测时可为空。声明样例：

```json
{"wy":{"actions":["musicUrl","lyric"],"qualitys":["128k","320k","flac"]}}
```

注意字段拼写是`qualitys`，不是qualities。声明支持、初始化通过和每首歌实际可播放互不等价。

主要错误：503运行时/策略不可用，可能附plugins空数组。无需consent即可读目录，该接口不占插件操作双槽。

### 7.2 `GET /api/plugin_probe`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `plugin` | 否 | 真实16位小写hex ID，或default/稳定别名；缺省走后台默认 |
| `consent` | 是 | 必须为字符串1 |

200示例：`{ok:true,state:"initialized",sources:{},detail:"初始化通过；不代表每首歌曲可播放"}`。422示例：`{ok:false,error:"ValueError",detail:"插件执行失败，可能是接口离线、授权限制或不兼容的 LX 扩展"}`。

会清除网关该插件最后取流状态，再触发脚本初始化；不需要歌曲key，不验证某首音频。可产生第三方网络请求，不应后台自动连续运行。

主要错误：400缺consent/ID无效；403禁用；429频率/并发；422执行失败；503无默认；502运行时通信失败。

### 7.3 `GET /api/plugin_resolve`

| 参数 | 必需 | 说明 |
| --- | --- | --- |
| `key` | 是 | 服务器已知歌曲；仅五个LX映射平台 |
| `plugin` | 否 | 同probe；推荐新受管客户端使用0000000000000000 |
| `consent` | 是 | 1 |
| `quality` | 否 | 默认128k，必须在该插件该平台的qualitys中；无统一固定音质枚举 |

200：

```json
{"url":"/api/plugin_media?ticket=<TICKET>","plugin":"0123456789abcdef","state":"resolved","expires_in":3600}
```

url为本站相对取流路径，plugin为映射后的真实插件ID。服务器只将已缓存歌曲元数据交给插件；拒绝直接上传任意musicInfo或audioURL。返回resolved只表示获取并校验了地址，不是已拉到有效音频。

票据3600秒、512条有界缓存；无需一次性消费，可重复请求或Range，但可能因淘汰/重启提前失效。票据没有在当前实现中绑定客户端IP或设备ID，拥有者可尝试访问，应视为临时访问凭据，不传播/记录完整URL。平台/插件禁用会使已有票据取流403。

主要错误：400consent/ID/不支持平台；403禁用；410key不可用；422插件失败/不声明音质；429频率/并发；503无默认；502异常/非法地址。

### 7.4 `GET /api/plugin_media`

查询参数 `ticket` 必需。无需consent、plugin或key，实际平台和插件从票据读取；仍执行全局封禁和票据内音源/插件启用检查。

成功200/206音频，Range合同、内容验证与原平台媒体相似；可能透传Content-Range/Accept-Ranges/ETag/Last-Modified/Content-Length。该接口**没有download=1附件命名合同**，客户端下载应自行保存已验证的二进制数据。

主要错误：410票据过期/未知；403插件或音源禁用；416Range不合法；429插件双槽繁忙；502第三方拒绝/非有效音频/续传资源变化/连接异常。该路径不调用原平台playback_status状态表，但会影响plugins的最近取流status/detail。

### 7.5 `GET /api/plugin_lyric`

参数key/plugin/consent与resolve相同；quality也可传递给运行时，默认128k，但歌词动作没有musicUrl的同一音质声明检查。

200仅为 `{lyric: string}`。插件value为对象时取其lyric，为字符串时直接使用，其他类型变为空字符串。该网关分支不保证与 `/api/lyric` 一样包含source/key/lyric_adapter或相同规范化流程。

主要错误与resolve相同，除音乐URL及票据签发相关分支。客户端仍须有自己的大小和格式限制。

## 8. 非API发布文件

自建后台的“发布管理”支持上传安装包/源码包、明确发布更新渠道、查看校验值和删除未发布文件；“API运维”支持维护模式。示例网站的后台只模拟这些操作，真实管理API与音乐API均403。静态示例不会发布真实更新。

| GET路径 | 内容 |
| --- | --- |
| `/downloads/latest-phone.json` | 手机更新feed |
| `/downloads/latest-watch.json` | 手表更新feed |
| `/downloads/latest.json` | 旧双模式feed；不能代替当前两端独立feed |
| `/downloads/Wristhyme-Phone-<version>.apk` | 手机安装包 |
| `/downloads/Wristhyme-<version>.apk` | 手表安装包 |
| 上述文件 + `.sha256` | SHA256文本 |
| `/downloads/*-source.zip` | 对应源码包，以实际发行文件名为准 |

两端feed主要字段：`versionCode` integer、`versionName` string、`applicationId` string、`minSdk` integer、`apkUrl` string、`sha256` string、`notes` string；新发布可附 `signerSha256`。这些路径由静态文件服务处理，错误未必为JSON。

2026-09-30发行记录中两端为2.2.2/222；实际更新仍读各自feed。手机包名 `cn.tolike.wristhyme.phone`，手表 `cn.tolike.wristhyme`。下载前核对包名/版本/平台兼容性，下载后核对SHA256与签名，不仅依靠健康接口version或网页文案。
