# Music_Server 多类型搜索与歌手详情设计 日期:2026-04-23 状态:已确认设计,待实现计划 范围:`catalog-sync`、`Music_Server`、`MusicFree` ## 1. 背景 当前 `Music_Server` 面向 MusicFree 插件只支持单曲搜索,链路上存在三个直接影响使用的问题: 1. 搜索页只能返回 `单曲`,无法返回 `歌手` 和 `歌单`。 2. 即使补出歌手搜索结果,当前 MusicFree 的歌手详情页默认固定为 `单曲 / 专辑` 两个 tab,而本次目标是不再展示专辑,只展示该歌手的全部歌曲。 3. 当前已有榜单详情能力,但如果把榜单混入“歌单搜索”结果,插件详情入口还不能自动识别并转发到榜单详情接口。 当前三端职责边界保持不变: - `catalog-sync` 负责采集、同步、下载和维护源库 `catalogsync.db`。 - `Music_Server` 负责把源库导出成只读快照,并对 MusicFree 插件暴露 `/mf/v1/*` 接口。 - `MusicFree` 负责插件调用、结果展示和详情页交互,不直接访问源站接口或服务端数据库。 本轮需要解决的是: 1. `Music_Server` 支持 `单曲 / 歌手 / 歌单` 三类搜索。 2. `歌手` 搜索结果按平台分别显示,不做跨平台合并。 3. 点进歌手后只展示该歌手的全部可播歌曲,不再展示专辑。 4. `歌单` 搜索同时返回普通歌单和现有榜单,并且点进去后能正常打开对应详情。 ## 2. 目标与非目标 ### 2.1 目标 - `Music_Server` 暴露 MusicFree 兼容的三类搜索接口。 - `Music_Server` 导出稳定的歌手只读模型,避免运行时从 `songs.singers` 临时反推。 - `Music_Server` 只返回至少包含 1 首可播歌曲的歌手和歌单结果。 - `Music_Server` 歌手详情只返回该歌手的可播歌曲列表,并支持分页。 - `Music_Server` 歌单搜索同时覆盖普通歌单和榜单。 - `Music_Server` 插件同时支持 `music`、`artist`、`sheet` 三类搜索。 - `MusicFree` 对 `Music_Server` 歌手详情仅展示单曲列表,不展示专辑 tab。 ### 2.2 非目标 - 不做跨平台歌手身份合并。 - 不做歌手专辑详情能力。 - 不改动其它插件的歌手详情行为。 - 不新增新的播放解析策略。 - 不改动 MusicFree 全局搜索架构,只做与 `Music_Server` 插件兼容所需的最小改动。 ## 3. 方案选择 评估过三个方向: 1. 运行时从 `catalog_tracks.singers` 现算歌手搜索和详情。 2. 在导出阶段补歌手读模型,再由服务端和插件接入。 3. 服务端只提供少量接口,更多聚合逻辑放到插件端处理。 本轮选择方案 2,原因如下: - `catalog-sync` 源库已经有 `artists` 和 `artist_songs` 表,可以稳定表达“平台内歌手”和“歌手作品关系”。 - 歌手详情需要分页、排序和稳定 id,用导出读模型比字符串现算更可靠。 - 榜单混入歌单搜索后,插件只需识别 id 类型并分发详情请求,不需要自己做复杂聚合。 ## 4. 核心设计 ### 4.1 读模型扩展 在 `catalog_read.db` 中新增两张表: #### `catalog_artists` - `artist_id integer primary key` - `artist_key text not null unique` - `platform text not null` - `remote_artist_id text` - `name text not null` - `normalized_name text not null` - `avatar_url text` - `description text` - `playable_song_count integer not null` 用途: - 作为歌手搜索结果和歌手详情头部的主表。 - `artist_id` 直接沿用源库 `artists.id`,避免重新映射。 - `artist_key` 沿用源库 `platform + remote_artist_id/normalized_name` 的稳定语义。 #### `catalog_artist_tracks` - `artist_id integer not null` - `song_id integer not null` - `position integer not null` 用途: - 存储歌手与歌曲的展开关系。 - 用于歌手详情分页和稳定排序。 排序约定: - `position` 由导出时确定,默认按歌曲名升序、`song_id` 升序生成稳定序号。 ### 4.2 导出策略 `Music_Server/scripts/export_catalog_read.py` 在现有导出流程基础上新增歌手导出: 1. 从源库 `artists` 读取歌手主体。 2. 通过 `artist_songs` 关联到 `songs`。 3. 通过 `file_assets + file_locations(status='active')` 过滤出当前可播歌曲。 4. 仅保留 `playable_song_count > 0` 的歌手进入 `catalog_artists`。 5. 将这些可播歌曲关系写入 `catalog_artist_tracks`。 补充规则: - 优先使用源库 `artists` 关系,不从 `songs.singers` 反推歌手。 - 如果歌手缺少头像或简介,允许写空。 - 如果歌手没有可播歌曲,不进入只读库,这样搜索和详情天然只面向可用结果。 ### 4.3 服务端接口 在现有 `/mf/v1` 之下新增四个接口: #### `GET /mf/v1/search/artists` 参数: - `q` - `page` - `page_size` 返回 MusicFree 兼容形状: - `isEnd` - `data` 单个结果字段: - `id`: `catalogsync:artist:{artist_id}` - `platform`: 平台名,例如 `netease` / `qq` / `kuwo` - `name` - `avatar` - `worksNum`: 可播歌曲数 - `description` - `supportedArtistTabs`: `["music"]` #### `GET /mf/v1/artists/{artist_id}` 返回歌手详情头部字段,至少包含: - `id` - `platform` - `name` - `avatar` - `worksNum` - `description` - `supportedArtistTabs` #### `GET /mf/v1/artists/{artist_id}/tracks` 参数: - `page` - `page_size` 返回: - `isEnd` - `musicList` 歌曲仍复用当前单曲对象结构和播放链路。 #### `GET /mf/v1/search/sheets` 参数: - `q` - `page` - `page_size` 语义: - 同时搜索 `catalog_playlists` 和 `catalog_toplists` - 将两类结果统一映射成 MusicFree 的 sheet 形状 单个结果字段: - `id` - 普通歌单:`catalogsync:playlist:{playlist_id}` - 榜单:`catalogsync:toplist:{toplist_id}` - `platform` - `title` - `coverImg` - `description` - `worksNum` - `playableSongCount` - `playCount` ### 4.4 查询语义与排序 #### 单曲搜索 保持现有语义: - 只返回有 `active` 文件位置的歌曲。 - 排序为: 1. 歌名精确匹配 2. 歌名前缀匹配 3. 歌名模糊匹配 4. 歌手名模糊匹配 5. `lower(name)` 升序 6. `song_id` 升序 #### 歌手搜索 只搜索 `catalog_artists`,并保持平台内独立: - 不做跨平台合并。 - 只返回 `playable_song_count > 0` 的歌手。 - 排序为: 1. 歌手名精确匹配 2. 歌手名前缀匹配 3. 歌手名模糊匹配 4. `playable_song_count desc` 5. `lower(name) asc` 6. `artist_id asc` #### 歌单搜索 同时搜索 `catalog_playlists.name` 和 `catalog_toplists.name`: - 只返回 `playable_song_count > 0` 的结果。 - 结果合并后按以下规则排序: 1. 标题精确匹配 2. 标题前缀匹配 3. 标题模糊匹配 4. `play_count desc` 5. 类型稳定顺序:普通歌单优先于榜单 6. 稳定 id 升序 ### 4.5 插件适配 `Music_Server` 的两个插件资产都要同步修改: - `src/music_server/plugin_assets/music_server.js` - `src/music_server/plugin_assets/music_server_lan.js` #### 搜索能力 - `supportedSearchType` 改为 `["music", "artist", "sheet"]` - `search(query, page, type)` 改为按类型分发: - `music` -> `/mf/v1/search/songs` - `artist` -> `/mf/v1/search/artists` - `sheet` -> `/mf/v1/search/sheets` #### 歌手映射 新增 `mapArtistItem(...)`,负责把服务端歌手结果映射到 MusicFree 形状: - `id` - `name` - `avatar` - `worksNum` - `platform` - `description` - `supportedArtistTabs` #### 歌手详情 新增 `getArtistWorks(artistItem, page, type)`: - 当 `type === "music"` 时,请求 `/mf/v1/artists/{artist_id}/tracks` - 其余类型返回空列表 #### 歌单详情入口识别榜单 增强 `getMusicSheetInfo(sheetItem, page)`: - 若 id 前缀是 `catalogsync:playlist:`,继续走 `/mf/v1/playlists/*` - 若 id 前缀是 `catalogsync:toplist:`,自动走 `/mf/v1/toplists/*` 这样榜单即使从“歌单搜索”结果页点入,也能正常打开详情。 ### 4.6 MusicFree 最小兼容改动 当前 MusicFree 的歌手详情页固定渲染 `music` 和 `album` 两个 tab。 本轮只对歌手详情页做最小改动: 1. 如果 `artistItem.supportedArtistTabs` 存在,则使用该数组作为当前歌手详情页 tab 集合。 2. 否则继续回退到默认 `["music", "album"]`。 3. 当 tab 只有一个时,不渲染 tab 栏,直接展示对应列表。 预期效果: - `Music_Server` 歌手详情页直接显示“全部歌曲”。 - 其它插件继续维持原本的“单曲 / 专辑”体验,不受影响。 ## 5. 数据流 目标数据流如下: 1. `catalog-sync` 持续维护源库中的 `artists`、`artist_songs`、`songs`、`playlist_songs`、文件位置等数据。 2. `Music_Server` 执行导出脚本,把源库转换成包含歌手读模型的 `catalog_read.db`。 3. `Music_Server` 通过 `CatalogReader` 对三类搜索与详情提供只读查询。 4. `Music_Server` 插件按 `music / artist / sheet` 三类请求分发到对应接口。 5. MusicFree 搜索页展示三类结果。 6. 用户点击歌手结果后,插件只拉取该歌手的歌曲列表;点击歌单结果时,普通歌单和榜单都能按各自详情接口打开。 ## 6. 错误处理与兼容性 ### 6.1 服务端 - 空查询直接返回空列表。 - 不存在的歌手、歌单、榜单返回 `404`。 - 非法 id 返回 `400` 或 `404`,与现有公开 id 处理风格保持一致。 - 歌手详情和歌曲列表都只面向只读库中已导出的歌手 id,不做运行时兜底推断。 ### 6.2 插件 - `artist` 类型的非 `music` 详情请求直接返回空结果,不抛异常。 - 歌单详情里若识别到榜单 id,则自动转发到榜单详情,不暴露给上层页面额外判断。 - 若服务端返回空列表,插件返回 `isEnd: true` 的空结果,保证 MusicFree 页面可正常结束加载。 ### 6.3 客户端 - 对不带 `supportedArtistTabs` 的旧插件保持原行为。 - 对 `Music_Server` 插件,只有一个 tab 时隐藏 tab 栏,不影响列表分页和批量操作。 ## 7. 测试策略 ### 7.1 `Music_Server` 需要覆盖以下测试层次: 1. 导出脚本测试 - 验证 `artists` / `artist_songs` 被正确导出到 `catalog_artists` / `catalog_artist_tracks` - 验证没有可播歌曲的歌手不会被导出 - 验证歌单搜索联合结果可覆盖普通歌单和榜单 2. `CatalogReader` 测试 - `search_artists(...)` - `get_artist(...)` - `list_artist_tracks(...)` - `search_sheets(...)` - 排序、分页、空查询和边界条件 3. 路由测试 - `/mf/v1/search/artists` - `/mf/v1/artists/{artist_id}` - `/mf/v1/artists/{artist_id}/tracks` - `/mf/v1/search/sheets` - token 鉴权兼容现有行为 ### 7.2 插件与客户端 至少覆盖以下验证: 1. 插件搜索分发 - `music` - `artist` - `sheet` 2. 插件详情分发 - 歌手详情只拉歌曲 - 搜索结果中的榜单通过 `getMusicSheetInfo(...)` 正确落到榜单详情接口 3. MusicFree 页面行为 - `Music_Server` 歌手详情只显示歌曲列表 - 其它插件歌手详情仍保留 `单曲 / 专辑` ## 8. 实施范围 预计涉及以下文件: ### `Music_Server` - `scripts/export_catalog_read.py` - `src/music_server/services/catalog_reader.py` - `src/music_server/routes/mf_catalog.py` - `src/music_server/plugin_assets/music_server.js` - `src/music_server/plugin_assets/music_server_lan.js` - `tests/test_export_catalog_read.py` - `tests/test_catalog_reader.py` - `tests/test_mf_catalog_routes.py` - `tests/test_plugin_routes.py`(如需校验插件导出字段) ### `MusicFree` - `src/pages/artistDetail/components/body.tsx` - 可能附带 `src/pages/artistDetail/store/atoms.ts` - 可能附带 `src/pages/artistDetail/components/resultList.tsx` 原则: - 只为支持单 tab 歌手详情做最小必要改动。 - 不改动全局搜索框架和其它插件约定。 ## 9. 验收标准 满足以下条件即视为完成: 1. MusicFree 中 `Music_Server` 插件搜索页可切换并返回 `单曲 / 歌手 / 歌单` 三类结果。 2. `歌手` 结果按平台分别显示。 3. 点进歌手后直接看到该歌手的全部可播歌曲,并可正常分页与播放。 4. 歌手详情页不再展示专辑 tab。 5. `歌单` 搜索可同时返回普通歌单和榜单。 6. 从搜索结果点进榜单时,详情页和歌曲播放都正常。 7. 其它插件的歌手详情行为不发生变化。