Skip to content

歌曲 API ​

本文档基于以下源文件编写:

目录 ​

  1. 概述
  2. 列表与查询
  3. 增删改
  4. 歌词与标签
  5. 文件整理
  6. 播放与媒体
  7. HLS 反向代理
  8. 音轨与参与歌手

1. 概述 ​

章节来源: internal/handlers/music.go、internal/app/routers.go

歌曲管理模块是 Songloft 核心 API,覆盖歌曲增删改查、播放流分发、封面/歌词、音频标签写入、文件整理和重复检测。所有端点均需 JWT 认证(BearerAuth),基础路径 /api/v1。

歌曲类型: local(本地文件)、remote(外部 URL / 插件音源)、radio(在线广播/直播流)。

Song JSON 序列化规则(MarshalJSON 统一处理):

  • url -- 统一为 /api/v1/songs/{id}/play(HLS 电台为 .../play.m3u8)
  • cover_url -- 有封面时 /api/v1/songs/{id}/cover,否则空
  • lyric_url -- 有歌词时 /api/v1/songs/{id}/lyric,否则空
  • source_url -- 仅 remote/radio 类型返回原始流地址

部分只读字段(Song 对象随详情/列表返回):

  • is_live -- 是否直播流(仅 remote 类型语义生效)
  • is_video -- bool,是否含真实视频轨(扫描时 ffprobe 探测,排除仅封面图片);客户端据此渲染画面 / 选择投屏 MIME(0024 迁移引入)

错误响应格式: {"error":"...", "detail":"..."}(detail 可选)。


2. 列表与查询 ​

章节来源: internal/handlers/music.go、internal/database/filters.go

2.1 GET /songs -- 分页列表 ​

路径: /api/v1/songs | 认证: BearerAuth

获取歌曲列表,支持按类型过滤、关键词搜索、路径前缀过滤和分页。

查询参数:

参数类型必填默认值说明
typestring否--local / remote / radio
keywordstring否--搜索关键词(匹配标题、艺术家、专辑)
path_prefixstring否--按 file_path 前缀过滤
limitint否20每页数量,上限 100000
offsetint否0偏移量

成功响应 (200): {"songs": [Song...], "total": 100, "limit": 20, "offset": 0}

错误: 500 获取歌曲列表/总数失败


2.2 GET /songs/ids -- ID 列表 ​

路径: /api/v1/songs/ids | 认证: BearerAuth

与 /songs 共享过滤条件(type、keyword、path_prefix),仅返回 ID。用于「全选当前筛选范围」等批量操作场景,不分页。

成功响应 (200): {"ids": [1, 2, 3], "total": 3}

错误: 500 获取歌曲 ID 列表失败


2.3 GET /songs/duplicates -- 重复检测 ​

路径: /api/v1/songs/duplicates | 认证: BearerAuth

通过音频指纹(Chromaprint)查询本地歌曲中内容相同的重复组。需先完成指纹计算(POST /scan/fingerprints)——扫描后自动计算指纹默认关闭(见 /settings/scan-auto-fingerprint)。指纹只采样每首歌前 120 秒,故同一指纹内还会按全片时长以 30 秒容差二次聚簇(相邻比较,时长未知不切),排除「统一片头」类碰撞。

成功响应 (200):

json
{
  "groups": [
    {
      "fingerprint": "AQADtNIyR...",
      "songs": [
        {"id": 1, "title": "夜曲", "artist": "周杰伦", "album": "十一月的萧邦",
         "duration": 253.5, "file_path": "/music/夜曲.mp3", "format": "mp3",
         "bit_rate": 320, "file_size": 10485760, "cover_url": "/api/v1/songs/1/cover",
         "added_at": "2024-01-01T12:00:00Z"}
      ]
    }
  ],
  "total_groups": 1,
  "total_duplicates": 2
}

错误: 500 查询重复歌曲失败


2.4 GET /songs/facets -- 标签分类聚合 ​

路径: /api/v1/songs/facets | 认证: BearerAuth

按指定维度聚合曲库,返回该维度下非空取值、各自的歌曲数量及一首代表歌曲的封面 URL,用于「分类浏览」的卡片网格。取到某取值后可用 /songs?<field>=<value> 拉取该分类下歌曲。

