ShuffleMuse 的 Web UI 与自动化客户端使用同一组 /api 路由。生产环境通常与 SPA 同源,不提供跨域 CORS 配置。
- Compose 默认基础地址:
http://127.0.0.1:8080/api。 - 密码模式使用
shufflemuse-sessionCookie,不使用 Bearer Token。 - 认证白名单内的 TCP 直连对端无需 Cookie。
- 无密码模式下除登录/退出路由不存在外,其余 API 全部公开。
密码模式路由矩阵:
| 路由 | 未登录可访问 |
|---|---|
POST /api/auth/login |
是 |
POST /api/auth/logout |
是,用于清 Cookie;不要求已有有效 Session |
GET /api/status |
是,但未认证响应最小化 |
GET /api/ready |
是 |
POST /api/rescan |
否 |
| 其他曲库、Browse、标签、媒体和 Graveyard 路由 | 否 |
认证检查先于曲库 readiness 检查。因此密码模式下未登录访问曲库接口,即使首次扫描仍在进行,也先得到 401 UNAUTHORIZED。
JSON 错误统一使用:
{
"error": "human-readable message",
"code": "STABLE_CODE"
}error 适合展示或排障,客户端分支应优先使用 code 和 HTTP 状态。
- Host 必须命中
MUSIC_ALLOWED_HOSTS,否则400 INVALID_HOST。 POST/DELETE的跨站Sec-Fetch-Site或 Origin/Host 不一致会得到403 CSRF_BLOCKED。- CLI 可不发送 Origin。
- 登录、新增标签和队列 POST body 最大 8 KiB,并严格拒绝未知字段和第二个尾随 JSON 对象。
- 当前严格 JSON 解码器不强制
Content-Type: application/json,但客户端仍应正确设置。 q最大 200 UTF-8 字节;dir、path最大 4096 字节。
支持分页的端点接受:
| 参数 | 规则 |
|---|---|
page |
默认 1,正整数,只能出现一次 |
limit |
端点默认值,正整数,只能出现一次,最大 1000 |
非法分页返回 400 INVALID_PAGINATION。请求超过最后一页不是错误,会返回空数组和不变的 total。
曲库、搜索和标签文件列表使用:
{
"id": "base64url-stable-id",
"filepath": "Artist/Album/track.flac",
"name": "track",
"dir": "Artist/Album"
}filepath 和 dir 相对 MUSIC_DIR。id 由清理后的相对路径派生;文件移动或重命名会得到新 ID。
仅在密码模式注册。
请求:
{
"password": "secret",
"remember": false
}成功:200 OK,设置 Cookie。
{"status":"logged in"}失败:
400 INVALID_JSON;413 REQUEST_TOO_LARGE;401 UNAUTHORIZED;429 LOGIN_IP_BLOCKED,并提供Retry-After秒数。
curl 示例:
curl -i -c shufflemuse.cookies \
-H 'Content-Type: application/json' \
--data '{"password":"secret","remember":false}' \
http://127.0.0.1:8080/api/auth/login密码模式下始终注册。撤销当前已知 Token、设置过期 Cookie,并返回 204 No Content。未知或缺失 Cookie 仍返回 204。
curl -i -b shufflemuse.cookies -X POST \
http://127.0.0.1:8080/api/auth/logout未认证的密码模式响应只包含:
{
"authRequired": true,
"authenticated": false
}已认证的密码模式响应示例:
{
"fileCount": 68,
"libraryReady": true,
"libraryGeneration": 3,
"scanStatus": "idle",
"opusBitrate": 160,
"uptime": "1h2m3s",
"lastScan": "2026-07-20T12:34:56Z",
"scanError": null,
"authRequired": true,
"authenticated": true
}无密码模式和 TCP 对端命中认证白名单时,同一完整响应中的 authRequired 为 false、authenticated 为 true。
字段:
| 字段 | 说明 |
|---|---|
fileCount |
当前快照中的音频数 |
libraryReady |
数据端点是否可用 |
libraryGeneration |
路径集合版本;仅路径集合变化时增加 |
scanStatus |
initializing、idle、scanning 或 error |
opusBitrate |
当前生效的 Opus 转码码率,单位 kbps |
lastScan |
最近成功扫描的 RFC3339 时间;从未成功时为 null |
scanError |
最近扫描错误;没有时为 null |
白名单客户端会看到 authRequired:false、authenticated:true,即使全局配置了密码。
此端点在密码模式下也公开,适合容器健康检查。
就绪:
HTTP/1.1 200 OK
{"ready":true,"generation":3}未就绪:
HTTP/1.1 503 Service Unavailable
{"ready":false,"generation":0,"code":"LIBRARY_SCANNING"}首次扫描成功后,后续后台重扫期间继续返回 200;服务开始关机时立即返回 503。
接受一次异步全量扫描请求:
HTTP/1.1 202 Accepted
{"status":"accepted"}可能错误:
409 LIBRARY_INITIALIZING:首次扫描尚未完成;409 SCAN_IN_PROGRESS:已有扫描;409 RESCAN_UNAVAILABLE:当前 API 没有 Rescanner;500 RESCAN_ERROR。
请求成功只表示已接受,不表示扫描完成。轮询 /api/status 观察 scanStatus、scanError 和 generation。
默认 limit=50。
{
"items": [],
"total": 68,
"page": 1,
"generation": 3
}队列 ID 是 32 字节随机值的 Base64URL 表示;服务端只保存 SHA-256 摘要。队列排列不可变,分页只是同一排列的切片,固定每页 200 条且不接受 limit。
队列描述:
{
"id": "opaque-base64url-token",
"tag": "favorite",
"createdGeneration": 3,
"total": 68,
"pageSize": 200
}队列项在普通文件字段之外增加零基 queueIndex 与请求时的在线状态:
{
"id": "file-id",
"filepath": "Artist/track.flac",
"name": "track",
"dir": "Artist",
"queueIndex": 0,
"available": true
}请求字段均可省略:
{
"tag": "favorite",
"pinFileId": "file-id",
"replaceQueueId": "old-queue-token"
}tag 限定候选;pinFileId 只在该在线文件也属于标签候选时固定到第一位,
否则忽略并返回 pinApplied:false。这保证标签队列不会混入标签外曲目。
replaceQueueId 只在新队列完整创建后删除旧队列。初始队列和 Randomize
不发送 pin。
成功返回 201 Created,包含 queue、第一页 items、page:1、libraryGeneration 和 pinApplied。
返回 queue、固定最多 200 个 items、page 和请求时的 libraryGeneration。正常重扫后 ID、顺序、位置和 total 不变;离线项只变为 available:false,新文件不会插入旧队列。
{"fileId":"file-id"}文件已经在队列中时返回原队列、零基 queueIndex、所在页及该页。文件在线但不在队列中时,原子创建以该文件为首、保留原剩余顺序的替代队列,并在响应的 queue.id 返回新令牌。
{"dir":"Artist/Album"}读取目录中直接包含的在线音频文件(不递归),按不区分大小写的文件名
排序,移到当前队列最前并去重。原队列的其他项目保持原顺序。成功后原子
替换队列令牌并返回 200 OK;响应包含新队列的第一页以及
directoryTrackCount。
目录及 symlink 解析后的目标都必须位于 MUSIC_DIR 内。空目录或没有在线
音频的目录返回 422 NO_AUDIO_FILES;不存在的目录返回
404 NOT_FOUND。
幂等返回 204 No Content,令牌不存在或格式无效也相同。
队列稳定错误:404 QUEUE_NOT_FOUND、404 FILE_NOT_FOUND、
422 NO_AUDIO_FILES、503 QUEUE_BUSY、503 QUEUE_CAPACITY。JSON、
分页、认证和内部错误沿用公共错误码。
默认 limit=50。对文件的无扩展名 name 做 Unicode 字符串的大小写转换后子串匹配;不搜索目录、标签或媒体 metadata。
{
"items": [],
"total": 7,
"page": 1,
"query": "summer",
"generation": 3
}错误:400 MISSING_QUERY、400 QUERY_TOO_LONG、分页错误。
成功:
{
"title": "君玉",
"codec": "FLAC",
"bitrateKbps": 986,
"bitrateApproximate": false,
"durationSeconds": 245.32
}title 来自容器 TITLE,缺失时回退第一条音轨的 TITLE;两者都为空时省略该字段。标题去除首尾空白并限制为 512 个 UTF-8 字节。ffprobe 优先使用第一条音轨 bitrate,其次使用容器 bitrate;两者都缺失时按文件大小和时长估算,并设置 bitrateApproximate:true。
错误:404 NOT_FOUND、503 MEDIA_BUSY、504 MEDIA_TIMEOUT、422 METADATA_ERROR。
按 cover.jpg → cover.png → folder.jpg → folder.png 返回指定相对目录同级、大小写不敏感的封面。dir 必须恰好出现一次并使用干净的相对路径;路径穿越、绝对路径、反斜杠和任一目录 symlink 返回 400 INVALID_DIR。目录不存在或没有可用封面返回 404 COVER_NOT_FOUND。该端点不会递归查找子目录、扩展到其他文件格式或探测音频内嵌封面。
顺序:同目录 cover.jpg → cover.png → folder.jpg → folder.png → 音频内嵌第一视频流;目录文件名大小写不敏感。内嵌 descriptor 与 metadata 共享同一次 ffprobe、文件身份缓存和 singleflight,不会再启动独立 descriptor probe。外置封面不接受 symlink。文件严格超过 20 MiB、任一边严格超过 8192 或总像素严格超过 40 MP 时返回 404 COVER_NOT_FOUND。
外置封面任一边严格超过 1536 或文件严格超过 1 MiB 时,实时转换为最长边 1024、不放大的 JPEG q3;PNG 先合成白底。JPEG 转换结果不超过原文件 85% 才采用,否则本次请求发送原 JPEG。小型外置封面原样发送,因此未触发转换的 PNG 保留透明度。内嵌封面固定输出最长边 1024 的 JPEG q3。
响应头:
Content-Type:图片类型;Content-Disposition: inline;转换输出文件名为cover.jpg;Cache-Control: private, max-age=3600;ETag:由源路径、大小、mtime、阈值和编码规格生成;X-Cover-Source:embedded或命中的实际目录封面文件名(保留原始大小写)。
HEAD 和能得到 304 的条件请求只发现 descriptor,不启动 FFmpeg。可能转换的 JPEG/PNG 在 HEAD 中预先声明 image/jpeg,但不声明未知的转换后 Content-Length。每个非缓存 GET 都重新转换;仅并发中的相同转换 singleflight,不建立服务端图片结果缓存。错误:404 NOT_FOUND、404 COVER_NOT_FOUND、503 MEDIA_BUSY、504 MEDIA_TIMEOUT、500 COVER_ERROR。等待队列满或等待超时返回 503 MEDIA_BUSY;15 秒执行 deadline 返回 504,FFmpeg 失败不会降级发送超大原图。
参数:dir 默认为根目录 .,默认 limit=100。先对目录排序,再对文件排序,然后把两组拼成一个统一分页序列。为避免任意深页码迫使服务端在内存中保留整个超大目录,单次请求最多排序前 50,000 个条目;仍落在目录内容范围内的更深分页返回 400 INVALID_PAGINATION,已经超出目录末尾的分页仍返回空页。
{
"directories": [
{"name":"Artist","path":"Artist"}
],
"files": [
{
"id":"...",
"name":"cover.jpg",
"path":"cover.jpg",
"dir":".",
"kind":"image",
"mimeType":"image/jpeg",
"size":12345,
"modified":"2026-07-20T12:34:56Z",
"previewable":true,
"playable":false
}
],
"audioCount":0,
"total":2,
"page":1,
"generation":3
}audioCount 是该目录全部直接子项中当前可播放的音频文件数,不受当前
分页影响;子目录中的音频不计入。
音频文件还包含 playable:true、audioId 和不含扩展名的 trackName。
错误:400 INVALID_DIRECTORY、400 QUERY_TOO_LONG、404 NOT_FOUND、500 BROWSE_ERROR、分页错误。
以内联方式返回允许预览的原文件:
- 图片:JPG/JPEG/PNG/GIF/WebP/AVIF/BMP;
- PDF;
- 不超过 2 MiB 的 TXT/CUE/LOG/M3U/M3U8/MD/NFO/LRC/JSON。
不允许的类型或超大文本返回 415 PREVIEW_UNAVAILABLE。路径不存在、不是普通文件或不在音乐根内返回 404 NOT_FOUND。
以 attachment 返回任意可浏览普通文件的原始内容,不会转码。音频为 Cache-Control: private, no-store,其他文件为 private, max-age=3600。两种文件服务均由 http.ServeContent 支持 Range 和条件请求。
Browse 路径必须位于音乐根,且不能包含隐藏组件或系统杂项名称。不要在音乐根内存放可被下载的敏感普通文件。
{"tags":["favorite","rock"]}请求:
{"tag":"favorite"}成功:201 Created。
{"tag":"favorite"}服务端标签限制:1–50 个 ASCII 字节,只允许字母、数字、.、-、_。同一文件上的完全相同标签返回 409 DUPLICATE_TAG;服务端区分大小写,Web UI 额外执行大小写不敏感的重复检查。
其他错误:400 MISSING_TAG、400 INVALID_TAG、严格 JSON 错误、404 NOT_FOUND。
成功 204 No Content。文件或关联不存在时返回 404 NOT_FOUND。客户端应对路径段进行 URL 编码。
只包含当前曲库快照中的在线文件:
{
"tags": [
{"name":"favorite","count":12},
{"name":"rock","count":8}
]
}仅存在于 missing 路径的标签不会出现。
默认 limit=50,只返回当前在线文件;Web UI 固定请求 200 条一页。服务端在一个有取消检查的顺序遍历中同时计算在线总数并只收集目标页,不会为每一页先物化完整标签文件数组:
{
"items": [],
"total": 12,
"page": 1,
"generation": 3
}下载 shufflemuse-tags.csv:
Content-Type: text/csv; charset=utf-8;Content-Disposition: attachment;Cache-Control: private, no-store;- UTF-8 BOM;
- 表头
filepath,name,dir,tags,status; - 每个持久化路径一行,含 online 与 missing;
- 多标签按名称排序并以分号连接;
- 行按 filepath 排序;
- 危险公式起始字符前增加
'。
导出不接受标签筛选参数,也没有 CSV 导入 API。
默认 limit=50。
{
"items": [
{
"filepath":"Old/track.flac",
"name":"track",
"dir":"Old",
"tags":["favorite","lost"]
}
],
"total":1,
"page":1,
"generation":3
}删除指定缺失路径的全部标签关联,不删除磁盘文件。不存在的路径也返回 204 No Content,因此操作是幂等的。
如果路径在当前快照中在线,返回:
HTTP/1.1 409 Conflict
{"code":"FILE_ONLINE","error":"tagged file is online"}缺失 path 返回 400 MISSING_PATH,超长返回 400 QUERY_TOO_LONG。
| 查询参数 | 行为 |
|---|---|
无 mode |
.opus 原样发送;其他音频实时转码。这是兼容模式,不是 Web UI 默认请求。 |
mode=original |
原文件,支持 Range |
mode=opus |
强制转码为 Ogg Opus |
mode=opus&start=12.5 |
从 12.5 秒开始转码 |
start 必须是有限、非负数字,且只能与 mode=opus 一起使用。其他 mode 返回 400 INVALID_STREAM_OPTIONS。
原文件响应:
- 扩展名对应音频
Content-Type; Accept-Ranges: bytes;Cache-Control: private, no-store。
转码响应:
Content-Type: audio/ogg; codecs=opus;Cache-Control: private, no-store;X-Accel-Buffering: no;- 不提供 Range。
HEAD 对需要转码的请求只返回头,不启动 FFmpeg。转码映射第一条音轨并移除视频/封面流。请求取消会终止 FFmpeg。
错误:404 NOT_FOUND、400 INVALID_STREAM_OPTIONS、503 MEDIA_BUSY、504 MEDIA_TIMEOUT、500 STREAM_ERROR。Opus 启动后 15 秒仍未成功写出首字节时,若响应尚未提交则返回 504;响应体已经开始传输后只能关闭流并记录日志。
| HTTP | code | 常见来源 |
|---|---|---|
| 400 | INVALID_HOST |
Host 不在允许列表 |
| 400 | INVALID_JSON |
畸形、未知字段或尾随 JSON |
| 400 | INVALID_PAGINATION |
page/limit 非法或 limit > 1000 |
| 400 | QUERY_TOO_LONG |
q/dir/path 超过字节限制 |
| 400 | MISSING_QUERY |
Search 缺 q |
| 400 | INVALID_DIRECTORY |
Browse dir 逃出音乐根或格式非法 |
| 400 | INVALID_DIR |
目录封面 dir 缺失、重复、逃逸或包含 symlink |
| 400 | MISSING_TAG / INVALID_TAG |
标签 body 非法 |
| 400 | INVALID_STREAM_OPTIONS |
mode/start 非法 |
| 400 | MISSING_PATH |
Graveyard 删除缺 path |
| 401 | UNAUTHORIZED |
未登录或密码错误 |
| 403 | CSRF_BLOCKED |
跨站请求或 Origin/Host 不一致 |
| 404 | NOT_FOUND |
文件、目录、ID 或标签关联不存在 |
| 404 | COVER_NOT_FOUND |
没有可用封面 |
| 409 | DUPLICATE_TAG |
重复添加完全相同标签 |
| 409 | FILE_ONLINE |
试图从 Graveyard 删除在线路径 |
| 409 | LIBRARY_INITIALIZING |
首次扫描中发起重扫 |
| 409 | SCAN_IN_PROGRESS |
已有重扫 |
| 413 | REQUEST_TOO_LARGE |
JSON body 超过 8 KiB |
| 415 | PREVIEW_UNAVAILABLE |
文件类型或文本大小不可预览 |
| 422 | METADATA_ERROR |
ffprobe 或 metadata 不可用 |
| 429 | LOGIN_IP_BLOCKED |
登录 IP 已封禁 |
| 500 | BROWSE_ERROR |
目录读取失败 |
| 500 | TAG_ERROR / CSV_ERROR |
标签库或 CSV 生成失败 |
| 500 | COVER_ERROR |
封面读取/提取失败 |
| 500 | RESCAN_ERROR |
无法接受重扫 |
| 500 | STREAM_ERROR |
流开始前发生内部错误 |
| 503 | LIBRARY_SCANNING |
曲库尚未有成功快照 |
| 503 | MEDIA_BUSY |
转码、metadata 或封面提取的媒体槽满或超时 |
| 503 | QUEUE_BUSY / QUEUE_CAPACITY |
队列构建等待或缓存预算不足 |
| 504 | MEDIA_TIMEOUT |
已启动的媒体任务或 Opus 首字节超过 deadline |
- 监控只使用
/api/ready,状态面板使用已认证/api/status; - 写请求不要伪造浏览器跨站头;经反向代理时保留外部 Host;
- 登录后把 Cookie jar 限制为私密文件;
- 处理列表时记录 generation;多页 generation 变化时从第一页重试;
- 不要把 CSV 当作可恢复数据库;
- 对 429 使用
Retry-After,对 503 使用有上限的退避,不要立即无限重试。