SwingMusic 3.0.0 API 使用详解

SwingMusic 3.0.0 HTTP API 实测手册:JWT 鉴权与路由前缀、检索/详情/音频流/收藏/播放列表全链路 curl 实证、可编排 Python 演示脚本与五类对接排障

SwingMusicSwingMusic音频流媒体2026/10/30

SwingMusic 3.0.0 API 使用详解

目标:本文面向需要以脚本或第三方程序对接 SwingMusic 的读者,梳理 v3.0.0 的 HTTP API 面貌——鉴权方式、路由前缀规律、检索与播放链路、收藏与播放列表写操作、OpenAPI 规范获取,并附 curl 最小调用集。全部请求与响应均在 Ubuntu 24.04 服务器实测通过(服务端口 8154,曲库 4 首、2 张专辑、2 位艺术家)。

一、环境信息

  • 操作系统:Ubuntu 24.04(x86_64)
  • SwingMusic:v3.0.0(部署见《SwingMusic 3.0.0 Docker Compose 部署指南》)
  • 服务端口:8154(宿主)→ 1970(容器内)
  • 曲库:4 首测试音频(2 张专辑:Tidal Lines、晨雾专辑;2 位艺术家:Blue Harbor、秋日山道)
  • 演示账号:admin / q1w2e3r4
  • 工具:curl 8.x + python3

二、前置条件

  1. 服务在线且曲库已扫描(探活与初始化状态检查):
curl -s -o /dev/null -w 'GET / -> %{http_code}\n' http://localhost:8154/
curl -s http://localhost:8154/onboarding-data

实测输出:

GET / -> 200
{"adminExists":true,"onboardingComplete":true,"rootDirsSet":true,"scanMessage":"","userHome":"/music"}
  1. 已有管理员账号(向导创建,或 swingmusic --password-reset 重置口令)。

三、API 基础

3.1 协议与规范获取

SwingMusic 基于 Flask + flask_openapi3 构建,HTTP/1.1(无 WebSocket 业务端点),交互均为 JSON(音频流除外)。v3.0.0 提供完整 OpenAPI 文档站:

资源路径说明
Swagger UIGET /docs/交互式接口文档页
OpenAPI 规范GET /docs/openapi.json机读全量规范
curl -s -b /tmp/sm-ck.txt http://localhost:8154/docs/openapi.json -o /tmp/openapi.json \
  && python3 -c "
import json, collections
d = json.load(open('/tmp/openapi.json'))
paths = d['paths']
ops = [m for p in paths.values() for m in p if m in ('get','post','put','delete')]
print(d['info']['title'], '|', d['info']['version'])
print('paths:', len(paths), '| operations:', len(ops), '|', dict(collections.Counter(ops)))"

实测输出:

Swing Music API | 3.0.0
paths: 58 | operations: 95 | {'get': 61, 'post': 32, 'delete': 1, 'put': 1}

💡 技术拓展:SwingMusic 的 API 设计走「名词+动词路径」而非严格 RESTful——路径前缀表达资源域(/album、/artist、/getall…),动作用路径后缀表达(/favorites/add、/playlists/new)。对接时不必按 REST 语义猜状态码,以本文实测为准。

3.2 鉴权方式

全站 JWT Bearer 鉴权(中间件全局注入,静态资源与登录端点豁免)。流程:登录 → 取 accesstoken → 后续请求带 Authorization: Bearer <token>(或 Cookie 亦可)。

登录(无 /api 前缀,注意路由挂载在根路径):

curl -s -X POST http://localhost:8154/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"q1w2e3r4"}' \
  -c /tmp/sm-ck.txt | python3 -c "import json,sys; d=json.load(sys.stdin); print('accesstoken[:36]:', d['accesstoken'][:36], '...')"

实测输出:

accesstoken[:36]: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

响应体实测含 accesstoken / refreshtoken / maxage / msg 四个字段(maxage: 2592000 即 Cookie 有效期 30 天,msg: Logged in as admin);业务令牌为 accesstoken,过期后用 /auth/refresh 换新。以下两种携带方式等价,本文为简洁统一用 Cookie(curl -b 从登录响应自动落盘):