查询参数:

参数类型必填默认值说明
fieldstring是--聚合维度:genre / artist / album / language / style / year / decade(year/decade 的 value 为数字字符串,年代如 "1990" 表示 1990-1999)
keywordstring否--对取值模糊搜索
limitint否20分页大小,上限 100000
offsetint否0分页偏移
sortstring否count排序维度:count / name
orderstring否--排序方向:asc / desc(count 缺省 desc,name 缺省 asc)

成功响应 (200):

json
{
  "field": "artist",
  "facets": [
    {"value": "周杰伦", "count": 42, "cover_url": "/api/v1/songs/1/cover"}
  ],
  "total": 128,
  "limit": 20,
  "offset": 0
}

total 为该维度去重取值总数。

错误: 400 缺少或不支持的 field | 500 服务器错误


2.5 GET /songs/stats -- 曲库统计 ​

路径: /api/v1/songs/stats | 认证: BearerAuth

返回曲库汇总统计(单次查询完成)。

成功响应 (200):

json
{
  "total_songs": 1234,
  "local_songs": 1000,
  "remote_songs": 200,
  "radio_songs": 34,
  "total_duration": 43210.5,
  "total_file_size": 53687091200,
  "artist_count": 87,
  "album_count": 152,
  "genre_count": 23
}

错误: 500 获取曲库统计失败


2.6 GET /songs/random -- 随机歌曲 ​

路径: /api/v1/songs/random | 认证: BearerAuth

随机返回若干首歌曲,支持与 /songs 相同的类型/关键词/路径过滤。

查询参数:

参数类型必填默认值说明
typestring否--local / remote / radio
keywordstring否--搜索关键词
path_prefixstring否--按 file_path 前缀过滤
exclude_playlist_labelsstring否--排除带指定 label 的歌单内歌曲(逗号分隔)
limitint否50返回数量,上限 500

另支持歌曲标签过滤参数(与 /songs 一致,见标签管理 API)。

成功响应 (200): {"songs": [Song...], "total": 50}(total 等于实际返回条数)

错误: 500 获取随机歌曲失败


2.7 GET /songs/folders -- 文件夹浏览 ​

路径: /api/v1/songs/folders | 认证: BearerAuth

按目录层级浏览本地歌曲。path 为相对于 music_path 的路径(空 = 根目录),返回下一级子文件夹(含各自递归歌曲数)与当前目录的直属歌曲。路径做 traversal 防护(含 .. 返回 400)。

查询参数:

参数类型必填说明
pathstring否相对于 music_path 的子目录(空 = 根)
keywordstring否对子文件夹名模糊过滤

成功响应 (200):

json
{
  "path": "评书",
  "parent_path": "",
  "music_path": "/music",
  "folders": [{"name": "三国演义", "path": "评书/三国演义", "song_count": 120}],
  "total_folders": 2,
  "songs": [Song...],
  "total_songs": 5
}

错误: 400 未设置音乐目录 / 非法路径(含 ..) | 500 读取失败


2.8 GET /songs/names -- 歌名与歌手名清单 ​

路径: /api/v1/songs/names | 认证: BearerAuth

一次性返回曲库中指定维度的全部去重、非空取值(按名称升序),不分页、无计数、无封面等冗余字段。用于客户端自动补全/快速跳转。

查询参数:

参数类型必填说明
fieldstring是title(歌名)或 artist(歌手名)

成功响应 (200): {"field": "artist", "names": ["周杰伦", "林俊杰"], "total": 2}

错误: 400 缺少或不支持的 field | 500 获取曲库名录失败


3. 增删改 ​

章节来源: internal/handlers/music.go、internal/models/models.go

3.1 GET /songs/{id} -- 获取详情 ​

路径: /api/v1/songs/{id} | 认证: BearerAuth

路径参数: id (int, 必填) -- 歌曲 ID

成功响应 (200): 完整 Song 对象。

错误: 400 无效 ID | 404 歌曲不存在


3.2 PUT /songs/{id} -- 更新歌曲 ​

路径: /api/v1/songs/{id} | 认证: BearerAuth

路径参数: id (int, 必填)

请求体:

