lxh2875931338
|
dbbf215ba8
|
style(ui): 主页列表尺寸按设计稿收紧
- 清听 28->24、媒体库 22->20、我的收藏标题 22->20
- 本地音乐/最近播放按钮文字 14->13
- 收藏项:封面 44->32、标题 16->13、副标题 13->11,
padding 2->0,minVerticalPadding 0
- 行高约 90dp -> 71dp,更接近设计稿的密度
|
2026-11-01 15:06:57 +08:00 |
|
lxh2875931338
|
7d701c5dfc
|
feat(search): WebDAV 文件夹内搜索
- 列表页右上角「全部播放」换成搜索(刷新保留)
- 新页面 WebDAVSearchPage:输入关键字实时过滤当前文件夹的歌曲
- 匹配范围:文件名、元数据标题、艺术家(大小写不敏感)
- 结果显示封面/标题/艺术家,点击从该首开始播放并返回列表
- 空状态分两种:未输入提示、无结果提示
真机验证:搜索 "forza" 命中 2 首,一条显示标签标题、一条显示文件名。
|
2026-11-01 14:55:58 +08:00 |
|
lxh2875931338
|
83ab47d8fc
|
fix(metadata): 补上事件订阅,缓存命中也推送一次
上一条只加了字段和回调,listen 没插进去(列表不会刷新)。
另外歌曲元数据在 SQLite 命中时直接 return,也不会发事件——
导致"标签其实早就在库里、列表却还显示文件名"。
- HomePage / WebDAVFileListPage 补上 stream.listen 与 cancel
- MetadataService 的 SQLite 命中分支也 publish 一次(幂等,按 path 覆盖)
验证方式:播放一首收藏里显示文件名的歌 -> 返回主页 -> 该行应变为标签标题。
|
2026-11-01 12:22:46 +08:00 |
|
lxh2875931338
|
6ca31c37ba
|
feat(metadata): 元数据补全后主动推送界面刷新
之前标签补全后,收藏/文件列表要退出重进才显示新信息。
- 新增 MetadataEvents 事件总线(MetadataUpdate: path + songs 行)
- MetadataService 写库成功后 publish
- 首页收藏列表订阅:按 path 替换对应 SongItem(标题/艺术家/封面)
- WebDAV 文件列表订阅:按 path 更新 _metaByPath
效果:歌曲播完标签抓取完成后,列表里的那一行自动变成标签信息,
不需要返回重进。
|
2026-11-01 12:18:21 +08:00 |
|
lxh2875931338
|
632713d133
|
feat(ui): 主页收藏/歌单改为跟手 PageView
之前是"横向手势 + 淡入切换",与播放页的跟手滑动不一致。
- 列表区改为 SliverFillRemaining + PageView,直接跟手左右滑
- 媒体库/按钮保持固定,外层 CustomScrollView 不再滚动
- 收藏/歌单各自是独立 ListView,可正常纵向滚动
- 去掉原来的 GestureDetector 和淡入动画
- 标题行指示器继续跟随当前页
真机验证:左右滑动切换正常,指示器跟随。
|
2026-11-01 12:15:44 +08:00 |
|
lxh2875931338
|
9a932e92d4
|
style(ui): 小窗封面居中并加大,控件上移 8px
- 封面尺寸 30% -> 38%(上限 380dp),并在窗口中水平居中
- 控件行底部留白 12 -> 20(整体上移 8px)
|
2026-11-01 12:11:16 +08:00 |
|
lxh2875931338
|
5a1910bee4
|
fix(ui): 小窗播放页内容撑满窗口
紧凑模式原来用 SingleChildScrollView 直接包内容,
内容不足窗口高度时底部会留一大片空白。
改为 LayoutBuilder + ConstrainedBox(minHeight) + IntrinsicHeight:
- 内容不足时撑满并让 Spacer 把控件推到底部
- 内容超出时仍可滚动,不会把控件挤出屏幕
真机(Flyme 小窗)验证:封面缩小、歌词页隐藏、五个控件完整贴底。
|
2026-11-01 12:08:59 +08:00 |
|
lxh2875931338
|
6cb96e95dc
|
feat(ui): 小窗/分屏自适应播放页
按可用宽高比判断紧凑模式(全屏 3192/1368≈2.33,小窗 2577/1368≈1.88):
- 封面由撑满宽度改为固定方形(可用高度的 30%,上限 340dp)
- 标题 24→20、艺术家 16→14,各段间距收紧
- 歌词页不显示(PageView 两页:信息 / 主页),指示器同步改两段
- 紧凑模式下整页可滚动,避免控件被挤出屏幕
不依赖"记住分辨率":直接读当前 MediaQuery,窗口尺寸变化时自动切换。
|
2026-11-01 12:06:12 +08:00 |
|
lxh2875931338
|
432e3b1260
|
fix(webdav): 重新登录成功后回到主页,而不是停在文件列表
原因:_connect() 成功后用 pushReplacement(WebDAVFileListPage),
把 Setup 页替换成文件列表,所以看起来"登录完不返回"。
- 改为 Navigator.popUntil(isFirst):无论首次配置还是重新登录,都回主页
- 首页"重新登录"入口改为 await push,返回后重新加载 WebDAV 状态并刷新
- 清理两个未使用的 import
|
2026-11-01 11:55:38 +08:00 |
|
lxh2875931338
|
7bf8d18f18
|
fix(ui): 加入歌单弹层不再被迷你播放器遮挡
原因:迷你播放器挂在 Navigator overlay 上,层级高于底部弹层,
弹层里的歌单列表被它盖住,看起来像"没读到歌单"。
(日志确认查询正常:读到 1 个歌单)
- RouteManager 抽到 services/route_manager.dart,新增临时隐藏开关
- 打开"加入歌单"弹层时隐藏迷你播放器,关闭后恢复
- main.dart 的 shouldShow 改用 showMiniPlayer(原来直接读路由,开关不生效)
- 弹层底部加 SafeArea minimum + 16px 余量,不再贴着手势区
真机验证:弹层正常显示 "TL / 1 首",底部留出安全距离。
|
2026-11-01 00:09:27 +08:00 |
|
lxh2875931338
|
f793223311
|
feat(ui): 主页收藏/歌单左右滑动切换
- 媒体库下方按钮由三个减为两个(本地音乐 / 最近播放),对称排列
- 收藏区改为双页:我的收藏 / 歌单列表,左右滑动切换
- 标题行随页变化(图标 + 文案),右侧加两点指示器(当前页长条)
- 歌单项左侧改为歌单图标(不再是歌曲封面),显示名称与歌曲数
- 点击歌单进入详情页,返回后自动刷新
- 手势挂在 CustomScrollView 外层,纵向滚动不受影响
真机验证:左右滑动切换正常,指示器跟随,歌单列表显示 TL / 1 首。
|
2026-10-31 23:55:02 +08:00 |
|
lxh2875931338
|
e89bd6381a
|
feat(playlist): 歌单集合页 + 歌单详情页 + 首页入口
style(ui): 播放页红心与标题居中对齐、标题单行
- playlists_page:歌单列表(封面/名称/歌曲数)、新建、重命名、删除
- playlist_detail_page:歌单内歌曲、播放全部、单曲播放、移除
- 首页"歌单列表"按钮接入 PlaylistsPage
- 播放页:标题行改为 center 对齐、标题限制单行,
收藏按钮压到 36x36,红心与标题不再错位
|
2026-10-31 23:37:47 +08:00 |
|
lxh2875931338
|
e3f72a69a9
|
style(ui): 播放页间距等宽 + 控制行上移 25 + 进度条重写
- 标题/歌手/专辑的间距统一为 10,视感更整齐
- 底部留白 70 -> 45(控制行上移 25)
- 进度条改用 LayoutBuilder + Positioned 绝对宽度,
背景/缓冲/已播三层顺序明确,不再出现两端亮块
|
2026-10-31 23:31:54 +08:00 |
|
lxh2875931338
|
1c57d59d73
|
style(ui): 播放页内容下移 + 收藏移到标题右侧 + 底部改播放模式
按草图调整:
- 主播放页顶部留 16px,封面与指示器拉开距离
- 收藏心从底部控制行移到标题右侧(空心/实心红随收藏状态)
- 底部控制行第四位改为播放模式按钮(循环图标),队列保持在第五位
|
2026-10-31 23:17:01 +08:00 |
|
lxh2875931338
|
6351ef1395
|
fix(ui): 播放页顶部指示器在屏幕居中
AppBar 添加 centerTitle: true,指示器不再偏向左侧。
|
2026-10-31 23:03:30 +08:00 |
|
lxh2875931338
|
abe960cf58
|
style(ui): 播放页指示器改为"当前页长条"样式 + 补齐底部安全区
指示器(按设计稿):
- 三段:左点 / 中条 / 右点
- 当前页对应的段展开为 30x7 长条并高亮白色,
另外两段收起为 7x7 小点(暗)
- 主页=中间长条,信息页=左长条,歌词页=右长条,220ms 动画过渡
- 点击任一段可直接跳到对应页
安全区:
- PageView 外层包 SafeArea(top: false),
控制按钮不再贴着 mback 手势区
|
2026-10-31 22:59:27 +08:00 |
|
lxh2875931338
|
7cd1e13a82
|
feat(ui): 播放页改为三页横向切换(信息 / 主页 / 歌词)
- PageView 三页:左=文件信息,中=播放主页,右=歌词
- 顶部指示器:两点+连线,左点亮=信息页,右点亮=歌词页,
中间都暗=主页;点圆点可直接跳转
- 文件信息页:标题/歌手/专辑/文件夹/文件名/格式/路径,
优先用 DB 元数据,路径与文件名做解码
- 歌词页:占位(暂无歌词 + 歌名)
真机验证:慢速左右滑动切换正常,指示器跟随。
|
2026-10-31 22:55:16 +08:00 |
|
lxh2875931338
|
70227afea5
|
feat(playlist): 歌单数据层 + 加入歌单入口
数据层:
- PlaylistSummary(歌单名/歌曲数/封面),DB 用一条 SQL 取摘要,
封面取歌单内第一首有封面的歌
- PlaylistRepository.getSummaries() / addSongs()(批量加入并去重)
- 修复 AudioService.playPlaylist:之前把文件路径直接当 URL,
歌单播放必然失败;现在用 WebDAVService.getFileUrl 构造
入口(视觉为临时实现,等设计稿替换):
- 文件列表长按 -> "加入歌单" 底部弹层
- 可在弹层里新建歌单并直接加入
真机验证:长按歌曲 -> 新建 "TL" -> 数据库 playlists/playlist_songs 均有记录。
测试:新增 playlist_summary 4 个用例,累计 29 个全绿。
|
2026-10-31 22:43:55 +08:00 |
|
lxh2875931338
|
e010a9c0d3
|
style(ui): 去掉进度条滑块圆点
圆点是别的播放器的常见元素,我们的设计里不需要。
进度条保持"细线 + 时间在两侧"的纯净形态。
|
2026-10-31 22:32:47 +08:00 |
|
lxh2875931338
|
1604f296c8
|
feat(ui): 播放页改为大封面 + 左对齐信息 + 底部控制骨架
按设计稿重排(背景色保持 #0E1211 不变):
- 接近全宽的圆角封面(1:1,24 圆角)
- 标题/艺术家左对齐,下面补专辑名与所在目录
- 进度条改为"时间 + 滑块 + 时间"同一行,滑块加可见圆点
- 控制行:上一首 / 播放(68 圆) / 下一首 / 收藏 / 播放队列
- 收藏按钮接 favorites 表,状态随当前歌刷新
- 更多菜单:切换播放模式、播放队列、清除缓存
|
2026-10-31 22:32:10 +08:00 |
|
lxh2875931338
|
888ea3fd54
|
style(ui): 列表行距收紧 + 收藏项显示封面
- 收藏列表:vertical padding 6 -> 2,ListTile 用 compact,
标题 17->16、副标题 14->13,行高明显变紧凑
- 收藏项 leading 从音符图标换成 SongArtwork(复用 songs.artwork_path)
- WebDAV 文件列表:contentPadding 去掉 vertical 2,加 compact
真机截图确认:行距更接近设计稿,有元数据的歌显示封面。
|
2026-10-31 22:05:44 +08:00 |
|
lxh2875931338
|
30e68780f4
|
feat(playback): 拥塞网络退避重试 + mpv 缓冲加大
针对公司共享出口(整栋楼抢网):
- 恢复退避:连续失败 3 次后 30s、4 次 60s、5 次 120s,
冷却结束自动再试一次;累计 6 次后停止,等用户手动点
- 用户手动点播放会清零退避计数
- 成功出声(position>1s)后清零
- mpv 缓冲加大:cache-secs=60s、demuxer-max-bytes=64MiB、
demuxer-readahead-secs=30
真机验证:缓冲参数设置成功,起播 1576ms(此前 2098ms)。
|
2026-10-31 22:03:09 +08:00 |
|
lxh2875931338
|
5c4b0c2728
|
fix(playback): 网络切换重连不再抢在切歌前面
日志(21:51,用户刚把手机切到有线):
16.828 尝试重连:为爱痴狂(从 78s 继续) ← 网络变化触发,但用户已点新歌
17.569 play(新歌)
18.128 尝试重连:Communications(从 0s) ← 两条流程交叠
23.669 Failed to open -> 30.459 起播 5568ms
- _onNetworkChanged:去抖开始时记录 songId + playbackGeneration,
2s 后若已切歌则忽略本次重连
- _recoverFromStall:await 打开完成后校验 generation,
切歌了就放弃本次恢复,避免把旧歌重新打开
|
2026-10-31 21:54:15 +08:00 |
|
lxh2875931338
|
b439d1cb8c
|
fix(playback): 播放请求去重,避免同一次点击触发多次 open
日志证据(21:45):同一次切歌 play() 被调用 3 次
(31.798、32.839、33.442),互相顶掉,起播拖到 2977ms。
- _playCurrent 增加 800ms 同曲去重:同 index 且短时间内重复的请求直接忽略
- 实测:点击后 play() 只调用 1 次,起播 2977ms -> 2098ms
说明:剩余 ~2s 主要是 mpv 建连 + 首包 + 音频输出,
跨 FRP 链路属于正常范围,后续可单独调 mpv 参数优化。
|
2026-10-31 21:50:18 +08:00 |
|
lxh2875931338
|
fdca721250
|
fix(playback): 自动恢复熔断,避免坏文件导致无限重试
日志证据(21:32~21:33):同一首 "Communications - Keeno"
Failed to open ×42,自动恢复 ×54 —— 看门狗每 10s 重开一次,死循环。
- 新增 _recoveryAttempts:同一首歌最多自动恢复 3 次
- 起播看门狗、网络切换、打开失败重试都受熔断约束
- 位置 >1s(真的出声)后清零,正常歌曲不受影响
- 切歌清零,用户手动点播放可重新尝试
- 熔断后写入 lastPlaybackError="无法播放:<title>" 并停住,不再刷请求
测试 25 个全绿;release 已安装。
|
2026-10-31 21:41:37 +08:00 |
|
lxh2875931338
|
ca4546871c
|
fix(playback): 位置回退保护 + 恢复用最后已知位置 + 缓冲感知重试
对应 10/24 日志里的"播到一半中断后从头重播":
23:05:26 ffurl_read -> 从 77s 重连成功
23:05:33 再断 -> position 归 0 -> 看门狗"从 0s 继续",听感就是重新开始
- 新增 _lastKnownPosition:位置流累积,切歌归零;
所有恢复路径(看门狗 / 网络切换 / 打开失败)都用它续播
- 位置回退保护(抽成 utils/position_guard.dart):
已播 >10s 且新位置 <5s 时忽略回退,除非用户 seek / 切歌 / 正在恢复
- _scheduleOpenRetry 改为缓冲感知:
缓冲余量 >=12s 时延迟 4s 重试(不打断),否则 1.2s;
重试前若位置已推进超过 1s,判定 mpv 自愈,取消重连
- 收藏列表点击改为整队入队(setQueue,从点击项开始)
测试:新增 position_guard 6 个用例,累计 25 个全绿。
真机验证:收藏整队日志 "17 首,从第 16 首开始",播放正常。
|
2026-10-31 21:22:56 +08:00 |
|
lxh2875931338
|
672a545c38
|
feat(ui): 收藏项无元数据时,副标题显示上级文件夹名(带括号)
之前无标签时副标题固定写"收藏",和"歌手"语义混在一起。
现在取 path 的上一级目录(解码后)显示为 "(B-未分类)" 这种形式:
- 括号清楚表明是目录,不是艺术家
- 有标签时仍优先显示艺术家
- 目录层级不足或解码失败时回退为空(displaySubtitle 兜底"未知艺术家")
真机验证:收藏项显示 "13楼的大笨象 - 郑融" / "(B-未分类)"。
|
2026-10-24 21:44:14 +08:00 |
|
lxh2875931338
|
d3ed871626
|
fix(ui): 收藏列表文件名解码,避免显示成百分号编码串
收藏项没有元数据时回退文件名,但 path 是百分号编码的播放路径,
直接截取会显示 "13%E6%A5%BC%E7%9A%84..." 这种不可读内容。
- 展示前对文件名做 Uri.decodeComponent,解码失败才用原串
- 播放仍使用编码后的原 path,不受影响
验证:收藏 "13楼的大笨象 - 郑融" 在首页正常显示中文歌名。
|
2026-10-24 21:42:01 +08:00 |
|
lxh2875931338
|
c1e720311e
|
feat(ui): 文件列表收藏红心 + 首页收藏绑定 favorites 表
交互调整(按讨论):
- WebDAV 文件列表:整行点击 = 播放(原来的行为),右侧播放按钮改成收藏红心
已收藏实心红,未收藏空心;点击即写/删 favorites 表
- 首页"我的收藏"改为读 favorites 表 + songs 元数据;
从文件列表返回时自动刷新
- 收藏项右侧红心改为真正删除(原来只是内存 removeAt)
修复:
- 收藏列表项透明度绑定 _listReveal,首屏为 0 导致条目完全透明
(收藏成功但看不到)。收藏项改为固定 opacity 1.0。
验证(真机):
列表点红心 -> favorites 表 +1 -> 返回首页 -> 收藏区出现该条目
|
2026-10-24 21:38:36 +08:00 |
|
lxh2875931338
|
4a8b1f28db
|
feat(ui): 设置页第一版(缓存管理 / 诊断导出 / 连接状态 / 关于)
设置页此前没有任何入口,且内容全是占位。现在:
- 首页右上角菜单按钮 → 设置页
- 缓存:标签条数、封面数量/占用、临时探测文件占用,一键清理(保留歌单与收藏)
- 诊断:导出 版本/连接/缓存/当前播放/最近错误 到 txt,经 FileProvider 走系统分享
- 连接:显示 WebDAV 地址与用户,支持测试连接、退出登录
- 关于:版本、构建号、播放内核与 UI 许可说明
- 删除未被引用的 FullPlayerPage(与 player_page 重复)
真机验证:设置页正常打开,诊断导出成功调起系统分享面板。
|
2026-10-24 21:28:22 +08:00 |
|
lxh2875931338
|
d07086aaa0
|
feat(ui): 歌曲列表改用封面图,替代统一的小图标
- 新增 SongArtwork 组件:内存字节 > 本地封面文件 > 图标兜底,圆角裁剪
- WebDAV 文件列表:前导图标换成封面(来自 songs.artwork_path)
- 播放列表页:批量查封面路径(getSongsByPaths),正在播放的歌优先用内存里的封面字节;
切歌后重新拉一次,保证新补全的封面能显示
- 没有封面的歌仍回退为音符/播放图标,不影响列表结构
|
2026-10-24 20:47:12 +08:00 |
|
lxh2875931338
|
0a1ebfa5df
|
feat(ui): WebDAV 文件列表改用元数据标题/艺术家显示
之前列表只显示文件名,即使 DB 里已经有标签。
- SongDatabase 新增 getSongsByPaths():一次批量按路径查(分块 400,避开变量上限)
- WebDAVFileListPage:
* 加载目录后批量取元数据,title/artist 有值就替换文件名
* 副标题显示 "艺术家 · 大小",没有标签时仍显示大小
* 监听 AudioService,正在播放的歌补全元数据后立即刷新对应行
* 没有标签的新歌自然回退文件名,不影响使用
|
2026-10-24 20:41:21 +08:00 |
|
lxh2875931338
|
18308d9d4e
|
feat(metadata): 元数据分阶段探测(128KB → 512KB)+ song_key 改为纯 URL
渐进探测:
- FileProvider.getFile 增加 maxBytes 参数,WebDAV 按需只取头部
- MetadataService 先取 128KB 读标签;title/artist 都为空时再补到 512KB
- 缓存文件按上限区分(metadata_<hash>_128k.part / _512k.part)
song_key:
- 从 md5(url|size) 改为 md5(url)。分阶段探测的大小不同,
带 size 会让同一首歌产生两个键;URL 在各阶段稳定。
- 代价:同 URL 换内容不自动失效,由清除单曲缓存/forceRefresh 兜底
- 数据库升到 v6,清空 songs / metadata_cache(可再生)
验证(release,真机):
📦 取头部 128KB(上限 128KB,不再整文件下载)
🧭 状态机: first_seen → pending(validations=1)
📊 pending conf=0.92 evidence={...}
✅ saved: 两生关 - 谢安琪
测试:新增 song_key 3 个用例,累计 19 个全绿。
|
2026-10-24 20:28:50 +08:00 |
|
lxh2875931338
|
b8138ad125
|
feat(metadata): 元数据引擎第 3 步之一——一致性与冲突感知的状态机
之前只要 confidence>=0.6 且 validation_count>=2 就升级 verified,
两次读到不同结果也会被当成"验证通过",错误标签一旦写入很难纠正。
新增 MetadataStability(纯函数,可单测):
- 首次见到 → pending(first_seen),次数 1
- 与上次记录标题/艺术家冲突 → pending(conflict),次数重置为 1
- 一致且次数 >= 2、置信度达标 → verified(stable)
- 置信度不足 → pending(low_confidence),次数继续累计
- 字段缺失不算冲突;比较时忽略大小写与首尾空格
metadata_service 改为调用该状态机,并打印判定原因:
🧭 [MetadataService] 状态机: stable → verified(validations=2)
测试:新增 test/metadata_stability_test.dart 7 个用例,
连同原有 9 个共 16 个,flutter test 全绿。
已构建并安装 release 验证。
|
2026-10-24 20:24:55 +08:00 |
|
lxh2875931338
|
9f53d1c604
|
fix(playback): 打开失败后 1.5s 立即重试,切歌前摇从 ~20s 降到 ~7s
实测时间线(20:08:53 那次切歌):
53.881 next -> play
53.947 open 完成 65ms
59.525 player error: tcp ffurl_read / Failed to open (mpv 5.6s 后放弃)
09:13.801 起播看门狗才触发重连 (又等 14s)
原因:看门狗 5s 一跳、阈值 10s,最坏要两个 tick 才触发。
- error 流里识别 Failed to open / ffurl_read,1.5s 后立即重试一次
(每首歌最多一次,force 模式跳过节流)
- 进度看门狗 tick 10s -> 5s,任何卡死最多多等 5s
预期:失败发生在 5~6s,重试在 7~8s 开始,前摇 ~10s 降到 7~8s;
若首次打开成功则完全无感。
|
2026-10-24 20:14:31 +08:00 |
|
lxh2875931338
|
98db2321fb
|
fix(playback): 冷启动不再预加载音频,首次播放按保存进度打开
按今日日志定位:所谓"后台假死"其实是进程被系统回收
(13:14:46 ActivityManager: has died: prcp FGS,当时内存压力下批量杀进程)。
冷启动后旧代码会先联网 play→seek→pause,既慢又会和用户点播放打架。
- restorePlaybackState: 只恢复队列/进度/UI,不联网,不再调用 _loadRestoredPlayback
- 新增 playOrResume(): UI 按钮、通知栏、耳机键统一入口;
冷启动首次播放走 _playCurrent(open + seek 到保存位置 + play)
- AudioPlayerHandler.play() 改走 playOrResume()
- 播放中每 10s 持久化一次进度,冷启动恢复更准确
- 窗口期收紧:网络轮询 10s→3s、切换去抖 3s→2s、
起播看门狗 15s→10s、中段卡死 30s→20s
真机验证(release):
冷启动 8s 内 play 调用 0 次(不再预加载)
按键播放:冷启动首次播放 → open 21ms → restored position 15.751s → 1.5s 出声
|
2026-10-24 20:04:42 +08:00 |
|
lxh2875931338
|
30111a5f0c
|
feat(metadata): 元数据引擎第 2 步——置信度模型 + pending/verified 状态机
之前 evaluate() 写死 0.8、decide() 直接透传,history 参数没人用,
"自学习"只有骨架。这次补上:
置信度(MetadataNormalizer.evaluate)
- 字段来源可追溯:NormalizedMetadata 新增 titleFromTag / artistFromTag / albumFromTag
- 基础分 0.25;标题 +0.20、标题来自标签再 +0.10(文件名当标题则 -0.10)
- 艺术家 +0.15、来自标签 +0.10;专辑 +0.10;年份 +0.03;封面 +0.02
- 历史一致性:同艺人 >=2 条 +0.05,同专辑 >=2 条 +0.03
- 结果 clamp 到 0.05~0.98,且每个加分项写入 evidence,便于日志解释
状态机(MetadataService)
- 只有 confidence>=0.6 且 status=verified 的记录才直接命中
- pending 记录下次播放会重新读取并合并(自修复)
- validation_count 累加;>=2 次且置信度达标才升级为 verified
- 新结果置信度低于旧结果时保留旧字段,只累加验证次数
- 日志输出 status / confidence / validation_count / evidence
测试
- 新增 test/metadata_normalizer_test.dart,9 个用例覆盖来源标记、分级、
历史加权、证据、阈值;flutter test 全绿
真机验证
- 起播看门狗在真实弱网下触发:23:44:50 打开失败 → 15s 后自动重连 →
1.4s 出声(open 仅 100ms)
|
2026-10-23 23:45:29 +08:00 |
|
lxh2875931338
|
a5849969d3
|
perf(metadata): 元数据探测从 2MB 降到 512KB
1Panel 访问日志显示:2MB 的探测请求在当前链路上要 10~18 秒,
期间 libmpv 的 206 吞吐被压到 ~1Mbps,直接拖慢起播。
- _maxBytes 2MB -> 512KB:常见标签与封面都在此范围内
- 读不到标签时仍由 80% 重试兜底
- 日志文案同步更新
另:访问日志 CSV 属于本地参考,加入 .gitignore
|
2026-10-23 23:16:07 +08:00 |
|
lxh2875931338
|
af895c18e1
|
fix(metadata): 元数据回退到文件名时,在 80% 处重试一次
云服日志显示:对 /dav/.../*.flac 的 Range GET 在 10.3s 后返回 502,
是反代等待上游首包超时(清远源站响应慢),与我方 15s 超时无关。
影响仅限该曲的标签/封面,播放不受影响,但会一直回退文件名。
- _loadMetadataForCurrentSong: source == 'filename' 时置 _metadataNeedsRetry
- 播放到 80% 且该曲未重试过 → 再抓一次(每首歌最多一次)
- 因"播放未稳定"被跳过抓取的情况也标记为待重试
- 换歌时清空标记
|
2026-10-23 23:11:39 +08:00 |
|
lxh2875931338
|
2d571eaae4
|
feat(playback): 下一首预热(60% 时 HEAD 预握手)
日志实测:open 完成仅 37~60ms,但起播耗时 2.1~2.8s,
说明等待发生在 mpv 拿首批数据,而不是建连。
- 新增 PrefetchService:HEAD + Range 0-0,只走 DNS/TLS/反代,
不下载正文;失败静默,不影响播放
- AudioService 在进度达到 60%(且时长 > 60s)时预热下一首,
每首歌只触发一次;shuffle 模式下按当前随机索引推算
待验证:播到 60% 时应出现
🔥 [AudioService] 预热下一首:xxx
🔥 [Prefetch] 预热完成 xxxms,status=200/206
对比下一首的 "起播耗时" 是否下降。
|
2026-10-23 23:06:18 +08:00 |
|
lxh2875931338
|
111369c20c
|
feat(playback): 起播看门狗 + 起播耗时打点
现象(Plastic Love):state=PLAYING、position 恒为 0,75 秒内没有任何
buffering/error 日志,mpv 既不报错也不推进。通用看门狗要求"近期有缓冲",
因此完全没反应。
- 新增起播看门狗:position 持续为 0 超过 15s 且 playing=true → 强制重连
(不再要求 buffering 事件;暂停时 playing=false,不会误触发)
- 中段卡死判定也放宽为 playing || recentlyBuffering
- 打点:
* PlaybackService: "open 完成 xxxms,play 完成 xxxms"
* AudioService: "起播耗时 xxxms(open 完成 -> 出声)"
下次再出现"下一首很久",日志能直接区分是 open 慢还是数据到得慢。
|
2026-10-23 21:55:13 +08:00 |
|
lxh2875931338
|
b517c75cdd
|
feat(playback): 网络切换后自动重连当前歌曲
mpv 的 HTTP 长连接不会跟随网络迁移:换 Wi-Fi / Wi-Fi<->移动数据
会换掉出口 IP,旧连接失效但不一定报错,表现为进度停住、一直缓冲。
- 新增 NetworkWatcher:每 10s 枚举 IPv4 接口,签名变化即视为网络切换
(不依赖插件和额外权限,也能覆盖"同类型网络换 AP"的情况)
- AudioService 收到变化后 3s 去抖,然后强制重连当前歌曲并 seek 回原位置
(force 参数跳过 2 分钟节流,因为这是明确的外部事件)
真机验证(公网链路 + 移动数据):
21:43:23.123 检测到网络变化
21:43:26.124 主动重连(3s 去抖)
21:43:26.289 自动恢复完成(165ms)
随后 position 35.5s -> 41.7s 正常推进
|
2026-10-23 21:44:55 +08:00 |
|
lxh2875931338
|
937bcdd63d
|
fix(playback): 缓冲抖动下看门狗失效 + 元数据改到播放稳定后再抓
实测(公网链路,自然播完 -> 自动下一首)暴露两个新问题:
1. 看门狗在 buffering 抖动时永远不触发
原实现每次 buffering=true 都重置 30s 计时器,而弱网下它会 true/false
每 2~5s 抖一次,于是计时器一直被重置,卡死 60s+ 也不会自愈。
改为记录"最近一次缓冲时间",用「进度 30s 未推进 + 20s 内发生过缓冲」
两个条件判定,暂停时不会误触发。
2. 元数据抓取在起播瞬间抢带宽
新歌 open 后 mpv 还在持续填缓冲,此时插入 2MB 标签下载会把缓冲打回 0。
实测下一首因此要等 60s+。
改为等播放真正稳定(进度 >= 5s 且连续 2s 不在 buffering,最多等 30s)
再抓元数据;等不到就跳过本次(下次播放再试),优先保证出声。
验证:公网 + 移动数据下切歌,next 发起后 2ms 即 open;
未稳定时日志出现"播放未稳定,跳过本次元数据抓取",不再有 2MB 下载与播放抢带宽。
|
2026-10-23 21:35:40 +08:00 |
|
lxh2875931338
|
2b33c17aab
|
fix(playback): 增加进度看门狗,兜住不产生 buffering 事件的半开连接
公网链路实测(清远->华为云->香港CN2->openresty->OpenList HTTPS)中
出现过 tcp: ffurl_read 错误但未触发 buffering=true 的情况,
仅靠缓冲事件无法覆盖这类"连接半死"的卡顿。
- 每 10s 检查一次播放进度;播放中且 30s 未推进 → 触发自愈(重开 + seek 回原位)
- 切歌中/暂停时重置基准,避免误触发
- 与 buffering 看门狗共用 2 分钟节流
验证:公网路径连续播放 65s,无 false positive,进度正常推进。
|
2026-10-23 21:18:02 +08:00 |
|
lxh2875931338
|
9e58d52678
|
fix(playback): 修复切歌永久假死,并让元数据抓取不再抢播放带宽
问题(后台播一段时间后无法缓冲,点下一首也没反应,只能重启):
1. _isChangingTrack 是没有超时的锁:_playCurrent() 置 true 后,
只有走完 finally 才会清。mpv 的 open() 在弱网下可能挂很久,
锁就一直不释放,之后 next()/previous() 全被静默拒绝。
2. 元数据用 dio.download 整文件下载,和 mpv 流式播放抢带宽,
20Mbps FRP 下播放流只剩一两百 K/s,缓冲长期为 0。
3. 没有订阅 mpv 的 error/buffering,失败无感知、无法自愈。
改动:
- playback_service: open 20s / 控制命令 5s 超时;失败时 stop 回空闲并上抛异常
- audio_service:
* 去掉「正在切歌就拒绝」的逻辑,改为 latest-wins(epoch 校验,
旧请求自行放弃,只有最新请求解锁)
* 播放失败不再吞掉:记录 lastPlaybackError 并 notifyListeners
* 订阅 stream.error / stream.buffering
* 缓冲超过 30s 自动重连当前歌曲并 seek 回原位置(2 分钟内最多一次)
- file_provider: 元数据改为 Range 只取头部 2MB,流式写入并在切歌时取消上一请求
- home_page: 去掉重复的 PlaybackService().play()(同一首歌会被 open 两次),
登录校验提前
验证:dart analyze 无 error;gradlew assembleDebug BUILD SUCCESSFUL。
真机验证待设备重新连接后进行。
|
2026-10-23 21:04:45 +08:00 |
|
lxh2875931338
|
46a9add5cb
|
build+fix: debug 包独立为 .dev,并修复元数据缓存永远查不到
包名:
- debug 使用 com.lxh.qingting_player.dev(应用名"清听 Dev"),可与 release 并存
- release 保持 com.lxh.qingting_player
- Android 包名每一段必须以字母开头,因此用 .dev 而非 _dev
- AGP 9 需显式开启 buildFeatures.resValues;移除 manifest 中已废弃的 package 属性
元数据(两个独立原因叠加):
1. song_key 之前用 String.hashCode / 本地临时文件 mtime,都不稳定;
2. songs.remote_path 存的是完整 URL,而所有查询都用相对路径(song.id),
getSongByPath 永远 miss,UI 只能回退到文件名。
- 新增 utils/song_key.dart:songKeyFor(url,size)=md5(url|size),跨进程稳定
- metadata_service: remote_path 改存 fileId(相对路径),并改用稳定 song_key
- audio_service / file_provider: 同步使用稳定键
- 数据库升级 v5:清空 songs / metadata_cache(可再生缓存;歌单与收藏按路径存储,不受影响)
- device-check.ps1: 支持 -Package,默认校验 dev 包,启动组件改为向 PackageManager 解析
验证:两次独立 dart 进程 song_key 输出一致;真机 debug/release 并存安装,单实例自检通过。
|
2026-10-22 23:47:32 +08:00 |
|
lxh2875931338
|
6445ee94bf
|
fix(playback): 播放中不再向系统上报 idle,修复播放设备页显示未在播放
audio_service 把 idle 映射成 PlaybackState.STATE_NONE,之前用 duration>0 判断,
时长尚未解析出来时会一直上报 NONE。改为 playing || duration>0 即视为 ready。
设备验证(MEIZU 21 Pro / Android 16):
播放 -> state=PLAYING(3),暂停 -> state=PAUSED(2),不再出现 NONE(0)。
同时加固 device-check.ps1:确认进程真正退出后再抓冷启动日志,最多重试两次。
|
2026-10-22 22:57:34 +08:00 |
|
lxh2875931338
|
56c26e2fd5
|
snapshot: 存档 flutter create 模板覆盖后的现场,作为多实例修复前的基线
|
2026-10-22 21:59:09 +08:00 |
|
lxh2875931338
|
40d4af322b
|
开始进入修复阶段
|
2026-09-02 21:42:42 +08:00 |
|
lxh2875931338
|
cdf77b308d
|
开始给原先适配期的状态擦屁股:现在是修复播放页与播放列表页的显示
|
2026-09-02 20:44:46 +08:00 |
|