# 方式一:Authorization 头
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8154/getall/albums
# 方式二:登录 Cookie(-c 保存,-b 回放)
curl -s -b /tmp/sm-ck.txt http://localhost:8154/getall/albums

未携带令牌访问业务接口的实测表现:

curl -s -o /dev/null -w 'noauth -> %{http_code}\n' http://localhost:8154/getall/albums

实测输出:

noauth -> 401

3.3 路由前缀总览

58 个路径按前缀分域(openapi.json 实测归纳):

前缀域代表端点
/auth认证与配对/auth/login、/auth/profile/create、/auth/getpaircode、/auth/pair
/getall库列表分页/getall/albums、/getall/artists、/getall/folders、/getall/playlists
/album、/artist实体详情POST /album(详情+曲目)、GET /artist/<hash>、GET /artist/<hash>/albums
/search检索/search/top、/search/(loadmore 分页)
/file音频流GET /file/<trackhash>/legacy、GET /file/silence
/folder文件夹浏览POST /folder
/favorites收藏/favorites/add、/favorites/remove、/favorites/tracks
/playlists播放列表/playlists/new、/playlists/{id}/add、/playlists/{id}/delete
/notsettings管理/notsettings、/notsettings/update、/notsettings/trigger-scan
/home、/nothome首页聚合/nothome/recents/added 等

注意两组易混前缀:/home 与 /nothome、/settings 与 /notsettings——后者是前者「未登录态受限视图」的历史命名(响应结构一致,管理动作仅 /notsettings 组有)。

3.4 通用响应结构

  • 列表接口统一 {total, start, items} 三段(getall 系)或 {top_result, tracks, albums, artists}(search);
  • 详情接口首键 info,曲目数组 tracks;
  • 错误统一 {"error": "<原因>"} + 对应 HTTP 状态码;
  • 参数校验失败返回 FastAPI 风格 422 {"detail":[...]}(实测见 4.3)。

四、核心端点实测

4.1 曲库列表(/getall)

按字段排序与翻页。start 是列表项偏移(支持 -1 倒序取末页,实测正常),sortby 可用 title/artist/album/pin/year/duration/playbitrate 等:

curl -s -b /tmp/sm-ck.txt 'http://localhost:8154/getall/albums?start=0&sortby=title&reverse=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('total:', d['total'], '| albums:', [(i['title'], i.get('albumhash')) for i in d['items']])"

实测输出:

total: 2 | albums: [('Tidal Lines', '0a5ebd99f07e6619'), ('晨雾专辑', '0437d10dc607ae05')]

同域端点:/getall/artists、/getall/folders、/getall/playlists、/getall/ genres 等,参数与响应结构一致。

4.2 专辑详情(POST /album)

专辑详情是 POST 语义(请求体传 hash 与可选 limit),同时返回信息与曲目数组:

curl -s -b /tmp/sm-ck.txt -X POST http://localhost:8154/album \
  -H 'Content-Type: application/json' -d '{"albumhash":"0a5ebd99f07e6619","limit":2}' \
  | python3 -c "
import json,sys
d = json.load(sys.stdin)
print('title:', d['info']['title'], '| year:', d['info'].get('date'), '| trackcount:', d['info']['trackcount'])
for t in d['tracks']: print('  track', t['track'], '-', t['title'], '| hash:', t['trackhash'])"

实测输出:

title: Tidal Lines | year: 2025 | trackcount: 2
  track 2 - Sea Glass | hash: d1f72f5faf6c58cb
  track 1 - Salt Air | hash: 64ae7697bcc37102

info 内同时含 og_title(清洗前原始标题)与 cleanAlbumTitle 生效后的 title。注意两点:①响应内 tracks 数组按曲目序号倒序返回(脚本侧需自行按 track 字段正排);②曲目 hash 会随音频内容/标签变化,编排时以详情接口实时返回为准,勿硬编码。

4.3 艺术家详情与专辑(GET /artist)

curl -s -b /tmp/sm-ck.txt http://localhost:8154/artist/49749c843ec9a947 \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('artist:', d['artist']['name'], '| albumcount:', d['artist']['albumcount'])"

实测输出:

artist: Blue Harbor | albumcount: 1

