初步敲定一个readme,后续继续推进功能性设计,直到认为功能基本达成了,readme再继续设计完善
This commit is contained in:
@@ -1,17 +1,49 @@
|
||||
# qt_player
|
||||
# 清听 QingTing
|
||||
|
||||
A new Flutter project.
|
||||
[](https://flutter.dev)
|
||||
[](https://android.com)
|
||||
|
||||
## Getting Started
|
||||
**让远程音乐,像本地音乐一样可靠。**
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
清听是一个面向 Android 的 WebDAV 音乐播放器。
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
它解决的事情很简单:
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||
> **让存储在 WebDAV 服务器上的音频文件,获得稳定、连续、可控的本地级播放体验。**
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
## ✨ 能力
|
||||
|
||||
- WebDAV 流式播放,无需下载
|
||||
- 后台播放,退出应用不停
|
||||
- 通知中心、锁屏、耳机控制
|
||||
- 元数据读取与 SQLite 缓存
|
||||
- 通知栏封面图
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
```bash
|
||||
# 克隆
|
||||
git clone https://your-repo/qingting.git
|
||||
cd qingting
|
||||
|
||||
# 获取依赖
|
||||
flutter pub get
|
||||
|
||||
# 运行
|
||||
flutter run
|
||||
```
|
||||
|
||||
详细步骤:[Getting Started →](./docs/getting-started.md)
|
||||
|
||||
## 📖 文档
|
||||
|
||||
- [整体架构](./docs/architecture.md) —— 系统怎么组织的
|
||||
- [播放链路](./docs/playback.md) —— 一首歌从点击到喇叭
|
||||
- [元数据](./docs/metadata.md) —— 歌曲信息怎么被认识
|
||||
- [Android 集成](./docs/android.md) —— 怎么成为 Android 播放器
|
||||
- [存储策略](./docs/storage.md) —— 远程与本地如何共存
|
||||
- [设计决策](./docs/decisions.md) —— 为什么选了这些方案
|
||||
|
||||
---
|
||||
|
||||
> 最好的播放器,是当你开始听歌之后,就再也感觉不到它技术细节的那一个。
|
||||
+686
@@ -0,0 +1,686 @@
|
||||
你说得完全对。上一版确实是把 `frpc-console` 的文档骨架“迁移”过来了,而不是为清听重新设计的。现在我们来彻底扭转这个方向。
|
||||
|
||||
核心变化:**不再围绕“工程体系”组织文档,而是围绕“一首歌的生命路径”来组织**。每个文档回答一个具体的、用户视角的问题,而不是抽象的技术模块。
|
||||
|
||||
---
|
||||
|
||||
## 清听 (QingTing) 文档体系(最终版)
|
||||
|
||||
### 📁 目录结构
|
||||
|
||||
```
|
||||
qingting/
|
||||
│
|
||||
├── README.md
|
||||
│
|
||||
├── docs/
|
||||
│ ├── getting-started.md
|
||||
│ ├── architecture.md
|
||||
│ ├── playback.md
|
||||
│ ├── metadata.md
|
||||
│ ├── android.md
|
||||
│ ├── storage.md
|
||||
│ └── decisions.md
|
||||
│
|
||||
├── lib/
|
||||
├── android/
|
||||
├── assets/
|
||||
└── pubspec.yaml
|
||||
```
|
||||
|
||||
**只有 7 个文档 + 1 个 README。**
|
||||
|
||||
---
|
||||
|
||||
### 📄 各文档定位
|
||||
|
||||
| 文档 | 回答的问题 | 风格 |
|
||||
|:---|:---|:---|
|
||||
| **README.md** | 清听是什么?为什么做它?怎么快速体验? | 产品入口,克制,有信条 |
|
||||
| **getting-started.md** | 怎么把代码跑起来? | 开发者上手,不含架构 |
|
||||
| **architecture.md** | 整体怎么组织的?核心原则是什么? | 架构总览,哲学融入其中 |
|
||||
| **playback.md** | **一首歌是怎么被播放出来的?** | **核心文档,完整链路** |
|
||||
| **metadata.md** | 歌曲信息是怎么被认识的? | 诚实说明当前能力与局限 |
|
||||
| **android.md** | 它怎么成为一个真正的 Android 播放器? | 平台能力,面向使用者 |
|
||||
| **storage.md** | 远程数据和本地状态怎么共存? | 数据策略,区别于普通播放器 |
|
||||
| **decisions.md** | 为什么最终选了这些方案? | 设计记录,每问三部分 |
|
||||
|
||||
---
|
||||
|
||||
### ✍️ 各文档内容
|
||||
|
||||
---
|
||||
|
||||
#### 1. `README.md` —— 入口
|
||||
|
||||
```markdown
|
||||
# 清听 QingTing
|
||||
|
||||
[](https://flutter.dev)
|
||||
[](https://android.com)
|
||||
|
||||
**让远程音乐,像本地音乐一样可靠。**
|
||||
|
||||
清听是一个面向 Android 的 WebDAV 音乐播放器。
|
||||
|
||||
它解决的事情很简单:
|
||||
|
||||
> **让存储在 WebDAV 服务器上的音频文件,获得稳定、连续、可控的本地级播放体验。**
|
||||
|
||||
## ✨ 能力
|
||||
|
||||
- WebDAV 流式播放,无需下载
|
||||
- 后台播放,退出应用不停
|
||||
- 通知中心、锁屏、耳机控制
|
||||
- 元数据读取与 SQLite 缓存
|
||||
- 通知栏封面图
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
```bash
|
||||
# 克隆
|
||||
git clone https://your-repo/qingting.git
|
||||
cd qingting
|
||||
|
||||
# 获取依赖
|
||||
flutter pub get
|
||||
|
||||
# 运行
|
||||
flutter run
|
||||
```
|
||||
|
||||
详细步骤:[Getting Started →](./docs/getting-started.md)
|
||||
|
||||
## 📖 文档
|
||||
|
||||
- [整体架构](./docs/architecture.md) —— 系统怎么组织的
|
||||
- [播放链路](./docs/playback.md) —— 一首歌从点击到喇叭
|
||||
- [元数据](./docs/metadata.md) —— 歌曲信息怎么被认识
|
||||
- [Android 集成](./docs/android.md) —— 怎么成为 Android 播放器
|
||||
- [存储策略](./docs/storage.md) —— 远程与本地如何共存
|
||||
- [设计决策](./docs/decisions.md) —— 为什么选了这些方案
|
||||
|
||||
---
|
||||
|
||||
> 最好的播放器,是当你开始听歌之后,就再也感觉不到它技术细节的那一个。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. `docs/architecture.md` —— 架构总览(哲学融入)
|
||||
|
||||
```markdown
|
||||
# 整体架构
|
||||
|
||||
> *“代码定义它如何工作,架构定义它如何活着。”*
|
||||
|
||||
---
|
||||
|
||||
## 四条原则
|
||||
|
||||
清听的设计围绕四个原则展开,它们共同指向同一个目标:**播放体验的完整性**。
|
||||
|
||||
### 1. 播放优先
|
||||
|
||||
Metadata 识别失败,不能阻塞播放。
|
||||
封面加载失败,不能阻塞播放。
|
||||
数据库异常,不能阻塞播放。
|
||||
|
||||
播放是主路径,其他一切都是旁路。
|
||||
|
||||
### 2. 状态是事实,UI 只是观察者
|
||||
|
||||
播放状态只有一个事实源:`AudioService`。
|
||||
|
||||
UI、通知中心、锁屏都只观察这个状态,不拥有它。
|
||||
|
||||
### 3. 异步结果必须验证身份
|
||||
|
||||
切歌后,上一首歌的元数据请求如果才返回,必须丢弃。
|
||||
|
||||
通过 `_playbackGeneration` 为每次切歌分配唯一身份。
|
||||
|
||||
### 4. 平台边界显式接入
|
||||
|
||||
Android 特有功能(FileProvider、前台服务)通过 `MethodChannel` 显式调用,不依赖第三方插件的封装。
|
||||
|
||||
---
|
||||
|
||||
## 分层结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ UI Layer │
|
||||
│ PlayerPage / MiniPlayer / Playlist │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ AudioService │
|
||||
│ 队列 · 状态 · 模式 · generation │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ AudioPlayerHandler │
|
||||
│ MediaSession · PlaybackState · 系统回调│
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ MetadataService │
|
||||
│ 读取 · 标准化 · 缓存 · 封面 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
用户点击歌曲
|
||||
↓
|
||||
设置队列 → 更新索引
|
||||
↓
|
||||
加载 WebDAV URL
|
||||
↓
|
||||
media_kit 播放
|
||||
↓
|
||||
发布 PlaybackState
|
||||
↓
|
||||
UI 更新 / 通知更新
|
||||
```
|
||||
|
||||
元数据加载是旁路,不阻塞上述流程。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. `docs/playback.md` —— 播放链路(核心文档)
|
||||
|
||||
```markdown
|
||||
# 播放链路
|
||||
|
||||
> *“一首歌从点击到喇叭出声,中间发生了什么?”*
|
||||
|
||||
这是清听最核心的文档。它描述的是整个播放系统如何协同工作,让用户获得连续、可靠的聆听体验。
|
||||
|
||||
---
|
||||
|
||||
## 总览
|
||||
|
||||
```
|
||||
User Action
|
||||
↓
|
||||
Queue Management
|
||||
↓
|
||||
Current Index
|
||||
↓
|
||||
Generation (防漂移)
|
||||
↓
|
||||
WebDAV URL
|
||||
↓
|
||||
media_kit
|
||||
↓
|
||||
Playback State
|
||||
↓
|
||||
AudioService → UI / Notification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 队列与索引
|
||||
|
||||
```dart
|
||||
class AudioService {
|
||||
List<Song> _queue = [];
|
||||
int _currentIndex = -1;
|
||||
List<int> _shuffledIndices = [];
|
||||
int _shuffledIndex = -1;
|
||||
}
|
||||
```
|
||||
|
||||
- `setQueue(queue, index)`:设置队列并开始播放
|
||||
- `next()` / `previous()`:根据当前模式(顺序/随机/单曲循环)计算下一首
|
||||
|
||||
---
|
||||
|
||||
## Generation 机制(防漂移)
|
||||
|
||||
这是清听处理异步竞态的关键设计。
|
||||
|
||||
```dart
|
||||
int _playbackGeneration = 0;
|
||||
|
||||
void _playCurrent() {
|
||||
_playbackGeneration++; // 每次切歌递增
|
||||
_loadMetadataForCurrentSong(_playbackGeneration);
|
||||
}
|
||||
|
||||
void _loadMetadataForCurrentSong(int generation) async {
|
||||
final data = await fetchMetadata();
|
||||
if (_playbackGeneration != generation) return; // 丢弃过期结果
|
||||
applyMetadata(data);
|
||||
}
|
||||
```
|
||||
|
||||
**场景:**
|
||||
|
||||
```
|
||||
歌曲 A → 请求元数据(gen=1)
|
||||
用户切到歌曲 B → gen=2,请求元数据
|
||||
歌曲 A 的元数据返回 → gen=1 ≠ 2 → 丢弃
|
||||
歌曲 B 的元数据返回 → gen=2 = 2 → 应用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PlaybackState 发布
|
||||
|
||||
```dart
|
||||
playbackState.add(PlaybackState(
|
||||
controls: [skipToPrevious, playPause, skipToNext],
|
||||
playing: playing,
|
||||
position: _currentPosition,
|
||||
updateTime: DateTime.now(),
|
||||
systemActions: {MediaAction.seek},
|
||||
));
|
||||
```
|
||||
|
||||
- `systemActions` 必须包含 `seek`,通知栏进度条才能拖动
|
||||
- 使用 `playPause` 而非分离的 `play`/`pause`
|
||||
- 每 500ms 节流发布,平衡性能与流畅度
|
||||
|
||||
---
|
||||
|
||||
## 完整时序
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant UI
|
||||
participant AudioService
|
||||
participant media_kit
|
||||
participant MetadataService
|
||||
|
||||
User->>UI: 点击歌曲
|
||||
UI->>AudioService: setQueue(queue, index)
|
||||
AudioService->>AudioService: _currentIndex = index
|
||||
AudioService->>AudioService: _playbackGeneration++
|
||||
AudioService->>media_kit: 加载 WebDAV URL
|
||||
media_kit-->>AudioService: 开始播放
|
||||
AudioService->>AudioService: 发布 PlaybackState
|
||||
AudioService-->>UI: 状态更新
|
||||
|
||||
par 旁路加载
|
||||
AudioService->>MetadataService: 请求元数据(携带 generation)
|
||||
MetadataService-->>AudioService: 返回
|
||||
AudioService->>AudioService: 校验 generation
|
||||
AudioService-->>UI: 更新歌曲信息
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4. `docs/metadata.md` —— 元数据
|
||||
|
||||
```markdown
|
||||
# 元数据
|
||||
|
||||
> *“歌曲信息是怎么被认识的?”*
|
||||
|
||||
---
|
||||
|
||||
## 数据来源
|
||||
|
||||
```
|
||||
WebDAV 音频文件
|
||||
│
|
||||
├── ID3 标签 (MP3)
|
||||
├── Vorbis 注释 (OGG/FLAC)
|
||||
└── MP4 原子 (M4A)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 读取流程
|
||||
|
||||
```
|
||||
1. 检查内存缓存 → 命中则返回
|
||||
2. 检查 SQLite → 命中则反序列化
|
||||
3. 下载文件头部 (~1MB) 到临时目录
|
||||
4. audio_metadata_reader 提取标签
|
||||
5. MetadataNormalizer 标准化
|
||||
6. 字段缺失时 fallback 到文件名/路径
|
||||
7. 写入 SQLite 和内存缓存
|
||||
8. 异步提取封面图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 标准化与 Fallback
|
||||
|
||||
```dart
|
||||
class MetadataNormalizer {
|
||||
String normalizeTitle(String? tag, String filename) {
|
||||
return tag?.isNotEmpty == true ? tag! : _extractFromFilename(filename);
|
||||
}
|
||||
|
||||
String normalizeArtist(String? tag, String? album, String path) {
|
||||
return tag?.isNotEmpty == true ? tag! : _extractFromPath(path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
优先级:**标签 > 文件名 > 路径推断**
|
||||
|
||||
---
|
||||
|
||||
## 封面图处理
|
||||
|
||||
```
|
||||
artwork bytes (内存)
|
||||
↓
|
||||
保存到 /data/.../artworks/{md5(id)}.jpg
|
||||
↓
|
||||
通过 MethodChannel 调用 FileProvider
|
||||
↓
|
||||
content:// URI
|
||||
↓
|
||||
MediaItem.artUri → 通知中心
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前状态与未来
|
||||
|
||||
**已实现:**
|
||||
- 一次读取 + fallback
|
||||
- SQLite 持久化
|
||||
- 封面图提取与推送
|
||||
|
||||
**计划中(渐进式识别):**
|
||||
- 5% 快速探测 (title/artist)
|
||||
- 70% 完整探测 (album/genre/year)
|
||||
- 100% 最终确认
|
||||
- 置信度与校验次数记录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5. `docs/android.md` —— Android 集成
|
||||
|
||||
```markdown
|
||||
# Android 集成
|
||||
|
||||
> *“它怎么成为一个真正的 Android 播放器?”*
|
||||
|
||||
---
|
||||
|
||||
## 与系统的关系
|
||||
|
||||
```
|
||||
Flutter (Dart)
|
||||
│
|
||||
├── audio_service (桥接层)
|
||||
│
|
||||
▼
|
||||
Android MediaSession
|
||||
│
|
||||
├── 通知中心 (播放/暂停/上一首/下一首/进度条)
|
||||
├── 锁屏控制
|
||||
├── 耳机按钮 (播放/暂停/切歌)
|
||||
└── Android Auto (预留)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MediaSession 契约
|
||||
|
||||
清听遵循 Android 媒体应用的契约规范:
|
||||
|
||||
| 要求 | 清听实现 |
|
||||
|:---|:---|
|
||||
| 声明 `MediaAction.seek` | `systemActions` 中包含 |
|
||||
| 使用 `playPause` 统一控制 | 不分离 play/pause |
|
||||
| 正确更新 `updateTime` | 每次发布同步更新 |
|
||||
| 前台服务 | 播放时启动 Foreground Service |
|
||||
|
||||
---
|
||||
|
||||
## 通知中心控制
|
||||
|
||||
| 控制 | 实现 |
|
||||
|:---|:---|
|
||||
| 播放/暂停 | `MediaAction.playPause` → `click(MediaButton.media)` |
|
||||
| 上一首/下一首 | `MediaAction.skipToPrevious/Next` |
|
||||
| 进度拖动 | `systemActions: {seek}` + `Handler.seek()` |
|
||||
| 封面图 | `MediaItem.artUri = content://...` |
|
||||
|
||||
---
|
||||
|
||||
## FileProvider(封面图推送)
|
||||
|
||||
Android 10+ 禁止 `file://` 跨应用传递。
|
||||
|
||||
```
|
||||
flutter_file_provider (不采用)
|
||||
↓
|
||||
MethodChannel: com.lxh.qingting_player/file_provider
|
||||
↓
|
||||
Kotlin: FileProvider.getUriForFile()
|
||||
↓
|
||||
content:// URI
|
||||
```
|
||||
|
||||
**为什么不用插件?** 维护状态不确定,兼容性问题。显式边界更可控。
|
||||
|
||||
---
|
||||
|
||||
## 后台播放
|
||||
|
||||
```dart
|
||||
await AudioService.start(
|
||||
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||
androidNotificationOngoing: false,
|
||||
androidStopForegroundOnPause: false,
|
||||
);
|
||||
```
|
||||
|
||||
- `ongoing: false`:暂停时不显示“正在运行”
|
||||
- `stopForegroundOnPause: false`:暂停时仍保留前台状态,快速恢复
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6. `docs/storage.md` —— 存储策略
|
||||
|
||||
```markdown
|
||||
# 存储策略
|
||||
|
||||
> *“远程数据和本地状态怎么共存?”*
|
||||
|
||||
---
|
||||
|
||||
## 两类数据
|
||||
|
||||
```
|
||||
远程数据 (WebDAV)
|
||||
│
|
||||
├── 音频文件 (流式播放,不缓存)
|
||||
└── 目录结构 (浏览用)
|
||||
│
|
||||
本地数据 (设备存储)
|
||||
│
|
||||
├── 元数据 (SQLite)
|
||||
├── 封面图 (文件系统)
|
||||
└── 播放状态 (内存)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 为什么不缓存音频文件?
|
||||
|
||||
- WebDAV 存储通常较大,本地存储有限
|
||||
- 流式播放已足够,无需额外下载
|
||||
- 版权/存储空间考量
|
||||
|
||||
> **只缓存“关于歌曲的信息”,不缓存歌曲本身。**
|
||||
|
||||
---
|
||||
|
||||
## SQLite 表结构
|
||||
|
||||
### songs
|
||||
|
||||
| 字段 | 说明 |
|
||||
|:---|:---|
|
||||
| `song_key` | WebDAV 路径 (唯一标识) |
|
||||
| `title` | 歌曲标题 |
|
||||
| `artist` | 艺术家 |
|
||||
| `album` | 专辑 |
|
||||
| `artwork_path` | 封面图本地路径 |
|
||||
| `confidence` | 置信度 (预留) |
|
||||
| `validation_count` | 校验次数 (预留) |
|
||||
|
||||
### metadata_cache
|
||||
|
||||
| 字段 | 说明 |
|
||||
|:---|:---|
|
||||
| `song_key` | 关联 songs |
|
||||
| `cache_path` | 缓存文件路径 (预留) |
|
||||
| `cache_created_at` | 创建时间 |
|
||||
|
||||
---
|
||||
|
||||
## 缓存策略
|
||||
|
||||
| 数据 | 缓存位置 | 有效期 |
|
||||
|:---|:---|:---|
|
||||
| 元数据 | SQLite | 永久 (除非 WebDAV 文件变更) |
|
||||
| 封面图 | 文件系统 | 永久 |
|
||||
| 播放进度 | 内存 | 会话级 |
|
||||
| 队列 | 内存 | 会话级 |
|
||||
|
||||
> 元数据缓存的失效策略:当前以 `song_key` (WebDAV 路径) 为唯一标识,文件变更后重新读取。
|
||||
|
||||
---
|
||||
|
||||
## 文件目录
|
||||
|
||||
```
|
||||
/data/data/com.lxh.qingting_player/
|
||||
├── app_flutter/
|
||||
│ ├── artworks/
|
||||
│ │ └── {md5(id)}.jpg
|
||||
│ └── temp/
|
||||
│ └── metadata_*.tmp
|
||||
└── databases/
|
||||
└── qingting.db
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 7. `docs/decisions.md` —— 设计决策
|
||||
|
||||
```markdown
|
||||
# 设计决策
|
||||
|
||||
> *“为什么最终选了这些方案?”*
|
||||
|
||||
每个决策三段:**问题 → 决策 → 原因**
|
||||
|
||||
---
|
||||
|
||||
## 为什么用 media_kit 而不是 just_audio?
|
||||
|
||||
**问题:** 需要稳定支持 WebDAV 流式播放和 MediaSession 集成。
|
||||
|
||||
**决策:** 选用 `media_kit`。
|
||||
|
||||
**原因:** `media_kit` 基于 libmpv,对流式 HTTP 支持更好,与 `audio_service` 集成更紧密。`just_audio` 虽社区更大,但对 MediaSession 的支持需要额外桥接。
|
||||
|
||||
---
|
||||
|
||||
## 为什么用 playPause 而非 play / pause 分离?
|
||||
|
||||
**问题:** 通知中心按钮状态偶尔错乱。
|
||||
|
||||
**决策:** 统一使用 `MediaAction.playPause`。
|
||||
|
||||
**原因:** Android MediaSession 对分离 action 的映射有特定逻辑,`playPause` 是系统原生支持的统一控制方式。
|
||||
|
||||
---
|
||||
|
||||
## 为什么不用 flutter_file_provider?
|
||||
|
||||
**问题:** 需要向通知中心传递 `content://` URI。
|
||||
|
||||
**决策:** 通过 `MethodChannel` 调用 Kotlin 侧原生 `FileProvider`。
|
||||
|
||||
**原因:** `flutter_file_provider` 插件维护状态不确定,Android 版本兼容性问题。显式平台边界更可控。
|
||||
|
||||
---
|
||||
|
||||
## 为什么引入 generation 机制?
|
||||
|
||||
**问题:** 切歌后,上一首歌的元数据返回会覆盖当前歌曲信息。
|
||||
|
||||
**决策:** 每次切歌递增 `_playbackGeneration`,异步回调校验身份。
|
||||
|
||||
**原因:** 防御性编程,处理异步竞态。这是清听保证 UI 状态永远属于当前歌曲的关键机制。
|
||||
|
||||
---
|
||||
|
||||
## 为什么用 SQLite?
|
||||
|
||||
**问题:** 需要持久化元数据,支持快速检索。
|
||||
|
||||
**决策:** 使用 `sqflite`。
|
||||
|
||||
**原因:** 边缘设备不需要 PostgreSQL;单文件备份复制即迁移;零配置零维护。
|
||||
|
||||
---
|
||||
|
||||
## 为什么进度更新节流到 500ms?
|
||||
|
||||
**问题:** 进度回调 (100ms/次) 导致频繁 UI 重建和通知刷新。
|
||||
|
||||
**决策:** 每 500ms 发布一次进度变化。
|
||||
|
||||
**原因:** 人眼对 500ms 的进度变化已足够流畅,在性能和体验之间取得平衡。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 这套文档体系的特征
|
||||
|
||||
1. **沿着“一首歌的生命路径”组织** —— 从点击、播放、元数据、Android 集成到存储,自然连贯
|
||||
2. **哲学融入架构,不单独成立文件** —— `architecture.md` 第一节讲原则,不做独立的 `philosophy.md`
|
||||
3. **`playback.md` 是绝对核心** —— 清听最有价值的文档,讲述完整链路
|
||||
4. **诚实区分已实现和计划中** —— `metadata.md` 明确标注当前状态与未来
|
||||
5. **`decisions.md` 保持简洁** —— 每问三部分:问题 → 决策 → 原因,不加 ADR 编号
|
||||
6. **没有“文档体系”的仪式感** —— 只有 7 个文档,每个回答一个具体问题
|
||||
|
||||
---
|
||||
|
||||
### 与 frpc-console 文档的差异
|
||||
|
||||
| 方面 | frpc-console | 清听 |
|
||||
|:---|:---|:---|
|
||||
| 核心主线 | 部署与长期运行 | 播放链路 |
|
||||
| 关键文档 | `deployment-engine.md` | `playback.md` |
|
||||
| 哲学位置 | 独立 `philosophy.md` | 融入 `architecture.md` |
|
||||
| 平台文档 | `edge-node.md` (硬件边界) | `android.md` (系统边界) |
|
||||
| 文档数量 | 10+ | 7 |
|
||||
| 文档命名 | 抽象命名 (engineering-philosophy) | 直白命名 (playback, metadata) |
|
||||
|
||||
---
|
||||
|
||||
> **frpc-console**:这是一个系统应该如何被长期运行。
|
||||
>
|
||||
> **清听**:这是一个播放器应该如何让用户忘记系统正在运行。
|
||||
|
||||
两个项目共享同一套工程哲学,但拥有各自独立的文档骨架。
|
||||
Reference in New Issue
Block a user