总览
首页不再承担所有事情。
现在的首页同时放推荐歌单、排行榜、歌单分类和热门歌曲,导致“发现”这件事没有单一入口。新结构把首页拆成两个明确任务:榜单回答“现在什么在流行”,歌单回答“我想听一组什么歌”。音乐库升级为“我的”,成为收藏与自建歌单的家。
“首页”不是改名,而是被拆掉了。平台排行榜归入榜单,推荐歌单与分类归入歌单,热门歌曲放在榜单下部。这样每个入口只有一个主动作,平台切换也不再同时代表“浏览哪个平台”和“用什么播放”。
四条会贯穿全套界面的规则
榜单和歌单顶部的平台选择只决定内容从哪里读;播放仍按设置里的聚合策略自动选源。页面上用“榜单来源”“歌单来源”说清楚,播放器里再显示“当前播放源”。
新建、改名、加歌、删歌先落本机 Room,界面立刻反馈;同步在后台完成。断网时仍是完整可用的音乐库,而不是一个空壳。
平台歌单、排行榜、专辑和自建歌单共用同一套歌曲列表与批量操作。换源、收藏、下载、加入歌单的位置一致,不为每种来源复制一遍页面。
榜单单平台失败时提示换来源;同步冲突时给出保留哪一版;更新签名不一致时说明无法覆盖安装。所有错误都避免只说“失败”。
哪些是旧能力重组,哪些是新增能力
| 入口 | 直接复用的现有能力 | 本次新增或重做 |
|---|---|---|
| 榜单 | topLists、topListSongs、hotSongs、聚合换源 | 独立榜单页、统一榜单详情、全榜播放、来源与播放分离的固定文案 |
| 歌单 | recommendedPlaylists、categories、categoryPlaylists | 独立推荐与分类视图、平台持久化、分类分页状态保留 |
| 搜索 | AggregateSearchEngine、人工合并/拆分、试听与 VIP 状态 | 搜索落地页、历史记录、链接自动识别、音源选择底部面板、结果状态保留 |
| 我的 | 收藏、最近播放、下载、自建歌单 | 统一音乐库结构、自建歌单编辑、Gist / Gitee 同步、冲突处理 |
| 设置 | 主题、聚合、账号、缓存 | 索引式设置、独立子页、更新通道、关于页 |
| 均衡器 | 现有 Media3 播放会话 | 五段实时均衡、内置与自定义预设、按设备音效能力降级 |
| 应用更新 | GitHub Actions Release 产物 | 更新清单、下载校验、签名校验、系统安装器、更新状态 UI |
可点击原型
先把入口点一遍。
下面的手机可以直接操作。底栏切换五个页面;榜单卡、歌单、搜索历史、同步图标、冲突提示、设置中的“关于与更新”都能继续往下走。原型里的数据和版本号是示意,不指向真实发布。
发现
榜单
更多榜单
平台热歌
网易云音乐浏览
歌单
今天适合听
换一批华语流行
查看分类跨平台
搜索
最近搜索
只保留关键词,不上传。
按类型搜索
结果会保留其他版本
不会自动合并本地优先
我的
最近喜欢
126 首最近播放
本机记录下载
24 首 / 186 MB偏好
设置
外观
播放
数据
其他
全部界面
每一个页面,
都先画出来再实现。
下面是完整界面图库。五栏主页面、所有内容详情、搜索与音源、音乐库四个分段、自建歌单编辑、同步连接与冲突、全部设置子页、播放器、歌词、队列、均衡器,以及平板和电视布局,都按同一套视觉规则单独绘制。
从榜单和编排进入音乐
发现
榜单
现在上升最快的歌
100 首 / 网易云音乐
更多榜单
全部飙升榜
按播放涨幅计算,每日更新
浏览
歌单
夜行车载:把城市调成静音
编辑推荐 / 38 首 / 网易云音乐
今天适合听
换一批浏览
歌单
夜行车载:把城市调成静音
编辑推荐 / 38 首 / 更新于今天
先接住问题,再把平台差异摊开
跨平台
搜索
聚合搜索:网易云、QQ、酷狗、酷我、咪咕
最近搜索
只保留关键词,不上传。
按类型搜索
结果会保留跨平台
搜索
其他版本
不会自动合并跨平台
搜索
晴天
周杰伦 / 叶惠美 / 04:29
换源是在目标平台重新检索同一录音,不会转移会员权益。找不到音源时,可以检查“其他版本”或调整匹配严格度。
跨平台
搜索
试试歌曲名加歌手,或换一个关键词。酷狗和酷我这次没有返回结果,可以单独重试。
本地音乐库、歌单编辑与云端副本
本地优先
我的
最近喜欢
126 首本地优先
我的
本地记录
我的
今天
昨天
本地文件
我的
夜间驾驶
32 首 / 49 分钟前编辑
本地优先
我的
音乐库同步
只同步你自建的歌单和歌曲信息
mymusic-library.json / 2 分钟前同步 / 自动同步已开启
同步到
Cookie、收藏、最近播放、下载文件和播放地址都不会进入同步文件。可在设置中关闭自动同步或限制 Wi-Fi。
把歌单放在哪里?
同步文件只包含自建歌单和歌曲元数据,不包含令牌、Cookie 或下载文件。
GitHub 私人令牌
需要 gist 权限。若使用已有 Gist,下一步可以粘贴链接;没有 Gist 时会为你新建一个私人 Gist。
或者不使用账号,导入 / 导出本地 JSON
mymusic-library.json / 8 个歌单
处理同步冲突
旧唱片 / 两个设备都改过这个歌单
上次同步后,两边都改动了曲目。
“合并两版”保留本机歌曲顺序,并把云端新增的歌曲补在后面,按来源与 ID 去重;删除项不会在合并时自动恢复。
每个子页只回答一个问题
偏好
设置
外观
播放与浏览
数据
其他
夜行车载
王宇宙Leto / 乔浚丞
主题
Android 12 以上跟随系统取色;Android 6–11 使用内置的深松绿 / 青瓷绿配色。电视端不使用触摸预览。
换源模式
自动:完整音源优先,其次按下面的平台顺序与码率选择。
优先平台
严格匹配会降低 Live、伴奏和重制版被误合并的概率。自动换源与手动换源都使用这里的规则。
恢复自动匹配参与聚合搜索的平台
默认浏览来源
只影响榜单、歌单等浏览页的初始来源,不会改变播放时的自动换源策略。
账号 Cookie 仅保存在本机,换源与 VIP 检测会使用它。
登录状态在下次启动时继续有效。
部分 VIP 曲目需要登录后才能获取播放地址。
清理建议
mymusic-library.json / 上次同步 2 分钟前
断开后本机歌单和非同步数据保留;远端文件不会被删除。
myMusic
版本 1.0.0.128 / versionCode 128
AGPL-3.0学习项目,请遵守各音乐平台服务条款。
myMusic
版本 1.0.0.128
新版本可用
1.0.0.129 / 18.4 MB / 2026-10-20
- 自建歌单可以同步到 Gitee 代码片段。
- 修复换源播放中断后进度没有恢复的问题。
- 电视端榜单焦点顺序调整。
Android 不允许静默安装。完成后会打开系统安装器,由你确认覆盖安装。
myMusic 的应用代码与移植的平台层均以 AGPL-3.0 发布。
查看完整文本上游与第三方
平台能力来源。当前纳入 commit 7cc4467,保留 Go 源码用于移植参考与许可证说明。
使用 OkHttp、原生加密工具与 Kotlin 实现,不引入 GoMobile、AAR 或额外后端。
均按其 Apache-2.0 或对应开源许可使用;完整列表随应用和仓库提供。
如果二次分发,请保留许可证、版权声明与修改说明,并遵守 AGPL-3.0 的源码提供义务。
Cookie、令牌、收藏、最近播放、下载文件、播放进度和匹配缓存都只保存在设备上。Cookie 与同步令牌使用 Android Keystore 加密。
启用同步后,只把自建歌单和歌曲元数据写入你的 GitHub Gist 或 Gitee 代码片段。同步文件不包含 Cookie、令牌、播放地址和下载文件。
应用直接请求音乐平台上游接口、更新清单和同步服务,没有 myMusic 自建中转服务器。第三方仍会看到其服务所需的常规请求信息。
复制诊断只包含版本、设备型号、Android 版本、启用平台和最近错误;不包含 Cookie、令牌或完整播放 URL。
退出平台账号或断开同步会清除对应的本机凭据;卸载应用会删除应用目录内的所有本地数据。
播放器、歌词、队列、均衡器与大屏布局
若月亮没来
王宇宙Leto / 乔浚丞
若月亮没来
王宇宙Leto / 乔浚丞
播放队列
32 首 / 当前第 4 首 / 来自“夜间驾驶”
队列沿用当前播放策略。某一首换源失败时保留原进度,并提示当前实际播放平台。
预设
榜单
现在上升最快的歌
100 首 / 网易云音乐
正在播放
页面设计 / 榜单
榜单只负责发现,
播放交给聚合层。
榜单是这次改版的新一级入口。平台排行榜本身不做跨平台合并,因为各平台的榜单口径不同,强行合成会产生一个没有意义的数字。跨平台只发生在“播放一首歌时重新找源”。
页面结构
榜单 ├─ 顶部来源选择 网易云 / QQ / 酷狗… ├─ 主推榜单 趋势信息 + 前 3 首 ├─ 更多榜单 该平台可用的榜单列表 ├─ 平台热歌 当前来源的热门单曲 └─ 迷你播放器 + 五栏底导 进入榜单 ├─ 封面、名称、更新频率、曲目数 ├─ 播放全部 / 换源设置 └─ 排名列表 ├─ 点击歌曲:播放并打开播放器 └─ 更多:换源、其他版本、合并/拆分、收藏、下载
为什么这样分
原首页的“排行榜”只是推荐位;榜单作为主入口后,需要承载来源切换、榜单列表、热歌和详情页。与其把四种模块塞进首页,不如让榜单成为可反复浏览的空间。
不新增“综合热榜”。上游没有统一的加权公式,平台数据也不具备可比性。页面只展示真实的平台榜单;如果未来有可靠的数据源,再单独设计。
记住上次浏览的榜单来源,但默认仍使用设置中的默认平台;首次进入时与设置一致。切换来源只刷新本页,不改变播放设置。
交互与状态
| 场景 | 界面行为 | 实现注意 |
|---|---|---|
| 切换榜单来源 | 顶部胶囊立即更新;主推榜单和列表显示骨架屏,失败时保留来源选择 | 取消旧请求;用 generation 防止旧来源回包覆盖新来源 |
| 加载榜单详情 | 显示封面和标题骨架;首批歌曲到达前不显示“暂无歌曲” | 缓存 `provider + chartId`;与通用歌单详情共用路由 |
| 播放全部 | 建立 canonical 队列,按现有聚合策略逐首解析;不预先解析整张榜单 | 避免 N 次封面/音频解析卡住界面;播放失败时继续到下一首并显示提示 |
| 平台没有榜单 | 隐藏主推区,显示“这个平台没有提供排行榜”与“换来源” | 根据 `ProviderInfo.topLists` 能力判断,不发空请求 |
| 部分歌曲不可播 | 歌曲行展示完整 / 试听 / VIP / 待检测标签,播放时按策略回退 | 沿用 `availabilityOf` 与 `SourceResolver`,不在 UI 层自行判断 |
实现映射
MusicRepository.topLists topListSongs hotSongs
sourceProvider topLists featuredChartId chartSongs loading / error
MediaList TrackRow SourcePickerSheet MiniPlayer
页面设计 / 歌单
推荐与分类,
在同一个浏览上下文里。
歌单页把原首页的“推荐歌单”和“歌单分类”合并。推荐负责低决策成本的进入,分类负责有目的的寻找;两者共用同一个平台来源和详情页,不产生两套卡片样式。
结构
歌单 ├─ 来源:网易云 / QQ / 酷狗 / 酷我… ├─ 推荐 | 分类 │ ├─ 推荐 │ ├─ 主推歌单:大封面 + 标题 + 曲目数 │ ├─ 横向推荐行 │ └─ 分类快捷入口 │ └─ 分类 ├─ 分类 chip 组(可折叠) ├─ 两列封面网格 └─ 上一页 / 下一页 详情 ├─ 封面、标题、来源、曲目数 ├─ 播放全部 / 收藏整张歌单 └─ 与榜单详情相同的歌曲列表
刻意不做的事
不在歌单页做“聚合歌单内容”。同名歌单只是名字相同,曲目、顺序和版本都可能不同。搜索页可以把多个平台的同名歌单并列展示;歌单页本身只读取一个平台。
上游没有实现分类的平台直接隐藏“分类”段控件,保留推荐。不要用“暂无分类”占一屏。
分类翻页后在列表底部展示“上一页 / 第 N 页 / 下一页”,不跳出当前歌单页。返回时恢复平台、分类和页码。
交互与状态
| 用户动作 | 结果 | 边界处理 |
|---|---|---|
| 切换歌单来源 | 来源芯片变化;推荐和分类一起刷新 | 只发该平台支持的请求;旧结果全部丢弃 |
| 切换推荐 / 分类 | 保持页面滚动位置独立记忆 | 没有分类能力时隐藏该入口 |
| 展开分类 | 分类 chip 行变多,按钮“收起” | 页面不跳转,未选中的分类不变 |
| 翻页 | 显示加载状态,底部追加或替换当前页 | 返回第一页或空页时禁用按钮;失败保留原页 |
| 点歌单 | 进入统一详情页,展示歌曲列表 | 详情加载失败仍保留标题和来源,可重试 |
统一详情页的好处:平台歌单、排行榜、专辑和自建歌单都使用同一套歌曲行动作。以后新增“按顺序播放”“批量下载”或“添加到我的歌单”,只需要改一处。
页面设计 / 搜索
结果要聚合,
差异要展开。
搜索默认跨平台,但不再把每个平台的按钮都铺在结果卡上。主结果只显示同一录音合并后的信息和整体可播状态;平台差异、其他版本和人工纠错放进音源面板。
从输入到播放
歌曲、歌单、专辑,或平台分享链接。只有提交才请求。
普通关键词走聚合搜索;链接识别到平台后走解析。
单曲按录音合并;专辑保守分组;歌单只并列同名来源。
音源面板选择平台,或按设置自动换源。
搜索落地页
最多 20 条,保存在本机 DataStore;可单条删除或全部清除,不随同步上传。
明确显示当前参与聚合的平台,点击可跳到设置;不把“当前平台”藏在二级筛选里。
检测到 URL 时在输入框内显示“打开链接”按钮;不再单独提供“链接”类型标签。
结果与音源面板
| 区域 | 展示内容 | 交互 |
|---|---|---|
| 平台状态行 | 每个已启用平台的返回状态、结果数、超时/错误 | 部分失败不影响已到达结果;整行可点击查看失败原因 |
| 主结果卡 | 封面、歌名、艺人/专辑、时长、可用标签、平台数 | 点击打开音源面板,不直接播放,避免误触和错误版本 |
| 音源面板 | 各平台完整/试听/VIP/待检测、音质,以及其他版本 | 单选一个平台后播放;“记住这个选择”写入本曲偏好 |
| 其他版本 | Live、Remix、伴奏等未自动合并的候选 | 可播放,也可“合并到…”当前曲目;拆分操作放在面板更多菜单 |
| 结果状态 | 加载、部分到达、空结果、全部失败 | 空结果给出搜索建议;超时明确列出平台 |
结果保留:切换底部页签不能丢掉查询和结果。用户可以搜到一半去“我的”创建歌单,再回到搜索结果继续。搜索请求应放在稳定的状态持有者里,而不是 Composable 的临时状态。
实现映射
继续使用 AggregateSearchEngine、TrackMatcher 和 SourceResolver,不改匹配规则。
新增 SearchHistoryStore、查询 / 类型 / 结果 / 平台状态 / 滚动位置;底部面板统一由 SourcePickerSheet 承载。
链接解析后的歌单、专辑进入统一详情;单曲直接打开音源面板或播放器。
页面设计 / 我的
先把音乐库做成
可以带走的东西。
“我的”承接喜欢、自建歌单、最近播放和下载。重点不是换一个名字,而是把自建歌单升级成真正属于用户的数据:可以编辑、可以导出、可以同步到 GitHub Gist 或 Gitee,并且在本机永远先可用。
页面结构
我的 ├─ 顶部:云同步入口(状态点) ├─ 统计条:喜欢 / 自建歌单 / 已下载 ├─ 段控件:喜欢 | 歌单 | 最近 | 下载 │ ├─ 歌单 │ ├─ 同步状态横幅 │ ├─ 歌单列表(已同步 / 待同步 / 冲突) │ └─ 新建歌单 │ ├─ 喜欢 / 最近 │ └─ 歌曲行,支持播放与加入歌单 │ └─ 下载 └─ 完成 / 下载中的本地文件 自建歌单详情 ├─ 播放全部 / 添加歌曲 ├─ 同步状态 ├─ 排序与移除 └─ 更多:改名、删除、导出
为什么默认停在“歌单”
“喜欢”是一组自动形成的歌,“自建歌单”是用户主动维护、最需要同步和编辑能力的部分。作为本次改版的重点,进入“我的”默认展示歌单,其余段按用户上次选择记忆。
这里的“我的”不是登录后的个人主页。应用没有自己的账号,只展示本机音乐库和第三方同步连接状态。
下载文件体积大且受版权限制;收藏与最近播放属于个人行为数据。V1 只同步自建歌单,避免把隐私和文件写入云端。
自建歌单的编辑交互
| 动作 | 触摸 / 鼠标 | 电视遥控器 |
|---|---|---|
| 新建歌单 | 底部“新建歌单”,弹出名称输入框 | 同样入口,输入使用系统键盘 |
| 添加歌曲 | 曲目行更多菜单 →“添加到歌单”;或歌单详情“添加歌曲”进入多选 | 菜单项获得焦点,确认后进入多选 |
| 调整顺序 | “编辑排序”后拖拽;也可用上下移动按钮 | 不使用拖拽,选中歌曲后按上下键移动 |
| 移除歌曲 | 曲目行更多菜单 →“从歌单移除” | 相同菜单,不使用滑动删除 |
| 改名 / 删除歌单 | 详情右上角更多菜单 | 相同菜单,删除二次确认 |
| 同步状态 | 歌单行右侧显示已同步 / 待同步 / 有冲突 | 状态同时有文字,不只靠颜色和圆点 |
立即反馈:所有编辑先写本机数据库,列表和歌单详情立即更新。同步失败只在同步入口显示状态,不用弹窗打断编辑;手动点“立即同步”时才展示详细错误。
页面设计 / 设置
设置是索引,
不是一个无限长的表单。
现有设置把所有内容堆在一个 LazyColumn 里,平台账号、聚合、缓存和 Cookie 混在一起。新设置按领域拆成独立子页,每个子页只回答一个问题;均衡器是设置索引里的独立入口。关于与更新也在这里有了稳定的入口。
设置索引
设置 ├─ 外观 │ └─ 主题 / 动态配色 / 减少动画 ├─ 播放与换源 │ └─ 换源模式 / 优先平台 / 试听 / 匹配严格度 ├─ 均衡器 │ └─ 开关 / 预设 / 频段 / 自定义曲线 ├─ 搜索与浏览 │ └─ 聚合平台 / 搜索范围 / 默认浏览来源 ├─ 平台账号 │ └─ Cookie / 扫码登录 / 退出 ├─ 音乐库同步 │ └─ 同步账户 / 自动同步 / Wi-Fi / 冲突 ├─ 存储 │ └─ 播放缓存 / 匹配缓存 / 下载 └─ 关于与更新 ├─ 版本、构建号、更新检查 ├─ 开源许可、项目主页 └─ 隐私说明、诊断信息
交互约定
设置项即改即存,没有底部“保存”按钮。令牌、Cookie 等敏感输入离开页面即清空明文,只保留加密存储。危险操作(清空缓存、断开同步、退出账号)都需要二次确认,并说明会影响什么。
宽度足够时变成左侧索引、右侧子页;电视仍使用导航栏 + 子页,不依赖触摸。所有开关和按钮获得明确焦点框。
只放版本、更新、许可、隐私和诊断。播放参数留在各自的设置子页。
子页边界
| 子页 | 包含 | 不包含 |
|---|---|---|
| 播放与换源 | 自动 / 优先 / 固定平台,优先顺序,是否允许试听,严格匹配,恢复自动匹配 | 登录、缓存、主题、均衡器 |
| 均衡器 | 开关、预设、设备实际频段、自定义曲线、重置 | 换源策略、登录、缓存 |
| 搜索与浏览 | 聚合平台多选,默认浏览来源,搜索范围 | 播放源排序(放播放与换源) |
| 平台账号 | 当前平台选择、Cookie、扫码登录、退出 | 自建歌单同步令牌 |
| 音乐库同步 | 同步后端、账号、文件、自动同步、Wi-Fi、冲突 | 平台 Cookie、下载管理 |
| 存储 | 播放缓存、匹配缓存、下载文件、占用统计 | 同步设置 |
| 关于与更新 | 版本、更新、许可、隐私、项目链接、诊断 | 账号和播放参数 |
新增能力 / 均衡器
音效在本机完成,
设置跟着设备走。
均衡器作为“设置 → 均衡器”的独立页面。它直接作用于当前播放会话,不修改音频文件,也不参与歌单同步;关闭页面后效果继续,重启应用后恢复上次设置。
界面与交互
设置 ├─ 播放与换源 │ ├─ 换源模式 │ ├─ 优先平台 │ └─ 试听 / 严格匹配 └─ 均衡器 ├─ 开关与当前输出 ├─ 预设 └─ 频段 / 自定义 均衡器 ├─ 开关 + 当前输出状态 ├─ 预设:平直 / 流行 / 摇滚 / 爵士 / 人声 / 自定义 ├─ 频段滑杆(按设备实际频段绘制) │ ├─ 拖动立即生效 │ ├─ 数值以 dB 显示 │ └─ 手动改动后自动切到“自定义” ├─ 恢复平直 / 保存为自定义 └─ 兼容性说明与当前播放源
为什么不做成浮层
均衡器是多参数、需要反复试听的页面。把它塞进播放器浮层会遮挡封面,也不方便精细拖动。独立页面能容纳完整频段、预设和兼容性说明,并能在播放器继续播放的同时操作。
实时生效:没有“应用”按钮。预设和频段变化直接写入音频效果,页面只显示当前值;退出页面不会撤销。
交互状态
| 场景 | 界面行为 | 实现注意 |
|---|---|---|
| 打开页面 | 显示开关、当前预设、真实频段与增益;无播放会话时开关可用但标注“播放后生效” | 音效对象随播放会话创建;无会话时不盲目创建 Equalizer |
| 切换预设 | 所有滑杆在约 150 ms 内过渡到目标曲线,保留开关状态 | 预设由应用定义,按对数频率插值到设备实际频段;不依赖系统预设名称 |
| 拖动频段 | 数值与滑杆实时变化,预设自动变成“自定义”;防抖写 DataStore | 以 0.5 dB 或 1 dB 为步进写 setBandLevel;滑杆视觉轨道可细,触摸热区至少 44–48dp |
| 电视 / 键盘 | 左右切换频段,上下按 1 dB 调节;焦点框清晰可见,不使用拖动 | 为自定义滑杆补 DPAD / 方向键处理;实时读屏描述“60 Hz,+6 dB” |
| 切换歌曲 / 换源 | 效果不中断,页面数值不重置 | 如果 Media3 重建了 audio session,释放旧效果、用新 session 重建并重放设置 |
| 切换输出设备 | 显示“当前输出:蓝牙耳机 / 扬声器”;若效果未生效明确说明 | 部分蓝牙 / USB 路由不应用第三方音效,不能假装成功 |
| 设备不支持 | 开关禁用并显示原因:系统或输出设备不支持均衡器 | 捕获 UnsupportedOperationException / IllegalStateException,保留本机设置但不影响播放 |
| 下载播放 | 同样使用本地文件播放会话,效果可用 | 音效不写回下载文件;关闭后恢复原始声音 |
实现方式
Media3 getAudioSessionId() 提供当前 audio session。
用 session 创建 android.media.audiofx.Equalizer。
应用预设 dB 曲线到设备实际频段,写 band level。
DataStore 保存开关、预设和自定义曲线。
控制器职责
class EqualizerController(
private val store: EqualizerSettingsStore,
) {
fun attach(sessionId: Int) // 创建或重建 Equalizer
fun detach() // 播放器销毁 / session 失效
fun setEnabled(enabled: Boolean)
fun applyPreset(preset: EqPreset)
fun setBandLevel(band: Short, millibels: Short)
fun resetFlat()
fun saveAsCustom()
}
// Media3:Player.Listener.onAudioSessionIdChanged
// 与 Player.getAudioSessionId() 获取会话。
// ExoPlayer 的相关 API 标注为 UnstableApi,
// 需要在 PlaybackService 中显式 OptIn。
保存与生效
DataStore 只保存 enabled、presetId 和自定义频段增益(dB)。应用启动或 PlaybackService 重建时读取并重新应用,不把设置写进歌曲或歌单。
预设曲线由应用定义:平直全 0 dB;流行强调低频与高频;摇滚提升低频和 3 kHz 附近;爵士轻微提升低频与高频;人声压低 60–230 Hz 并抬高中频。设备支持的频段数不同,就按中心频率做对数插值,而不是硬套五段。
不承诺所有耳机都生效。效果使用 Android 系统音频效果框架,受输出路由、OEM 音频实现和省电策略影响。界面要明确显示“当前输出是否生效”,不能把设置成功当成实际听到。
数据与生命周期
| 项目 | 处理 |
|---|---|
| 音效对象 | 不持久化;每次有可用 audio session 时创建,session 变化或播放器销毁时 release() |
| 持久化 | 开关、预设、自定义频段写入 DataStore;不跨设备同步,因为不同设备的频段和音频能力不同 |
| 权限 | 不申请运行时权限;使用应用自己的播放会话。若某些 OEM 需要额外权限或能力,降级为不可用,不阻塞播放 |
| 后台播放 | 控制器随 PlaybackService 存活;切到后台、锁屏或通知栏播放时效果保持 |
| 失败恢复 | 创建结果用显式状态表达:Available / Unsupported / NoSession;失败时开关灰显,播放本身不受影响 |
| 不做 | 不引入 NDK / 自研 DSP,不改变音频文件,不做响度战争式自动增益,不把音效设置写入同步 JSON |
测试重点:在扬声器、有线耳机和至少一款蓝牙耳机上验证开关与频段;覆盖切歌、换源、切后台、播放结束、音源失败与蓝牙重连。设备不支持时必须显示“未生效”,而不是静默把滑块移来移去。
重点能力 / 音乐库同步
云端是副本,
本机才是主版本。
新增同步的目标只有一个:让用户自建的歌单可以在多台 Android 设备之间带走。实现上采用“本地数据库 + 单一版本化 JSON 快照 + 三方合并”,先支持 GitHub Gist 和 Gitee 代码片段,再扩展到其他后端。
V1 同步范围:仅自建歌单及其曲目元数据。不同步收藏、最近播放、下载文件、Cookie、播放地址、播放进度。这个边界既降低隐私风险,也避免把大文件和易失效 URL 写进 Gist。
后端适配
| 后端 | 接入方式 | 鉴权 / 存储 | 版本识别 | 优先级 |
|---|---|---|---|---|
| GitHub Gist | GET/POST/PATCH /gists,建议私人 Gist,文件名固定 | Fine-grained PAT 或 classic token 的 gist 权限 | history[0].version + 内容 SHA-256 | V1 |
| Gitee 代码片段 | /api/v5/gists,创建时设 public=false | Gitee 私人令牌,勾选代码片段权限;API 接受 access_token | updated_at + 内容 SHA-256 | V1 |
| GitLab Snippets | /api/v4/snippets,私有 snippet | Personal Access Token,api 权限 | updated_at + 内容 SHA-256 | P2 |
| WebDAV | GET / PUT 固定 JSON 路径 | 服务地址、用户名、应用密码 | ETag + If-Match | P2 |
| 文件导入导出 | 系统文件选择器 SAF,读写 JSON | 无账号 | 不适用 | V1 |
Gitee 与 GitHub 的 Gist 更新接口不提供强等价的 If-Match 语义。客户端采用“写前重读、版本未变才写、写后回读”的优化并发策略,并把无法确认的并发保留为下一次同步的冲突,而不是静默覆盖。
同步文件格式
{
"schema": "mymusic.library",
"version": 1,
"deviceId": "a7c2e6f1",
"updatedAt": "2026-10-21T02:19:41Z",
"playlists": [
{
"id": "b5e1d0e8-3a2f-4ff8-84b8-2f7da0f4c139",
"name": "夜间驾驶",
"createdAt": "2026-09-03T12:10:00Z",
"updatedAt": "2026-10-20T22:40:10Z",
"deletedAt": null,
"tracks": [
{
"key": "netease:186016",
"provider": "netease",
"id": "186016",
"title": "晴天",
"artist": "周杰伦",
"album": "叶惠美",
"durationSec": 269,
"cover": "https://example.invalid/cover.jpg"
}
]
}
]
}
歌单使用 UUID,设备间不依赖自增主键。曲目使用 provider:id 作为稳定键。
删除歌单写入 deletedAt,同步一轮后再清理,避免另一台设备把删除项重新创建。
只存元数据。单一文件超过 900 KB 时停止自动上传并提示导出,不在 Gist 中无界增长。
同步流程
Room 立即提交,标记 dirty;界面先完成。
WorkManager 延迟合并,网络可用时启动。
上次同步基线 / 本机 / 云端逐歌单比较。
无冲突则上传;有冲突则停止并显示冲突入口。
连接流程
选择 GitHub Gist 或 Gitee,填写令牌。界面提供最小权限说明和创建令牌的入口文案,但不把令牌写到日志。
读取当前用户信息,验证令牌至少能访问 Gist。失败区分“未授权”“权限不足”“网络不可达”。
默认新建私人 Gist / 代码片段,文件名为 mymusic-library.json;也可以粘贴已有 Gist ID 或链接。
分别显示本机与云端的歌单数、曲目数。两端都有内容时,让用户选择合并、保留本机或使用云端;先自动导出一份本机备份。
保存远端版本号与内容哈希作为下次三方合并的基线。默认开启自动同步,允许只走 Wi-Fi。
冲突策略
| 本机 / 云端相对上次基线 | 处理 | 用户看到什么 |
|---|---|---|
| 只有本机改变 | 直接上传本机版本 | 同步横幅显示“正在同步”,完成后显示时间 |
| 只有云端改变 | 直接拉取云端版本更新本机 | 歌单行短暂显示“正在更新”,不用弹窗 |
| 两边都未变 | 不写远端,刷新同步时间 | 只更新“刚刚检查”状态 |
| 两边都变,已有一方删除 | 冲突,绝不自动删除 | “本机删除了这个歌单,云端修改了它。选择保留或删除。” |
| 两边都变,内容不同 | 冲突,默认“合并两版” | 显示歌单名、两侧曲目数与修改时间;可选本机、云端或合并 |
| 远端 JSON 损坏或 schema 不支持 | 停止上传,保护本机 | “云端文件无法读取,本机数据没有改动。”提供重新选择文件或导出本机数据 |
数据与安全
本机模型
playlists
id TEXT PK (UUID)
name TEXT
createdAt / updatedAt
dirty INTEGER
deletedAt TEXT?
playlist_entries
playlistId TEXT FK
position INTEGER
songKey TEXT
payload TEXT
sync_accounts
provider TEXT PK
remoteId TEXT
fileName TEXT
lastSyncedAt INTEGER
remoteRevision TEXT
autoSync INTEGER
wifiOnly INTEGER
sync_base
accountId TEXT PK
snapshot TEXT
remoteRevision TEXT
sync_conflicts
playlistId TEXT PK
localSnapshot TEXT
remoteSnapshot TEXT
detectedAt INTEGER
凭据与迁移
令牌使用 Android Keystore 的 AES-GCM 密钥加密,存入独立的 SecretStore,不进入 Room,也不进入同步 JSON。应用备份规则排除 SecretStore;断开同步时删除令牌和基线,但保留本机歌单。
现有 LocalPlaylist(id: Long, songs: JSON) 迁移到 UUID 歌单和逐条曲目表:开新表、解析旧 JSON、复制曲目和顺序、最后切换读取路径。迁移必须在事务中完成,旧表保留一个版本周期再删除。
私人 Gist 不是加密存储。它只是不出现在公开列表里。不要同步 Cookie、令牌或任何下载地址;同步文件只放歌单元数据。
自动同步的触发与克制
| 触发 | 行为 |
|---|---|
| 歌单编辑 | 标记 dirty,30 秒防抖后调度一次后台同步;5 分钟内不重复跑同一账户 |
| App 回到前台 | 若距上次同步超过 15 分钟且网络满足条件,静默拉取一次 |
| 手动“立即同步” | 忽略节流,显示显式进度和结果;失败弹 Snackbar,并给出重试 |
| 无网络 / 仅移动网络且开启 Wi-Fi 限制 | 保持待同步,不弹错误;网络恢复后由 WorkManager 重试 |
| 令牌失效 | 暂停自动同步,显示“需要重新登录”;本机编辑继续可用 |
新增能力 / 更新与关于
更新是一个校验流程,
不是一条下载链接。
应用内更新放在“关于与更新”:显示当前版本与构建号,自动检查静默进行,手动检查才展示结果。下载完成后必须校验哈希、包名、版本号和签名,再交给 Android 系统安装器,绝不静默安装。
发布清单
CI 在 Release 中同时发布 APK、update.json 和 APK 的 SHA-256。客户端读取稳定版的更新清单;清单不可用时退回浏览器打开 Release 页面。
{
"schema": "mymusic.update",
"channel": "stable",
"versionCode": 129,
"versionName": "1.0.0.129",
"tag": "v1.0.0.129",
"publishedAt": "2026-10-20T08:00:00Z",
"minSdk": 23,
"apk": {
"url": "https://github.com/mytv-android/myMusic/releases/download/v1.0.0.129/myMusic-1.0.0.129.apk",
"size": 19293798,
"sha256": "8b0f...9c4e"
},
"notes": [
"自建歌单可以同步到 Gitee 代码片段。",
"修复换源播放中断后进度没有恢复的问题。",
"电视端榜单焦点顺序调整。"
]
}
当前 CI 已经把 VERSION_CODE 写入构建,Release 的版本号形如 1.0.0.{run_number}。发布清单直接复用构建产物里的 versionName / versionCode,避免手工维护不一致。
检查与安装流程
启动后最多每 24 小时一次,缓存 ETag;手动检查立即请求。
只比较 versionCode,版本名仅用于展示。
后台下载 APK,显示进度,可取消;只接受清单中的大小。
SHA-256、包名、versionCode、签名证书逐项比对。
FileProvider 打开系统安装器,用户确认覆盖安装。
自动检查不打扰
启动时如果发现稳定版更新,只在设置索引的“关于与更新”旁显示一个圆点,不弹窗。用户进入关于页才看到版本差异与更新说明。同一版本被“稍后”后,24 小时内不再提示。手动检查则无论是否有更新都给出明确结果。
只提供稳定通道。当前 main 发布稳定版,dev 为预发布;V1 不跟随预发布,避免用户被开发版打断。将来要加测试通道时,在清单里增加 channel 并由用户显式选择。
必须处理的失败
| 网络失败 | 显示“检查更新失败”,提供重试和浏览器打开;不缓存错误结果 |
| APK 哈希不符 | 删除文件,提示“更新包校验失败”,不安装 |
| 包名不符 | 视为错误清单,拒绝安装并记录诊断信息 |
| 版本号不高于当前 | 视为服务端清单异常,提示已是最新版本 |
| 签名与当前安装不一致 | 明确说明无法覆盖安装,建议从当前渠道重新下载;不能引导用户直接卸载而丢数据 |
| 未知来源权限未开启 | 跳转到本应用的“安装未知应用”设置页;返回后保留已下载文件 |
| 存储空间不足 | 在下载前检查清单大小,显示所需空间,不产生半截文件 |
关于页内容
| 区块 | 内容 | 动作 |
|---|---|---|
| 应用 | 图标、名称、版本名与 versionCode、AGPL-3.0 标识 | 无 |
| 更新 | 检查更新、可用版本、发布说明、下载大小 | 打开更新面板 |
| 许可与来源 | AGPL-3.0 完整许可、music-lib 上游 commit、第三方依赖清单 | 查看文本或项目主页 |
| 隐私 | Cookie 和播放记录仅存本机;同步只写歌单元数据;没有集中式服务器 | 查看详细说明 |
| 诊断 | 版本、设备型号、Android 版本、启用的平台、最近一次同步/更新错误 | 复制,明确不含 Cookie 和令牌 |
| 链接 | 项目主页、问题反馈 | 浏览器打开 |
Android 集成点
REQUEST_INSTALL_PACKAGES 用于发起安装;Android 8+ 仍需用户在系统设置中允许“安装未知应用”。
使用已有的 androidx.core 提供的 FileProvider,在 Manifest 中声明 cache/updates 路径。
用 PackageManager.getPackageArchiveInfo 读取 APK 的包名、versionCode、签名证书,与已安装应用比较;Android 6–8 使用旧签名 API 分支。
在 Release job 中根据 output-metadata.json 生成 update.json 和 SHA-256,上传为 Release assets;只在成功完成测试后发布。
实现方案
先拆页面,
再拆状态。
现有 AppUi.kt 约一千行、MusicViewModel.kt 约一千四百行,首页、搜索、音乐库和设置都写在一起。建议分两步,而不是一次性重写:先把 UI 按新入口拆开,复用现有数据与播放逻辑;再把同步和更新做成独立的数据层与状态持有者。
目标结构
根导航五个入口;详情页使用 NavHost 路由。播放器保持全局浮层,不属于任何一个页签,切换页面不会中断。
ChartsScreen PlaylistsScreen SearchScreen LibraryScreen SettingsScreen ChartDetail / PlaylistDetail / LocalPlaylistDetail SyncCenter AboutScreen UpdateSheet
MediaList TrackRow SourcePickerSheet PlayerBar EmptyState ErrorState FocusableCard
保留全局播放与平台状态;拆出 SearchUiState、LibraryUiState、SyncUiState、UpdateUiState。不要继续把所有字段塞进一个 MusicUiState。
MusicRepository SettingsStore Room v4 EqualizerController SyncCoordinator PlaylistSyncProvider SecretStore UpdateRepository ApkVerifier
涉及的文件
| 路径 | 动作 | 内容 |
|---|---|---|
| ui/AppNavigation.kt | 新增 | 五栏根导航、详情路由、播放器浮层、返回栈 |
| ui/AppScaffold.kt | 新增 | 手机底栏 / 平板电视导航栏,迷你播放器,Snackbar 宿主 |
| ui/AppUi.kt | 拆分 | 保留共享 UI;主页、音乐库、设置拆到各自目录 |
| ui/charts/ | 新增 | 榜单列表、榜单详情、来源切换 |
| ui/playlists/ | 新增 | 推荐 / 分类浏览、平台歌单详情 |
| ui/search/ | 新增 | 搜索落地、结果列表、搜索历史 |
| ui/library/ | 新增 | 我的、自建歌单编辑、同步中心、冲突处理 |
| ui/settings/ | 新增 | 设置索引、播放与换源、均衡器、账号、存储、关于页、更新面板 |
| playback/EqualizerController.kt | 新增 | 绑定 Media3 audio session,创建 / 重建 Equalizer,应用预设与频段并处理不支持状态 |
| playback/PlaybackService.kt | 修改 | 在创建 / 重建播放器时把 audio session 交给 EqualizerController;服务销毁时释放效果 |
| data/sync/ | 新增 | PlaylistSyncProvider、Gist / Gitee 适配、合并引擎、WorkManager worker、SecretStore |
| data/update/ | 新增 | 更新清单解析、下载、哈希/签名校验、安装 Intent |
| data/AppStore.kt | 修改 | Room v4、歌单条目、同步账户与基线;DataStore 增加均衡器开关、预设和自定义频段;保留收藏 / 历史 / 下载表 |
| ui/MusicViewModel.kt | 提取 | 播放与聚合逻辑剥离为稳定内核;搜索、音乐库、同步各自状态 |
| .github/workflows/build.yml | 修改 | 发布 APK 时生成 update.json 与 SHA-256 |
| app/src/main/AndroidManifest.xml | 修改 | 安装包权限、FileProvider、密钥备份排除规则 |
| gradle/libs.versions.toml | 修改 | 加入 WorkManager;同步网络任务与重试 |
状态边界
保留为全局
这些状态会跨页签存在,且播放不能被搜索或音乐库页面的生命周期打断。播放器与 MediaController 仍由全局持有者管理。
下沉为页面状态
每个页面只持有自己需要的状态。切换页签时保留,离开应用后不要求恢复草稿;搜索历史、歌单来源等需要持久化的内容写入 DataStore。
分阶段实施
五个根入口、三类详情、全局播放器浮层;把现有首页模块移到榜单与歌单。先保持数据逻辑不变,确保没有功能倒退。
搜索落地页、历史、链接识别、音源底部面板;榜单、歌单、专辑接入同一套媒体列表与操作。
自建歌单改为 UUID + 曲目条目;加入排序、移除、添加到歌单;完成旧数据迁移与回归测试。
PlaylistSyncProvider 抽象、GitHub Gist、文件导入导出、三方合并、冲突 UI 和 WorkManager。先在本地文件后端上跑通合并测试。
接入 Gitee 代码片段,验证令牌权限、私人片段和更新语义;GitLab / WebDAV 作为适配器扩展,不阻塞首发。
Release 更新清单、下载校验、签名比对、系统安装器、关于页、许可与诊断;补齐 TV 焦点与失败状态。
接通播放器 audio session,加入五段 / 设备实际频段均衡、预设、自定义保存、输出设备状态和能力降级。先用真机验证系统是否应用效果,再发布。
测试重点
覆盖本机新增、远端新增、删除墓碑、双改冲突、远端损坏、同曲目不同顺序等场景。合并函数不依赖 Android,先用纯 JVM 测试。
伪造哈希不一致、错误包名、低版本号、签名不一致,确保都在打开系统安装器前被拒绝。
切换五个页签、打开详情、播放中断换源、下载优先本地文件,播放状态不能被页面销毁或搜索取消影响。
至少覆盖扬声器、有线耳机、一款蓝牙耳机与一台不支持音效的设备;测试重建 audio session 后设置是否恢复,以及切换输出时是否如实显示生效状态。
遥控器能完成搜索、换源、打开同步、处理冲突、调均衡器和检查更新;状态不只靠颜色表达,焦点框在全套组件中一致。
验收与边界
做到这些,
这次重设计才算完成。
验收清单
- 导航:底栏只有榜单、歌单、搜索、我的、设置五个主入口;首页内容没有丢失,分别落在榜单和歌单。
- 来源:榜单 / 歌单页明确标注内容来源;播放源在播放器和音源面板中单独显示,两个概念不混用。
- 搜索:提交才请求;切换页签后查询与结果保留;音源、试听、VIP、其他版本都能在两层内完成选择。
- 音乐库:自建歌单可新建、改名、加歌、排序、删除;编辑先落本机,断网可用。
- 同步:GitHub Gist 与 Gitee 至少各跑通一次创建、上传、拉取和冲突;令牌不以明文出现在数据库或日志。
- 冲突:两个设备修改同一歌单时不会被静默覆盖;用户能选择合并、本机或云端,且操作前有本机备份。
- 均衡器:开关、预设、自定义频段真实作用于当前播放;重建 audio session 或切歌后保持;不支持时明确显示“未生效”,不阻塞播放。触摸、键盘和遥控器都能调节频段。
- 更新:能够检查版本、下载、校验 SHA-256 与签名、打开系统安装器;签名不一致时给出明确解释。
- 关于:版本、构建号、许可、隐私说明、项目链接和诊断信息复制都可用。
- 设备:手机上单手可完成主流程;平板和电视使用侧边导航;所有操作可仅用遥控器完成。
这次明确不做
没有可信的统一排名口径,不编造跨平台名次。只展示各平台真实榜单。
V1 只同步自建歌单,避免把行为数据和个人偏好写进第三方 Gist。
不调用各平台的写接口,不产生账号间数据不一致。自建歌单只存在于本机与用户选择的同步后端。
Android 需要用户确认安装。应用负责下载和校验,系统安装器负责最后一步。
同名不代表同内容。只做并列展示,不做隐式合并。
不自建账号、不代理播放地址、不收集 Cookie。同步直接发生在设备和用户自己的 Gist / Gitee 之间。
下一步建议:先按阶段一完成五个入口和共享详情页,拿着这版 HTML 对照真机确认导航与焦点;然后单独评审同步协议和冲突文案,再接入 Gist。这样不会让“同步”这个新能力把整次改版拖成一次大爆炸式重构。