响应为 {"artist": {...}, "albums": {"albums": [...], "appearances": [...], "compilations": [...], "singles_and_eps": [...]}} 双键结构,艺术家下挂四类专辑分组,脚本取数时注意层级。

参数错误时的 422 校验响应(artisthash 传了 15 位而非规定长度):

curl -s -b /tmp/sm-ck.txt http://localhost:8154/artist/49749c843ec9a9 -w '\nHTTP %{http_code}\n'

实测输出:

{"detail":[{"type":"string_too_long","loc":["path","artisthash"],"msg":"String should have at most 16 characters","input":"49749c843ec9a9","ctx":{"max_length":16}}]}
HTTP 422

4.4 检索(/search/top 与 /search/)

/search/top 一次给出 top_result 与三类分域结果,中文检索实测:

curl -s -b /tmp/sm-ck.txt 'http://localhost:8154/search/top?q=%E6%99%A8%E9%9B%BE' \
  | python3 -c "
import json,sys
d = json.load(sys.stdin)
print('top:', d['top_result']['title'], '| album:', d['top_result'].get('album'))
print('albums:', len(d['albums']), '| artists:', len(d['artists']), '| tracks:', len(d['tracks']))"

实测输出:

top: 晨雾 | album: 晨雾专辑
albums: 1 | artists: 0 | tracks: 2

/search/ 按 itemtype 分页取更多(itemtype=tracks/albums/artists/folders),响应为 {"more": bool, "results": [...]} 平铺结构:

curl -s -b /tmp/sm-ck.txt 'http://localhost:8154/search/?q=Sea%20Glass&itemtype=tracks' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('results:', [t['title'] for t in d['results']])"

实测输出:

results: ['Sea Glass']

4.5 音频流(/file)

按 trackhash 取流,请求体/查询参数三要素:filepath(绝对路径,防越权)、quality(original 或 1411/1024/512/320/256/128/96)、container(mp3/aac/flac/webm/ogg)。原文件直出实测:

curl -s -b /tmp/sm-ck.txt -D - -o /dev/null \
  'http://localhost:8154/file/64ae7697bcc37102/legacy?filepath=%2Fmusic%2FBlue%20Harbor%2FTidal%20Lines%2F01-Salt%20Air.mp3&quality=original&container=mp3' \
  | head -8

实测输出(响应头节选):

HTTP/1.1 200 OK
content-disposition: attachment; filename="01-Salt Air.mp3"
content-type: audio/mpeg
content-length: 193902
last-modified: Fri, 30 Oct 2026 00:52:48 GMT
cache-control: no-cache
etag: "1793321568.695645-193902-1984696201"

Range 播放实测(返回 206 部分内容,拖动进度条依赖此能力):

curl -s -b /tmp/sm-ck.txt -r 0-99 -o /dev/null \
  -w 'range -> %{http_code} type=%{content_type} got=%{size_download}B\n' \
  'http://localhost:8154/file/64ae7697bcc37102/legacy?filepath=%2Fmusic%2FBlue%20Harbor%2FTidal%20Lines%2F01-Salt%20Air.mp3&quality=original&container=mp3'

实测输出:

range -> 206 type=audio/mpeg got=100B

⚠️ 实测提醒:quality/container 参数在 v3.0.0 已定义但被忽略——传 quality=128 实测返回的仍是 193902 字节原文件(200),按需转码路由在当前版本源码中处于注释停用状态;镜像内置 ffmpeg(7.1.4)目前服务于扫描期的音轨分析而非播放转码。选型如强依赖服务端转码,请以官方仓库后续版本说明为准。

4.6 收藏(/favorites)

三类对象(type=track/album/artist)的增查删,收藏按曲目实测(hash 用 4.2 实取的 Salt Air):

curl -s -b /tmp/sm-ck.txt -X POST http://localhost:8154/favorites/add \
  -H 'Content-Type: application/json' -d '{"hash":"64ae7697bcc37102","type":"track"}'

实测输出:

{"msg":"Added to favorites"}

查询收藏曲目:

curl -s -b /tmp/sm-ck.txt 'http://localhost:8154/favorites/tracks?limit=3&start=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('total:', d['total'], '| tracks:', [t['title'] for t in d['tracks']])"

实测输出:

total: 1 | tracks: ['Salt Air']

取消收藏:

curl -s -b /tmp/sm-ck.txt -X POST http://localhost:8154/favorites/remove \
  -H 'Content-Type: application/json' -d '{"hash":"64ae7697bcc37102","type":"track"}'

实测输出:

{"msg":"Removed from favorites"}

4.7 播放列表(/playlists)

创建(响应 201,返回 playlist 对象)、列表(data 包装)、删除(DELETE 方法):

curl -s -b /tmp/sm-ck.txt -X POST http://localhost:8154/playlists/new \
  -H 'Content-Type: application/json' -d '{"name":"深夜电台"}' \
  -o /tmp/newpl.json -w 'create -> %{http_code}\n'
head -c 120 /tmp/newpl.json; echo
curl -s -b /tmp/sm-ck.txt http://localhost:8154/playlists \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('playlists:', [(p['id'], p['name'], p['count']) for p in d['data']])"

实测输出:

create -> 201
{"playlist":{"_last_updated":"","_score":0,"count":0,"duration":0,"extra":{},"has_image":false,"id":1,"image":"None","im
playlists: [(1, '深夜电台', 0)]

同名重复创建实测返回 409:

curl -s -b /tmp/sm-ck.txt -X POST http://localhost:8154/playlists/new \
  -H 'Content-Type: application/json' -d '{"name":"深夜电台"}' -w '\nHTTP %{http_code}\n'

实测输出:

{"error":"Playlist already exists"}
HTTP 409

加曲用 /playlists/{id}/add,请求体字段是 itemhash(传 trackhash 会得到 422 缺参校验):

curl -s -b /tmp/sm-ck.txt -X POST http://localhost:8154/playlists/1/add \
  -H 'Content-Type: application/json' -d '{"itemhash":"64ae7697bcc37102"}' -w '\nHTTP %{http_code}\n'

实测输出:

{"msg":"Done"}

HTTP 200

随后列表 count 实测变为 1。删除用 DELETE 方法(POST 会得到 405):

curl -s -b /tmp/sm-ck.txt -X DELETE http://localhost:8154/playlists/1/delete -w '\nHTTP %{http_code}\n'

实测输出:

{"msg":"Done"}

HTTP 200

其余管理端点:/playlists/{id}/update(改名/置顶设置)、/playlists/{id}/pin_unpin、/playlists/{id}/remove-tracks、/playlists/{id}/remove-img,均按 openapi.json 中各 schema 传参。

4.8 管理端点(/notsettings)

配置查询、运行时改参、触发扫描与根目录管理四件套(改参的键清单与生效机制详见《SwingMusic 3.0.0 配置参数详解》):

curl -s -b /tmp/sm-ck.txt -X PUT http://localhost:8154/notsettings/update \
  -H 'Content-Type: application/json' -d '{"key":"scanInterval","value":10}'

实测输出:

{"msg":"Config updated!"}
curl -s -b /tmp/sm-ck.txt http://localhost:8154/notsettings/trigger-scan

实测输出:

{"msg":"Scan triggered!"}

五、可执行编排脚本

下面这个 Python 脚本串联完整业务链路,可直接落地复用:登录 → 检索 → 取专辑详情 → 收藏 → 建播放列表 → 取流播放(Range 前缀请求)。保存为 swingmusic_demo.py:

#!/usr/bin/env python3
"""SwingMusic 3.0.0 API 链路演示:登录→检索→详情→收藏→播放列表→取流。"""
import json, urllib.parse, urllib.request

BASE = "http://localhost:8154"
FILEPATH = "/music/Blue Harbor/Tidal Lines/01-Salt Air.mp3"

def call(path, data=None, token=None, method=None):
    req = urllib.request.Request(BASE + path, method=method or ("POST" if data else "GET"))
    req.add_header("Content-Type", "application/json")
    if token:
        req.add_header("Authorization", "Bearer " + token)
    body = json.dumps(data).encode() if data else None
    with urllib.request.urlopen(req, body) as r:
        return json.loads(r.read())

# 1. 登录
tok = call("/auth/login", {"username": "admin", "password": "q1w2e3r4"})["accesstoken"]
print("1. login ok, token len =", len(tok))

# 2. 检索中文关键词
top = call("/search/top?q=%E6%99%A8%E9%9B%BE", token=tok)
print("2. top_result =", top["top_result"]["title"])

# 3. 专辑详情(按曲目序号取第一轨——注意响应内曲目顺序为倒序)
alb = call("/album", {"albumhash": "0a5ebd99f07e6619", "limit": 2}, token=tok)
t1 = sorted(alb["tracks"], key=lambda t: t["track"])[0]
print("3. album =", alb["info"]["title"], "| first track =", t1["title"])

# 4. 收藏该曲目并回查总数
call("/favorites/add", {"hash": t1["trackhash"], "type": "track"}, token=tok)
favs = call("/favorites/tracks?limit=1&start=0", token=tok)
print("4. favorites total =", favs["total"])

# 5. 建播放列表(先建空表再单曲追加,预置 tracks 参数不参与计数)
pl = call("/playlists/new", {"name": "API 演示列表"}, token=tok)
call(f"/playlists/{pl['playlist']['id']}/add", {"itemhash": t1["trackhash"]}, token=tok)
pls = call("/playlists", token=tok)
print("5. playlists =", [(p["name"], p["count"]) for p in pls["data"]])

# 6. 取流前 256 字节(Range 播放前缀)
req = urllib.request.Request(
    f"{BASE}/file/{t1['trackhash']}/legacy?filepath={urllib.parse.quote(FILEPATH)}"
    "&quality=original&container=mp3")
req.add_header("Authorization", "Bearer " + tok)
req.add_header("Range", "bytes=0-255")
with urllib.request.urlopen(req) as r:
    print("6. stream =", r.status, r.headers["Content-Type"], len(r.read()), "bytes")

运行:

python3 swingmusic_demo.py

实测输出:

1. login ok, token len = 355
2. top_result = 晨雾
3. album = Tidal Lines | first track = Salt Air
4. favorites total = 1
5. playlists = [('API 演示列表', 1)]
6. stream = 206 audio/mpeg 256 bytes

💡 注意第 6 步:收藏与播放列表是可变操作,脚本每次运行 count/total 会随之增长;生产集成请先查后写(避免重复收藏)。

六、常见问题(FAQ)

Q1:接口 404?先查前缀。 SwingMusic 的 auth 蓝图挂载在根路径——登录是 /auth/login 而非 /api/auth/login(两形态实测只有前者 200);同理 getall/search/favorites/playlists 都不带 /api 前缀。全量路径以 GET /docs/openapi.json 为准。

Q2:401 但确定带了 token? 实测 401 一般三种:① token 没带上(Authorization 头拼写、Cookie 未回放);② 服务端 private.key/serverId 变更导致旧 token 全部失效(重建配置目录后必现,重新登录即可);③ 访问了管理端点但当前用户非 admin(/notsettings 组要求管理员,普通用户实测同样 401/404 依端点而定)。

Q3:422 校验错误怎么读? FastAPI 风格 {"detail":[{type,loc,msg,ctx}]},loc 指明是 path/body 哪个参数越界(实测 artisthash 长度超限见 4.3)。对照 openapi.json 中该端点的 schema 修正入参即可。

Q4:播放接口报错找不到文件? filepath 参数必须与该 trackhash 在库中的绝对路径完全一致(容器内视角,如 /music/...),且 URL 编码。不一致时实测返回 422/错误 JSON。做法:先查详情拿 trackhash 与 filepath,再原样拼流接口。

Q5:移动 App 怎么配对? 不手填 token:App 扫码/取码走 GET /auth/getpaircode(生成配对码)→ POST /auth/pair 交换会话,与本文 Bearer 流程并存,见 openapi.json /auth 域。

七、参考链接

  1. 官方仓库:https://github.com/swingmx/swingmusic (api/ 目录为各端点源码权威,v3.0.0)
  2. 本机接口文档:http://<服务器IP>:8154/docs/(Swagger UI)
  3. 本机 OpenAPI 规范:http://<服务器IP>:8154/docs/openapi.json
  4. 站内延伸:《SwingMusic 3.0.0 介绍与选型指南》、《SwingMusic 3.0.0 Docker Compose 部署指南》、《SwingMusic 3.0.0 配置参数详解》、《mStream 6.32.0 API 使用详解》