字段类型必填说明
titlestring是标题(不能为空)
artiststring否艺术家
albumstring否专辑
urlstring条件必填播放地址(非本地歌曲必填)
cover_urlstring否封面 URL
is_livebool否是否直播流(仅 remote 类型生效)

成功响应 (200): 更新后的 Song 对象。

错误: 400 无效 ID / 标题为空 / URL 为空 | 404 不存在 | 500 更新失败


3.3 DELETE /songs/{id} -- 删除歌曲 ​

路径: /api/v1/songs/{id} | 认证: BearerAuth

路径参数: id (int, 必填)

成功响应 (200): {"message": "歌曲已删除"}

错误: 400 无效 ID | 500 删除失败


3.4 POST /songs/remote -- 添加远程歌曲 ​

路径: /api/v1/songs/remote | 认证: BearerAuth

批量添加网络歌曲。每条至少提供 url 或 plugin_entry_path + source_data 组合。

请求体 (数组):

字段类型必填说明
urlstring条件音频直链(纯外链用;插件来源留空)
titlestring是标题
artiststring否艺术家
albumstring否专辑
cover_urlstring否封面 URL
durationnumber否时长(秒)
plugin_entry_pathstring条件插件 entryPath(如 "subsonic")
source_datastring条件插件音源元数据 JSON
dedup_keystring否去重 key(如 "nas1:001234");空时不去重
lyricstring否歌词内容或 URL
lyric_sourcestring否歌词来源类型

成功响应 (201): {"songs": [Song...], "count": 2}

错误: 400 请求无效 / 列表为空 / 标题为空 / 缺少音源标识 | 500 添加失败


3.5 POST /songs/radio -- 添加电台 ​

路径: /api/v1/songs/radio | 认证: BearerAuth

批量添加电台/广播。

请求体 (数组):

字段类型必填说明
urlstring是电台流地址
titlestring是电台名称
artiststring否描述
cover_urlstring否封面 URL

成功响应 (201): {"songs": [Song...], "count": 1}

错误: 400 请求无效 / 列表为空 / URL 或标题为空 | 500 添加失败


3.6 POST /songs/batch-delete -- 批量删除 ​

路径: /api/v1/songs/batch-delete | 认证: BearerAuth

请求体:

字段类型必填说明
idsint64[]是要删除的歌曲 ID 列表
delete_filesbool否是否同步删除本地音频文件(默认 false)

成功响应 (200): {"deleted": 3}

错误: 400 请求无效 / ID 列表为空 | 500 删除失败


3.7 POST /songs/clean -- 清理无效歌曲 ​

路径: /api/v1/songs/clean | 认证: BearerAuth

清理本地歌曲中文件已不存在或位于排除目录中的记录,同时删除关联封面文件。

成功响应 (200):

json
{"message": "清理完成", "total": 5, "file_not_found": 3, "in_excluded_dir": 2}

错误: 500 清理失败


3.8 远程歌曲元数据刷新 ​

对所有元数据缺失的远程歌曲,通过 ffprobe 探测时长、比特率、采样率、格式及标签并回填,异步执行。

POST /songs/refresh-metadata -- 启动刷新 ​

路径: /api/v1/songs/refresh-metadata | 认证: BearerAuth

成功响应 (202): {"status": "started"}

错误: 409 已在运行 | 500 未配置刷新器 / 启动失败

GET /songs/refresh-metadata/progress -- 查询进度 ​

路径: /api/v1/songs/refresh-metadata/progress | 认证: BearerAuth

成功响应 (200): MetadataRefreshProgress(含 status 等字段;未配置时返回 {"status": "idle"})。

POST /songs/refresh-metadata/cancel -- 取消刷新 ​

路径: /api/v1/songs/refresh-metadata/cancel | 认证: BearerAuth

成功响应: 204 No Content(幂等,无任务时也返回 204)。


4. 歌词与标签 ​

章节来源: internal/handlers/music.go、internal/models/lyric.go

4.1 PUT /songs/{id}/lyrics -- 更新歌词 ​

路径: /api/v1/songs/{id}/lyrics | 认证: BearerAuth

