# 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. 结论与总架构 采用三层结构: 1. `Music_Cloud` - 部署位置:NAS / 内网 - 角色:内容真相源、下载与存储资产管理、运维控制台 2. `Public Music Service` - 部署位置:公网服务器 - 角色:内容查询、播放解析、播放器后端、MusicFree 兼容接口 3. `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_id` - `generated_at` - `schema_version` - `playlist_count` - `track_count` - `file_count` - `cover_count` 发布方式推荐: 1. NAS 导出快照 2. 上传到对象存储或通过 `rsync/scp` 推送到公网服务器 3. 公网服务下载或接收最新成功快照 4. 先写到临时路径 5. 校验清单后原子替换线上读库 ### 5.3 只读目录镜像库 第一版不要求完整复制 `catalogsync.db` 全表,只同步公网查询真正需要的字段。 建议镜像库包含这些读模型表: - `catalog_playlists` - `playlist_id` - `platform` - `remote_playlist_id` - `source_kind` - `name` - `description` - `cover_url` - `play_count` - `song_count` - `synced_at` - `updated_at` - `catalog_tracks` - `song_id` - `platform` - `remote_song_id` - `name` - `singers` - `album` - `cover_url` - `duration_ms` - `metadata_json` - `catalog_playlist_tracks` - `playlist_id` - `song_id` - `position` - `catalog_track_files` - `song_id` - `quality_label` - `ext` - `file_size_bytes` - `backend_type` - `backend_name` - `locator` - `public_url` - `status` - `is_primary` - `catalog_track_presence` - `song_id` - `has_local` - `has_object_storage` - `has_private_origin` - `has_external_fallback` - `catalog_toplists` - `toplist_id` - `platform` - `name` - `description` - `cover_url` - `play_count` - `song_count` - `group_name` 说明: - 这些表是导出后的读模型,不要求与 `catalogsync` 内部表名完全一致 - 读模型允许从 `playlists / songs / playlist_songs / file_locations / song_backend_presence` 聚合而来 - 公网服务不依赖 `job_*` 运维表 ## 6. 存储与播放资源策略 ### 6.1 播放源优先级 公网服务按以下顺序选择播放源: 1. 对象存储 / CDN 上已有可播文件 2. 公网服务本地缓存文件 3. NAS 私有回源 4. 外部平台解析回落 说明: - 第 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` 现有插件接口,插件需要实现这些方法: - `getRecommendSheetTags` - `getRecommendSheetsByTag` - `getMusicSheetInfo` - `getTopLists` - `getTopListDetail` - `getMediaSource` 映射如下: - `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 返回对象 歌单基础对象: ```json { "id": "catalogsync:playlist:18165", "platform": "catalogsync", "title": "经典老歌:免费下载重温好旋律", "coverImg": "https://public-host/mf/v1/covers/playlists/18165", "description": "netease / 歌单广场", "worksNum": 126 } ``` 歌曲基础对象: ```json { "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` 用途: - 返回推荐歌单标签分组 响应示例: ```json { "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` 查询参数: - `tag` - `page` - `page_size` - `platform` - `sort` 默认排序: - `play_count_desc` 默认过滤: - 仅返回 `song_count > 0` 的歌单 #### `GET /mf/v1/playlists/{id}` 返回歌单头信息: - `id` - `title` - `coverImg` - `description` - `worksNum` - `playCount` #### `GET /mf/v1/playlists/{id}/tracks` 查询参数: - `page` - `page_size` 返回: - `sheetItem` - 仅第 1 页可返回 - `musicList` - `isEnd` #### `GET /mf/v1/toplists` 返回榜单分组数组,每组包含: - `title` - `data` 每个榜单对象字段与歌单对象兼容。 #### `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` 请求体示例: ```json { "song_id": "catalogsync:song:3476", "quality": "super" } ``` 响应体示例: ```json { "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` 查询参数: - `scope` - `recommend` - `toplist` - `favorite` - `mine` - `page` - `page_size` - `sort` #### `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_id` - `played_at` - `progress_seconds` - `source_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` 数据 第一版继续把这些数据视为事实来源: - `playlists` - `songs` - `playlist_songs` - `file_assets` - `file_locations` - `song_backend_presence` ### 10.2 建议补强字段 为了让公网服务更顺滑,建议在导出链路或源库中确保这些字段可用: - `playlists.cover_url` - `playlists.description` - `playlists.play_count` - `playlists.collected_song_count` - `songs.cover_url` - `songs.duration_ms` 要求: - 能拿到就导出 - 拿不到就允许为空 - 不要求第一版先完成所有平台补全 ### 10.3 Public Music Service 自身数据 公网服务不直接复用 `catalogsync.db` 作为用户库。 它需要至少两份本地数据: 1. `catalog_read.db` - 只读目录镜像库 - 来源是 `Music_Cloud` 导出 2. `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/resolve` - `stream_service` - 对象存储优先 - 可选 NAS 私有回源 - 外部平台回落 目标: - 让 `MusicFree` 可以实际播放 ### Phase 3: 播放器后端 交付内容: - `/player/v1/home` - `/player/v1/playlists/*` - `/player/v1/me/favorites/*` - `/player/v1/me/history` - `player.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`:与公网服务同部署、面向未来播放器前端的用户态 API - `Object Storage / CDN`:首选公网分发层 核心决策定为: - 公网服务与 `Music_Cloud` 分开部署 - 公网服务不直接读 NAS 主库,采用目录镜像快照 - `MusicFree` 只连 `/mf/v1/*` - 未来网页播放器只连 `/player/v1/*` - 播放优先级为“对象存储 -> 公网缓存 -> NAS 私有回源 -> 外部平台回落” 这份 spec 作为后续 implementation plan 的唯一设计依据。