20 KiB
Music_Cloud 公网音乐服务方案设计
日期:2026-04-19
状态:已确认设计,待实现计划
范围:Music_Cloud / catalogsync、独立部署的 Public Music Service、MusicFree 插件接入、播放器后端 API
1. 背景
当前 Music_Cloud / catalogsync 已经承担了这些职责:
- 从多个平台采集歌单池、榜单、歌曲元数据
- 同步歌单歌曲关系、歌手关系、热度数据
- 多源搜歌、解析、下载、去重
- 维护本地文件、对象存储、
file_locations、song_backend_presence - 提供运维后台,用于采集、同步、下载、上传、巡检
它本质上是内容底座和文件资产库,而不是面向最终听歌用户的播放器服务端。
接下来要补的是一套独立于 Music_Cloud 的公网服务,使其可以:
- 为
MusicFree提供完整音源插件所需接口 - 为未来自建网页播放器或 App 提供播放器后端接口
- 部署在另一台公网服务器,不与 NAS 上的
Music_Cloud混为一个服务
2. 目标
本方案的目标是:
- 保持
Music_Cloud只负责采集、同步、下载、上传、运维 - 新建一个可独立部署的
Public Music Service MusicFree插件只做薄适配,不直连数据库,不承担复杂播放回落逻辑- 公网服务优先播放
Music_Cloud已下载或已上传的文件 - 当本地或对象存储无可播文件时,公网服务再回落到外部平台解析
- 同时为未来网页播放器保留用户态 API 边界
非目标:
- 本阶段不设计社交、评论、推荐算法、多人权限系统
- 本阶段不把
catalogsync运维后台改造成前台播放器 - 本阶段不要求把现有
catalogsync.db直接暴露到公网
3. 结论与总架构
采用三层结构:
Music_Cloud- 部署位置:NAS / 内网
- 角色:内容真相源、下载与存储资产管理、运维控制台
Public Music Service- 部署位置:公网服务器
- 角色:内容查询、播放解析、播放器后端、MusicFree 兼容接口
Object Storage / CDN- 部署位置:云存储或兼容对象存储
- 角色:公网分发音频文件与封面
核心原则:
Music_Cloud与公网服务物理分开、职责分开- 公网服务默认读取本机只读目录镜像,不跨公网直接读 NAS 的 SQLite
- 公网服务可选通过私有链路向 NAS 请求未上传资源,但该链路不对公网开放
MusicFree和未来网页播放器只连接公网服务,不直连 NAS
4. 组件边界
4.1 Music_Cloud
保留现有职责:
- 歌单采集:歌单广场、榜单、手工输入歌单
- 歌单同步:歌单详情、歌曲列表、歌手派生
- 下载:跨平台搜歌、候选优选、落盘、去重
- 上传:对象存储补传、
file_locations更新、song_backend_presence刷新 - 运维:任务编排、日志、暂停恢复、巡检去重
新增一个导出职责:
- 导出供公网服务使用的目录镜像快照
4.2 Public Music Service
这是本方案新增的主服务。对外暴露两个命名空间:
/mf/v1/*- 面向
MusicFree插件 - 返回结构尽量贴近
MusicFree需要的字段
- 面向
/player/v1/*- 面向自建网页播放器或 App
- 提供用户态能力,例如收藏、历史、播放上下文
其内部拆为 6 个模块:
catalog_reader- 读取目录镜像库
playlist_service- 推荐歌单、榜单、歌单详情、歌曲分页
cover_service- 封面定位、代理、缓存
media_resolver- 选播放源、做优先级回落
stream_service- 签名流地址、Range、代理转发
player_service- 收藏、历史、最近播放、用户歌单等用户态能力
4.3 MusicFree 插件
插件是薄层,不负责:
- 数据库存取
- 文件路径选择
- 多平台解析
- 对象存储鉴权
- NAS 私有回源
插件只负责:
- 调用
/mf/v1/* - 把响应映射为
MusicFree插件结构 - 把
sheetItem/musicItem的稳定 ID 传回服务端
5. 数据同步方案
5.1 选择的方案
采用“快照镜像”而不是“公网服务直读 NAS 主库”。
理由:
- 主库仍由
Music_Cloud独占写入,风险最小 - 公网服务读本机只读库,延迟稳定,运维简单
- 即使 NAS 暂时不可达,公网服务也能继续提供上一次成功快照
- 后续可以升级到增量同步,但第一版不需要先上复杂 CDC
5.2 同步链路
Music_Cloud 在这些时机触发目录快照导出:
collect任务完成后sync任务完成后download任务完成后upload任务完成后- 定时兜底任务,例如每 30 分钟一次
导出产物:
- 一个目录镜像数据库,例如
catalog_read.db - 一个清单文件,例如
manifest.json - 可选封面缓存目录
manifest.json 至少包含:
snapshot_idgenerated_atschema_versionplaylist_counttrack_countfile_countcover_count
发布方式推荐:
- NAS 导出快照
- 上传到对象存储或通过
rsync/scp推送到公网服务器 - 公网服务下载或接收最新成功快照
- 先写到临时路径
- 校验清单后原子替换线上读库
5.3 只读目录镜像库
第一版不要求完整复制 catalogsync.db 全表,只同步公网查询真正需要的字段。
建议镜像库包含这些读模型表:
catalog_playlistsplaylist_idplatformremote_playlist_idsource_kindnamedescriptioncover_urlplay_countsong_countsynced_atupdated_at
catalog_trackssong_idplatformremote_song_idnamesingersalbumcover_urlduration_msmetadata_json
catalog_playlist_tracksplaylist_idsong_idposition
catalog_track_filessong_idquality_labelextfile_size_bytesbackend_typebackend_namelocatorpublic_urlstatusis_primary
catalog_track_presencesong_idhas_localhas_object_storagehas_private_originhas_external_fallback
catalog_topliststoplist_idplatformnamedescriptioncover_urlplay_countsong_countgroup_name
说明:
- 这些表是导出后的读模型,不要求与
catalogsync内部表名完全一致 - 读模型允许从
playlists / songs / playlist_songs / file_locations / song_backend_presence聚合而来 - 公网服务不依赖
job_*运维表
6. 存储与播放资源策略
6.1 播放源优先级
公网服务按以下顺序选择播放源:
- 对象存储 / CDN 上已有可播文件
- 公网服务本地缓存文件
- NAS 私有回源
- 外部平台解析回落
说明:
- 第 1 优先级最适合公网分发
- 第 2 优先级用于热点缓存或调试
- 第 3 优先级只走内网或私有网络,不暴露给公网
- 第 4 优先级用于“歌单来自平台 A,但平台 A 拿不到时到其他平台找同名候选”
6.2 NAS 私有回源
为避免“未上传但已下载的歌”在公网不可播,设计一个可选的私有回源能力:
Music_Cloud暴露一个仅内网可访问的私有 origin 接口- 该接口只接受来自公网服务的签名请求
- 该接口只负责:
- 验签
- 定位本地文件
- 以只读方式返回流
不允许:
- 对公网暴露 NAS 本地路径
- 公网直接浏览 NAS 目录
MusicFree或网页端直接访问 NAS
6.3 外部平台回落
当 Music_Cloud 中不存在可播放文件时,media_resolver 可回落到外部平台解析。
回落策略:
- 按歌曲名、歌手名进行跨平台搜索
- 候选优先匹配高可信条目
- 在可信候选内优先更高音质和更大文件
- 若音质接近,按配置的下载源顺序决定优先级
回落结果默认用于在线播放,不强制自动入库。
原因:
- 播放链路要快
- 不把播放器请求与下载入库任务耦合
- 避免用户点击播放时触发重型后台任务
7. MusicFree 兼容接口设计
7.1 插件能力映射
根据 MusicFree 现有插件接口,插件需要实现这些方法:
getRecommendSheetTagsgetRecommendSheetsByTaggetMusicSheetInfogetTopListsgetTopListDetailgetMediaSource
映射如下:
getRecommendSheetTags()GET /mf/v1/recommend/tags
getRecommendSheetsByTag(tag, page)GET /mf/v1/recommend/sheets
getMusicSheetInfo(sheetItem, page)GET /mf/v1/playlists/{id}GET /mf/v1/playlists/{id}/tracks?page={page}
getTopLists()GET /mf/v1/toplists
getTopListDetail(topListItem, page)GET /mf/v1/toplists/{id}GET /mf/v1/toplists/{id}/tracks?page={page}
getMediaSource(musicItem, quality)POST /mf/v1/media/resolve
7.2 稳定 ID 规范
插件与服务端交互时使用稳定前缀 ID:
- 歌单:
catalogsync:playlist:{playlist_id} - 榜单:
catalogsync:toplist:{toplist_id} - 歌曲:
catalogsync:song:{song_id}
这样做的目的是:
- 避免与其他插件的裸 ID 冲突
- 插件端可以稳定回传主键
- 服务端未来调整数据库内部结构时,不破坏插件协议
7.3 返回对象
歌单基础对象:
{
"id": "catalogsync:playlist:18165",
"platform": "catalogsync",
"title": "经典老歌:免费下载重温好旋律",
"coverImg": "https://public-host/mf/v1/covers/playlists/18165",
"description": "netease / 歌单广场",
"worksNum": 126
}
歌曲基础对象:
{
"id": "catalogsync:song:3476",
"platform": "catalogsync",
"title": "海屿你",
"artist": "马也 / Crabbit",
"album": "",
"artwork": "https://public-host/mf/v1/covers/songs/3476",
"duration": 0
}
7.4 /mf/v1/* 详细接口
GET /mf/v1/recommend/tags
用途:
- 返回推荐歌单标签分组
响应示例:
{
"pinned": [
{ "id": "all", "title": "全部" },
{ "id": "netease", "title": "网易云" },
{ "id": "qq", "title": "QQ音乐" },
{ "id": "kuwo", "title": "酷我" }
],
"data": [
{
"title": "来源",
"data": [
{ "id": "playlist_square", "title": "歌单广场" },
{ "id": "toplist", "title": "排行榜" }
]
}
]
}
GET /mf/v1/recommend/sheets
查询参数:
tagpagepage_sizeplatformsort
默认排序:
play_count_desc
默认过滤:
- 仅返回
song_count > 0的歌单
GET /mf/v1/playlists/{id}
返回歌单头信息:
idtitlecoverImgdescriptionworksNumplayCount
GET /mf/v1/playlists/{id}/tracks
查询参数:
pagepage_size
返回:
sheetItem- 仅第 1 页可返回
musicListisEnd
GET /mf/v1/toplists
返回榜单分组数组,每组包含:
titledata
每个榜单对象字段与歌单对象兼容。
GET /mf/v1/toplists/{id}
返回榜单头信息。
GET /mf/v1/toplists/{id}/tracks
与歌单 tracks 分页结构一致。
GET /mf/v1/covers/playlists/{id}
返回歌单封面。
约束:
- 不要求 Bearer Token
- 可直接返回对象存储地址、静态文件或代理流
- 应允许较长缓存时间
GET /mf/v1/covers/songs/{id}
返回歌曲封面,约束与歌单封面一致。
POST /mf/v1/media/resolve
请求体示例:
{
"song_id": "catalogsync:song:3476",
"quality": "super"
}
响应体示例:
{
"song_id": "catalogsync:song:3476",
"selected_source": {
"kind": "object_storage",
"backend": "main-s3",
"quality": "super",
"ext": "flac",
"size_bytes": 42345678
},
"stream": {
"url": "https://public-host/mf/v1/media/stream/eyJhbGciOi...",
"headers": {},
"expires_at": "2026-04-19T12:00:00Z",
"range_supported": true
}
}
8. 播放器后端接口设计
/player/v1/* 面向未来网页播放器或 App,不要求与 MusicFree 插件接口一致。
第一版范围只做单用户自用,不设计复杂账号体系。
8.1 用户能力范围
第一版支持:
- 首页聚合
- 推荐歌单列表
- 榜单列表
- 歌单详情
- 歌曲播放解析
- 收藏歌曲
- 收藏歌单
- 最近播放
- 播放记录上报
第一版不支持:
- 社交评论
- 多用户协作歌单
- 站内消息
- 推荐算法
- 付费体系
8.2 /player/v1/* 接口
GET /player/v1/home
返回首页聚合数据:
- 热门歌单
- 榜单分组
- 最近播放
- 收藏入口
GET /player/v1/playlists
查询参数:
scoperecommendtoplistfavoritemine
pagepage_sizesort
GET /player/v1/playlists/{id}
返回歌单头信息和统计信息。
GET /player/v1/playlists/{id}/tracks
返回歌曲分页列表。
POST /player/v1/tracks/{id}/play
返回播放器使用的播放地址,可内部复用 media_resolver。
GET /player/v1/me/favorites/tracks
返回收藏歌曲列表。
PUT /player/v1/me/favorites/tracks/{id}
收藏歌曲。
DELETE /player/v1/me/favorites/tracks/{id}
取消收藏歌曲。
GET /player/v1/me/favorites/playlists
返回收藏歌单列表。
PUT /player/v1/me/favorites/playlists/{id}
收藏歌单。
DELETE /player/v1/me/favorites/playlists/{id}
取消收藏歌单。
GET /player/v1/me/history
返回最近播放记录。
POST /player/v1/me/history
写入播放记录,字段包括:
track_idplayed_atprogress_secondssource_kind
9. 鉴权与安全
9.1 API 鉴权
/mf/v1/* 与 /player/v1/* 的 JSON 接口统一使用 Bearer Token。
第一版采用单用户 Token 模型:
- 服务端配置一个或少量长期 Token
- MusicFree 插件和网页播放器都用这个 Token
后续如需多用户,再升级为真正的登录体系。
9.2 封面访问
封面地址不适合依赖 Bearer Header,因为:
MusicFree的coverImg/artwork仅提供 URL- 图片加载通常不会自动带业务鉴权头
因此第一版采用以下之一:
- 封面接口直接公开只读访问
- 或返回可长期缓存的签名图片 URL
推荐第一版直接公开封面接口,因为风险低、实现简单。
9.3 播放地址保护
播放地址不能直接暴露真实文件路径、对象存储原始 Key、NAS 本地路径。
规则:
resolve接口只返回短时有效的流地址- 该地址带签名和过期时间
- 服务端验签后再执行真正的文件流读取或转发
要求:
- 支持
Range - 支持 HEAD 或等效元信息读取
- 支持必要的透传 Header
10. 数据库与存储设计
10.1 继续复用的 Music_Cloud 数据
第一版继续把这些数据视为事实来源:
playlistssongsplaylist_songsfile_assetsfile_locationssong_backend_presence
10.2 建议补强字段
为了让公网服务更顺滑,建议在导出链路或源库中确保这些字段可用:
playlists.cover_urlplaylists.descriptionplaylists.play_countplaylists.collected_song_countsongs.cover_urlsongs.duration_ms
要求:
- 能拿到就导出
- 拿不到就允许为空
- 不要求第一版先完成所有平台补全
10.3 Public Music Service 自身数据
公网服务不直接复用 catalogsync.db 作为用户库。
它需要至少两份本地数据:
catalog_read.db- 只读目录镜像库
- 来源是
Music_Cloud导出
player.db- 用户态数据库
- 保存收藏、历史、用户歌单、播放上下文
第一版可使用 SQLite:
- 对单用户自用足够
- 运维成本低
- 未来若用户态写入量明显增加,可平滑迁移到 PostgreSQL
11. 部署设计
11.1 Music_Cloud 部署
保持在 NAS:
- NAS 统一根目录:
/volume4/Music_Cloud catalogsync工作目录:/volume4/Music_Cloud/catalogsync- 数据库:
/volume4/Music_Cloud/catalogsync/data/catalogsync.db - 本地曲库目录:
/volume4/Music_Cloud/library - 歌单信息输出目录:
/volume4/Music_Cloud/playlists - 运维后台:
catalogsync serve
11.2 Public Music Service 部署
部署在公网服务器:
- 服务进程:一个 Web 服务
- 读库:
catalog_read.db - 用户库:
player.db - 缓存目录:封面缓存、热点流缓存
- 反向代理:Nginx / Caddy
若 Public Music Service 先部署在 NAS 上联调 / 过渡运行:
- 宿主机标准目录:
/volume4/Music_Cloud/Music_Server - Docker 项目目录:
/volume4/Music_Cloud/Music_Server/app - 运行时配置目录:
/volume4/Music_Cloud/Music_Server/config - 运行时数据目录:
/volume4/Music_Cloud/Music_Server/data - 运维脚本目录:
/volume4/Music_Cloud/Music_Server/bin - 不再使用旧路径:
/volume4/Music_Server下的app目录
11.3 对象存储
推荐启用对象存储作为主公网分发层:
- 音频文件 URL 优先走对象存储或 CDN
- 封面文件优先走对象存储或静态缓存
- 公网服务只负责解析、鉴权和必要的中转
12. 错误处理与降级
必须明确这些降级行为:
- 最新快照导入失败
- 继续使用上一次成功快照
- 对象存储不可用
- 回落到公网缓存、NAS 私有回源或外部平台
- 外部平台解析失败
- 返回“不可播放”,不触发自动下载
- 封面缺失
- 返回默认占位图
- 歌单歌曲数为 0
- 默认不在推荐列表返回
13. 测试策略
实现阶段至少覆盖这些测试:
- 目录快照导出与导入测试
- 目录镜像库查询测试
MusicFree插件协议契约测试- 歌单详情分页测试
- 榜单分页测试
media_resolver优先级测试Range流播放测试- Bearer Token / 流签名鉴权测试
- 收藏与历史 API 测试
手工验证至少包括:
- 在
MusicFree中成功加载推荐歌单 - 点进歌单后分页加载歌曲正常
- 榜单加载正常
- 已上传歌曲优先走对象存储
- 未上传但 NAS 有本地文件时,可通过私有回源播放
- 本地和私有回源都没有时,可回落到外部平台
14. 分阶段实施
Phase 1: 目录镜像与 MusicFree 浏览
交付内容:
Music_Cloud导出目录镜像快照- 公网服务加载
catalog_read.db /mf/v1/recommend/*/mf/v1/playlists/*/mf/v1/toplists/*/mf/v1/covers/*
目标:
- 先让
MusicFree能像音乐平台一样浏览歌单和榜单
Phase 2: 播放解析
交付内容:
/mf/v1/media/resolvestream_service- 对象存储优先
- 可选 NAS 私有回源
- 外部平台回落
目标:
- 让
MusicFree可以实际播放
Phase 3: 播放器后端
交付内容:
/player/v1/home/player/v1/playlists/*/player/v1/me/favorites/*/player/v1/me/historyplayer.db
目标:
- 为未来网页播放器或 App 提供后端
Phase 4: 首个前端播放器
交付内容:
- 自建网页播放器或客户端
说明:
- 该阶段不在本 spec 的实现范围内
- 本 spec 只负责把服务端接口边界设计清楚
15. 实施边界与仓库建议
建议保持仓库职责清晰,并以当前工作树为准:
D:\source\musicdl-catalog-sync-worktrees\catalog-sync- 继续承载
Music_Cloud / catalogsync - 负责目录镜像导出、私有回源签名能力、对象存储存在性同步
- 继续承载
D:\source\musicdl-catalog-sync-worktrees\Music_Server- 承载
Public Music Service - 包含
/mf/v1/*与/player/v1/* - 本 spec 与 implementation plans 也保存在这个仓库
- 承载
D:\source\musicdl-catalog-sync-worktrees\Music_Server\integrations\musicfree-plugin- 承载
MusicFree插件适配逻辑 - 最终产出单文件插件或对应构建产物
- 承载
第一阶段允许在 Music_Server 仓库内同时维护服务端代码和插件代码,但部署和运行时仍保持为两个独立产物。
16. 最终结论
最终架构定为:
Music_Cloud:内网内容底座Public Music Service:公网内容与播放服务MusicFree插件:薄适配层Player Backend:与公网服务同部署、面向未来播放器前端的用户态 APIObject Storage / CDN:首选公网分发层
核心决策定为:
- 公网服务与
Music_Cloud分开部署 - 公网服务不直接读 NAS 主库,采用目录镜像快照
MusicFree只连/mf/v1/*- 未来网页播放器只连
/player/v1/* - 播放优先级为“对象存储 -> 公网缓存 -> NAS 私有回源 -> 外部平台回落”
这份 spec 作为后续 implementation plan 的唯一设计依据。