更新歌词内容和来源。入参形态由 lyric_source 决定:

  • "url" -- 传 lyric_remote_url,运行时延迟拉取
  • 其它(scraped/file/embedded/cached/manual)-- 传 lyric/tlyric/rlyric/lxlyric

lyric_source=manual 标记用户手动调整,scanner 重扫不覆盖。

路径参数: id (int, 必填)

请求体:

字段类型必填说明
lyric_sourcestring否来源类型
lyricstring条件主歌词 LRC(非 url 来源)
tlyricstring否翻译歌词
rlyricstring否罗马音歌词
lxlyricstring否逐字歌词
lyric_remote_urlstring条件歌词远程 URL(url 来源)

成功响应 (200): {"message": "歌词已更新", "file_write_status": "written"}

file_write_status: written / unchanged / skipped / failed

错误: 400 无效 ID / 请求无效 | 404 歌曲不存在 | 500 更新失败


4.2 GET /songs/{id}/lyric -- 获取歌词 ​

路径: /api/v1/songs/{id}/lyric | 认证: BearerAuth

返回 LyricPayload JSON。内部根据 lyric_source 分发到数据库解包或远程 URL 拉取。

路径参数: id (int, 必填)

成功响应 (200):

json
{"lyric": "[00:00.00]夜曲...", "tlyric": "", "rlyric": "", "lxlyric": ""}

错误: 400 无效 ID | 404 歌曲/歌词不存在 | 502 远程拉取失败


4.3 PUT /songs/{id}/tags -- 写入音频标签 ​

路径: /api/v1/songs/{id}/tags | 认证: BearerAuth

将元数据写入数据库和本地音频文件标签(仅本地歌曲)。非空字段覆盖,空值保留。cover_data(base64) 优先于 cover_url。

路径参数: id (int, 必填)

请求体:

字段类型必填说明
titlestring否标题
artiststring否艺术家
albumstring否专辑
yearint否年份(>0 时覆盖)
genrestring否流派
lyricsstring否歌词 LRC(写入后 lyric_source=manual)
cover_datastring否封面 base64(优先)
cover_urlstring否封面 URL(下载后写入)
clear_coverbool否显式清空封面

成功响应 (200): {"song": Song, "file_write": "written"}

file_write: written / unchanged / skipped / failed / unsupported

错误: 400 无效 ID / 非本地歌曲 / 无效 base64 | 404 不存在 | 500 更新失败


4.4 GET /songs/{id}/song-tags -- 歌曲的自定义标签 ​

路径: /api/v1/songs/{id}/song-tags | 认证: BearerAuth

返回该歌曲已绑定的自定义标签列表(顶层数组,无绑定时返回 [])。标签的增删/绑定由「标签管理 API」(/song-tags)统一管理;此处只读。

路径参数: id (int, 必填)

成功响应 (200): [SongTag...],每项含 id / name / color / song_count / cover_url(可选)/ created_at。

错误: 400 无效的歌曲 ID | 500 获取歌曲标签失败


5. 文件整理 ​

章节来源: internal/handlers/music.go、internal/services/song_service.go

5.1 POST /songs/organize -- 批量整理文件 ​

路径: /api/v1/songs/organize | 认证: BearerAuth

批量移动/重命名本地歌曲文件。target_path 为相对于 music_path 的路径(含目录和文件名),扩展名必须与原文件一致。

请求体 (数组):

字段类型必填说明
idint64是歌曲 ID
target_pathstring是目标路径(相对于 music_path)

成功响应 (200):

json
[
  {"id": 1, "status": "ok", "file_path": "/music/周杰伦/夜曲.mp3"},
  {"id": 2, "status": "error", "error": "not a local song"}
]

错误: 400 请求无效 / 列表为空 / music_path 未设置 | 500 未配置


5.2 POST /songs/organize/preview -- 预览批量整理 ​

路径: /api/v1/songs/organize/preview | 认证: BearerAuth

dry-run 预览目录整理变更,不移动任何文件、不改数据库。返回每项 old_path→new_path 与状态。target_path 为相对 music_path 的路径(music_path 由服务端自取)。CUE 歌曲返回 skip;目标已存在或批内撞名返回 conflict。

请求体 (数组): 同 /songs/organize(id + target_path)。

