接入、版本与维护
1. 推荐调用流程
搜索到播放
- GET sources读取实时目录,处理空目录和禁用,保留id/backend区别。
- 用目录id发搜索。普通查询用search;10条增量列表用watch/search,不能混用两套page偏移。
- 保存Song.ref及展示字段,保留响应歌曲真实source。网易云排序信号仅与当前查询及policy一致时使用。
- 播放既有收藏时先restore;新搜索可直接使用key。410后允许有限恢复,400坏签名需要重新搜索。
- 当前受管解析策略:五个LX平台先sources读有效默认;非空则plugin_resolve使用稳定别名、consent=1、quality=128k;为空才走media。非LX平台用media。
- 插件返回的相对地址必须是本站plugin_media及ticket,禁止外部URL、凭证、fragment或重定向。直接媒体同样只能官方路径。
- 媒体请求带同一个已自愿注册的设备身份头,核验HTTP与Content-Type,交给播放器实际判断解码/播放。
- 原平台lyric与插件lyric独立;需要保存原平台歌词快照时使用snapshot。不要自动创造逐字时间或声称解析源歌词完全一致。
默认配置读取失败或插件失败不等于“没有配置”。不要吞错误然后静默换平台/插件;让用户知道当前失败,按产品策略提供明确重试或选择。
下载与续传
先确定解析方式,取得原平台media或plugin_media,再由客户端创建下载任务。本项目没有公开“创建服务器下载任务”接口。
原平台可download=1获取附件名;插件媒体自行命名。保存前确认是音频而非JSON/HTML,续传检查206/Content-Range/资源标记,200时从头处理。完整性无法只依inspect的size字符串判断;断流、缺长度、票据过期或资源变化应报告,不把文件自动标为完整。
分页刷新
列表仅进入页面/明确操作时加载,遵守has_more。首页refresh=1在轻量/榜单模块有清理快照作用;后续页refresh不等于重建全源快照。切源/切查询需取消旧任务或通过generation防止迟到响应覆盖当前列表。
重复页或去重后空页不应无限加载;通用网易云最多20页、轻量网易云第20页停止;推荐最多100个快照条目,B站轻量最多20个视频元数据条目。
2. curl 示例
以下代码块采用POSIX shell续行。Windows PowerShell请使用curl.exe,将命令写为单行或换为PowerShell反引号续行;不要把PowerShell内置curl别名当成curl程序。示例会访问线上接口,本次整理并未执行这些请求。
读取目录/搜索/公共歌单
curl --fail-with-body 'https://wristhyme.tolike.cn/api/sources'
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/search' \
--data-urlencode 'source=netease' \
--data-urlencode 'q=示例歌曲' \
--data-urlencode 'page=1'
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/watch/search' \
--data-urlencode 'source=qq' \
--data-urlencode 'q=示例歌曲' \
--data-urlencode 'page=1' \
--data-urlencode 'refresh=1'
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/playlist' \
--data-urlencode 'link=https://music.163.com/#/playlist?id=123456789' \
--data-urlencode 'page=1'
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/charts/hot' \
--data-urlencode 'source=netease' \
--data-urlencode 'page=1'
引用恢复与插件解析
REF/KEY由真实搜索响应读取,不能拿文档示例key尝试播放。
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/restore' \
--data-urlencode "ref=$REF"
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/plugin_resolve' \
--data-urlencode "key=$KEY" \
--data-urlencode 'plugin=0000000000000000' \
--data-urlencode 'consent=1' \
--data-urlencode 'quality=128k'
仅在产品具有相应第三方服务同意/受管解析政策时发送consent=1;匿名统计consent是另一个独立功能。没有默认插件时上述解析返回503。不要用curl -L无条件跟随重定向传播身份头。
后台同源请求形状
登录body由本地安全输入提供,避免把密码写进命令历史。这里只展示结构,不自动创建账号或运行管理操作:
POST /admin/api/login HTTP/1.1
Host: wristhyme.tolike.cn
Origin: https://wristhyme.tolike.cn
Content-Type: application/json
Content-Length: <UTF8_BODY_LENGTH>
{"username":"admin","password":"<ADMIN_PASSWORD>"}
随后一个设置默认插件请求:
POST /admin/api/default-plugin HTTP/1.1
Host: wristhyme.tolike.cn
Origin: https://wristhyme.tolike.cn
Cookie: __Secure-wristhyme_admin=<SESSION_TOKEN>
X-CSRF-Token: <CSRF_FROM_LOGIN_OR_SESSION>
Content-Type: application/json
Content-Length: <UTF8_BODY_LENGTH>
{"id":"<ENABLED_PLUGIN_ID>"}
这是HTTP结构模板,不是字节级可直接发送报文;Content-Length由HTTP库根据UTF-8实际body设置。不要把后台Cookie用于/api/media或第三方请求。
3. JavaScript接入示例
示例可用于同源前端或支持fetch的Node环境;跨域浏览器还受CORS限制。只演示JSON/解析链路,不含完整播放器或下载续传。
const ORIGIN = 'https://wristhyme.tolike.cn';
const LX_SOURCES = new Set(['netease', 'qq', 'kugou', 'kuwo', 'migu']);
async function getJSON(path, parameters = {}, signal) {
const url = new URL(path, ORIGIN);
if (url.origin !== ORIGIN || url.username || url.password ||
url.hash || !url.pathname.startsWith('/api/')) {
throw new Error('Invalid gateway URL');
}
for (const [key, value] of Object.entries(parameters)) {
url.searchParams.set(key, String(value));
}
const response = await fetch(url, { redirect: 'error', signal });
const result = await response.json();
if (!response.ok) {
const error = new Error(result.error || result.detail || `HTTP ${response.status}`);
error.status = response.status;
throw error;
}
return result;
}
async function mediaURL(savedSong, signal) {
const song = savedSong.ref
? await getJSON('/api/restore', { ref: savedSong.ref }, signal)
: savedSong;
if (!song.key) throw new Error('Missing song key');
if (LX_SOURCES.has(song.source)) {
const catalog = await getJSON('/api/sources', {}, signal);
if (catalog.default_plugin) {
const resolved = await getJSON('/api/plugin_resolve', {
key: song.key, plugin: '0000000000000000', consent: '1', quality: '128k'
}, signal);
if (typeof resolved.url !== 'string' ||
!resolved.url.startsWith('/api/plugin_media?ticket=')) {
throw new Error('Invalid plugin media path');
}
const media = new URL(resolved.url, ORIGIN);
if (media.origin !== ORIGIN || media.username || media.password ||
media.hash || media.pathname !== '/api/plugin_media' ||
!media.searchParams.get('ticket')) {
throw new Error('Invalid plugin media URL');
}
return media.href;
}
}
const media = new URL('/api/media', ORIGIN);
media.searchParams.set('key', song.key);
return media.href;
}
生产客户端还应限制JSON响应大小、设置超时、持久化ref、清理已过期票据、保护设备头并校验实际音频。本站禁用检查由网关执行;客户端不要用旧目录绕过服务端错误。
4. 版本差异
| 证据/阶段 | 协议变化或限制 |
|---|---|
| server-next历史网关 | 固定来源目录、搜索最多60、原平台媒体、LX基础合同;不能代表最新后台策略 |
| search221-signals | 网易云独立search_signals、当前页排序,信号不进key/ref |
| admin219及followup | 动态来源、启停/封禁、匿名统计、Cookie/CSRF后台;用户撤回标记及基础平台过滤 |
| restore-default225 | 默认真实ID与有效ID、稳定别名、禁用回退;隐藏新增/导入UI但保留API |
| netease-page20 | 通用网易云搜索上游单次60改20,不只是客户端截断 |
| watch220-light/backend | 新增watch/search、recommend、playlist;10项分页,网易云helper支持10/20 |
| desktop-baseline221/new-backend | 独立charts/hot路径、QQ/网易云每页10;不能误当推荐歌单 |
| android当前2.2.2 | 手机/手表受管默认解析、自动发送解析consent=1、去掉手动插件设置;本地私人库仍本地保存 |
| wristhyme-ios及桌面测试版 | 仍有各自手动插件/同意流程;不能假定采用Android2.2.2全部策略 |
老客户端硬编码来源可能不反映禁用目录;服务端仍可拒绝。排序支持也因客户端版本而异,服务端返回次序不保证旧客户端不二次重排。
独立包已只读取得包含动态策略、轻量分发和热榜的现有服务器源码,提取哈希见PROVENANCE.json;包装修改与历史合同分别记录,不再要求外部补丁目录。
5. 源码索引
所有链接相对文档目录,ZIP解压后可直接查看。原服务器来源及提取前哈希见PROVENANCE.json。
| 内容 | 来源 |
|---|---|
| 通用网关、key/ref、歌词、媒体及分发 | server.py |
| 轻量列表 | watch_browse.py |
| 网易云10/20参数合同 | netease_catalog.py |
| 搜索信号 | netease_signals.py |
| 去重元数据 | catalog_dedup.py |
| 插件及默认别名 | plugin_routes.py |
| 媒体验证 | media_validation.py |
| 歌词规范化 | lyric_normalize.py |
| 热榜 | hot_charts.py |
| 设备策略与动态来源 | admin_gateway.py |
| 后台HTTP | admin_server.py |
| 后台存储与默认配置 | admin_store.py |
| 脚本导入 | admin_import.py |
| 插件注册表 | plugin_registry.py |
| 运行时 | runtime.py |
| 工作进程 | worker.py |
| 反向代理 | Caddyfile |
| 上游注册路由 | internal/web目录 |
原工作区历史STATUS/客户端证据仅用于版本比较,不随本包分发;本包运行版本测试另见VERIFICATION.md,不替代生产音乐在线检查。
6. 文档验证
工作区提供仅离线的文档检查:
node .\docs\api\verify-docs.cjs
检查本地Markdown链接、JSON示例语法、代码块闭合、公开/管理/内部路由的文档覆盖,并解析JavaScript示例语法。它不访问网络、不运行部署脚本、不登录后台、不验证所有业务行为。
后续后端变更时应同步参数表、错误分支、模型与来源索引;若需要线上验收,应另行明确测试环境和允许范围。只读搜索也会触发上游联网;plugin_probe会执行脚本;register/forget和后台POST会改变状态,不能当作无副作用探测。
**建议维护验证项:**动态目录为空/自定义别名/禁用、key淘汰与ref恢复、默认回退与无默认、两套分页边界、empty与error区别、consent/quality校验、旧票据禁用、Range200/206/416/非音频、统计撤回与封禁例外、登录CSRF/会话过期/改密码、GitHub固定commit及导入哈希。