成功响应 (200): OrganizePreviewResult 数组,每项含 status(ok / conflict / skip / error)、old_path、new_path。

错误: 400 请求无效 / 列表为空


6. 播放与媒体 ​

章节来源: internal/handlers/music.go

6.1 POST /songs/{id}/activate -- 激活/预取 ​

路径: /api/v1/songs/{id}/activate | 认证: BearerAuth

客户端切歌前调用。后端取消同一会话下其他歌曲的进行中工作(prefetch / transcode / reassign),让插件 worker 与转码信号量让位给新歌。幂等,其他客户端会话不受影响。

路径参数: id (int, 必填)

成功响应: 204 No Content

错误: 400 无效 song_id


6.2 POST /songs/{id}/played -- 播放事件通知 ​

路径: /api/v1/songs/{id}/played | 认证: BearerAuth

客户端在歌曲开始播放、播放完成或被跳过时调用,后端将事件广播给已订阅播放事件的 JS 插件(通过 songloft.events.onPlayEvent 注册)。

路径参数: id (int, 必填)

查询参数:

参数类型必填说明
sourcestring否调用来源标识,如 songloft-player(官方客户端)、miot(小爱音箱插件)
typestring否事件类型:play(开始播放)/ finish(播放完成,默认)/ skip(用户跳过)
context_typestring否播放上下文类型,仅 type=play 生效:playlist 或分面维度(artist/album/genre/year/decade/language/style)
context_keystring否播放上下文标识,仅 type=play 生效:playlist 传歌单 ID,分面维度传该维度取值(如歌手名)

副作用: 当 type=play 且同时传入合法的 context_type + context_key 时,额外把该歌曲写入对应上下文的播放历史(同上下文内按歌曲去重,只保留最近 50 条)。落库失败只记日志,不影响响应码。详见 播放历史 API。

只有 type=play 会落库:finish 是同一首歌的重复信息;skip 上报的是上一首歌,此时客户端上下文可能已切换,会记错归属。

成功响应: 204 No Content

错误: 400 无效歌曲 ID / 事件类型非法 | 404 歌曲不存在


6.3 GET /songs/{id}/play -- 播放流 ​

路径: /api/v1/songs/{id}/play | 方法: GET / HEAD | 认证: BearerAuth

按歌曲 ID 流式返回音频,内部按 song.type 自动分发:

类型行为
localServeFile(支持 Range/seek)
remote(插件)CacheService 下载缓存后返回
remote(外链)直接代理转发
radio(HLS)反代或 302(取决于 hls-proxy 开关)
radio(非 HLS)直接代理上游流

路径参数: id (int, 必填)

查询参数:

参数类型说明
formatstring目标格式(如 opus);不同时走 ffmpeg 转码
prefetchstring"1" 时异步预热,立即返回 202

成功: 200 音频流 | 202 预拉取已触发 | 206 Range 部分内容 | 302 HLS 重定向

错误: 400 无效 ID | 404 不存在 | 502 音源不可用


6.4 GET /songs/{id}/play.m3u8 -- HLS 别名 ​

路径: /api/v1/songs/{id}/play.m3u8 | 方法: GET / HEAD | 认证: BearerAuth

与 /play 共享 handler 的 HLS 电台专用别名。URL 以 .m3u8 结尾便于播放器(Web 端 hls.js / 原生 media_kit)识别为 HLS 流并走 HLS 处理路径。客户端通过 Song.PlaybackURL() 自动获取正确 URL。参数与响应同 6.3。


6.5 GET /songs/{id}/cover -- 封面 ​

路径: /api/v1/songs/{id}/cover | 认证: BearerAuth

获取封面图片。优先返回本地封面,不存在时代理外部 CoverURL。

路径参数: id (int, 必填)

成功响应 (200): 图片二进制,Content-Type 按扩展名自动设置(jpeg/png/gif/bmp/webp),Cache-Control: public, max-age=31536000。

错误: 400 无效 ID | 404 歌曲/封面不存在 | 500 读取失败


7. HLS 反向代理 ​

章节来源: internal/handlers/hls.go

HLS 反代在 hls_proxy 开关开启(PUT /settings/hls-proxy)后生效。流程:serveRadio -> ServeProxy 拉取上游 m3u8 并改写 URI 为本机相对路径 -> 播放器回访 playlist/segment 拉取子层资源。每次入口做同源校验 + IsHostnameAllowed SSRF 防护,非同源 URL 保持原样不改写。

7.1 GET /songs/{id}/hls/playlist -- 代理播放列表 ​

路径: /api/v1/songs/{id}/hls/playlist | 方法: GET / HEAD | 认证: BearerAuth

反代 HLS 子层 m3u8。拉取上游 -> 同源校验 -> 改写 URI -> 回写。上游错误透传,播放列表上限 1 MB。

参数: id (path, int) | u (query, string, 必填, base64url 编码的上游 URL) | access_token (query, string, 可选)

成功 (200): 改写后 m3u8 文本,Content-Type: application/vnd.apple.mpegurl,Cache-Control: no-store

错误: 400 无效 ID / 缺少 u | 403 非同源 / 主机不允许 | 404 歌曲不存在 | 502 上游不可用


7.2 GET /songs/{id}/hls/segment -- 代理切片 ​

路径: /api/v1/songs/{id}/hls/segment | 方法: GET / HEAD | 认证: BearerAuth

反代音频切片/key/init 段。透传 Range 和上游响应头。

参数: id (path, int) | u (query, string, 必填, base64url 编码的上游 URL) | access_token (query, string, 可选)

成功: 200 切片内容 | 206 Range 部分内容。Cache-Control: no-store

错误: 400 无效 ID / 缺少 u | 403 非同源 / 主机不允许 | 404 歌曲不存在 | 502 上游不可用


8. 音轨与参与歌手 ​

章节来源: internal/handlers/music.go、internal/services/cache_service_transcode.go(AudioTrackInfo)、internal/models/artist.go

8.1 GET /songs/{id}/audio-tracks -- 音频流列表 ​

路径: /api/v1/songs/{id}/audio-tracks | 认证: BearerAuth

用 ffprobe 探测该歌曲文件的音频流,返回每条流的 audio-relative index(对应 ffmpeg -map 0:a:N)、title、language、codec、default。主要用于 Web 端双音轨(原唱/伴奏 mka)切换:前端据数量决定是否显示切轨入口,并用 index 调 /songs/{id}/play?track=N 抽轨播放。仅本地歌曲(或已落地缓存的网络歌曲)有文件可探测;无可探测文件或音频流 < 2 条时也正常返回(前端据此不显示切轨)。运行时按需探测,不落库。

路径参数: id (int, 必填)

成功响应 (200): {"tracks": [{"index": 0, "title": "", "language": "chi", "codec": "aac", "default": true}]}(无文件时返回 {"tracks": []})

错误: 400 无效 ID | 404 歌曲不存在


8.2 GET /songs/{id}/tracks -- 全部轨道枚举 ​

路径: /api/v1/songs/{id}/tracks | 认证: BearerAuth

枚举歌曲文件中的音频轨道,返回精简信息(index / codec / language / title,无 default 字段),以顶层数组形式返回。与 8.1 互补:8.1 返回带 default 标志的对象包装,用于双音轨切换;本端点用于通用轨道列举。

路径参数: id (int, 必填)

成功响应 (200): [{"index": 0, "codec": "aac", "language": "chi", "title": ""}](无文件时返回 [])

错误: 400 无效 ID | 404 歌曲不存在


8.3 GET /songs/{id}/artists -- 参与歌手 ​

路径: /api/v1/songs/{id}/artists | 认证: BearerAuth

返回歌曲的参与歌手列表(规范化多值歌手,迁移 0038 引入)。每项含歌手实体(id / name / created_at)、role(artist 主唱/表演者,或 album_artist 专辑歌手)与同角色内 position。用于对唱/合唱歌曲按搭档检索与展示。

路径参数: id (int, 必填)

成功响应 (200): {"artists": [{"artist": {"id": 1, "name": "周杰伦", "created_at": "2024-01-01T12:00:00Z"}, "role": "artist", "position": 0}]}(无关联时返回 {"artists": []})

错误: 400 无效 ID | 404 歌曲不存在 | 500 获取参与歌手失败

可用 PUT /songs/{id}/artists 全量替换参与歌手(详见 Swagger)。