Files

200 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SongTidy 项目计划
## 1. 项目概述
跨平台桌面应用,用于批量整理音频文件名:扫描/选择音频文件 → 解析文件名提取**歌手/歌曲名/注释** → 界面编辑(含一键交换歌手与歌曲名)→ 一键批量重命名。支持将编辑进度暂存为 XML「计划」文件,下次继续。
## 2. 技术栈
| 项 | 选型 |
|---|---|
| 框架 | .NET 10(`net10.0`,本地 SDK 10.0.401) |
| UI | Avalonia **11.3.22**(以本地 `avalonia.mvvm` 模板生成骨架后升级至此版本) |
| 主题/控件库 | **Semi.Avalonia 11.3.14**(替代模板自带主题,不使用默认界面库) |
| 图标库 | **IconPacks.Avalonia.Lucide 2.0.0** |
| MVVM | ReactiveUI + `Avalonia.ReactiveUI`(11.3.x,与 Avalonia 对齐) |
| 依赖注入 | 不引入 DI 容器,轻量组合(服务在 ViewModel 中直接构造) |
| 测试 | MSTest,独立项目 `SongTidy.Tests` |
以上库版本已验证可共存。`App.axaml` 中引入 Semi 主题替换模板默认主题;图标通过 IconPacks 的 Lucide 图标控件用于菜单项、交换按钮等处。
## 3. 领域规则
### 3.1 字段约束
- **歌曲名**:必选
- **歌手**:可选(对应不知道歌手的歌曲)
- **注释**:可选
### 3.2 文件名解析算法(纯字符串操作,不用正则)
输入为**不含扩展名**的文件名。
**步骤一 · 括号抽取(严格模式)**
1. 识别中文括号 `()` 与英文括号 `()` 两类,**同类才配对**(`(a)` 属落单)
2. 栈式扫描配对;**出现任何落单括号(未匹配的开或闭)→ 放弃整个抽取步骤**,全串原样进入步骤二(此时注释为空)
3. 每对括号:删去两侧括号符,内容 `Trim()`(仅标准空白,不含全角空格)作为注释素材
4. 抽到多个注释素材:以**单个半角空格**连接
5. 嵌套括号(罕见):最外层配对生效,内部括号作字面字符保留
6. 已抽取的括号对从源串中移除,剩余部分进入步骤二
**步骤二 · 连字符分割**
- 仅认半角 `-`(全角 `-`、破折号 `—`/`–` 等不认)
- 无 `-`:歌手为空,整串 `Trim()` 为歌曲名
- 恰好一个 `-`:前段归歌手、后段归歌曲名(粗暴分配,交换按钮兜底),各自 `Trim()`
- 多个 `-`:歌手为空,步骤二输入的整串 `Trim()` 为歌曲名
**示例**:`A - B (Live - Tokyo)(修正版).mp3` → 歌手 `A`,歌曲名 `B`,注释 `Live - Tokyo 修正版`
### 3.3 目标文件名格式(预设注册表,代码留扩展点)
| 歌手 | 注释 | 歌手在前 | 歌曲在前 |
|------|------|----------|----------|
| 有 | 有 | `A - N (C)` | `N (C) - A` |
| 有 | 无 | `A - N` | `N - A` |
| 无 | 有 | `N (C)` | `N (C)` |
| 无 | 无 | `N` | `N` |
- 注释括号固定半角 `()`
- **扩展名保留原样**,不参与解析与生成(flac 重命名后仍是 `.flac`)
- 实现为格式定义注册表(占位符 A/N/C + 各缺省分支模板),后续新增格式只需注册新定义
### 3.4 重命名校验关卡(全有或全无)
`输出 > 重命名` 点击后先做**全量校验**,任一问题即弹窗列出**全部**问题(含行号与原因)并拒绝执行:
1. 歌曲名为空
2. 字段含文件名非法字符(`\ / : * ? " < > |` 及控制字符),或目标名以点/空格结尾(Windows 限制)
3. 源文件已不存在(含 XML 加载后文件丢失的情形,在此统一拦截)
4. 列表中存在重复源路径(区分大小写的直接字符串比较,与合并规则一致)
5. 目标名冲突:多个条目生成相同目标名;或目标与磁盘既有文件冲突(自身源文件除外)——此项保持**大小写不敏感**,与文件系统实际行为一致(同目录不能同时存在 `a.mp3` 与 `A.mp3`),可挡住 `A→B` 且 `B→A` 这类顺序性冲突链
### 3.5 重命名执行
校验通过 → **MessageBox 式确认弹窗**(显示条目数与当前格式,防误触)→ 确认后批量执行 → 弹窗报告结果。执行成功后**条目保持不动**(仍指向旧文件名,再次重命名会被「文件缺失」拦截,属预期行为)。
## 4. 功能规格
### 4.1 主窗口
**菜单栏**:
- **文件**:新建计划|打开计划|—|保存计划|另存为计划|—|关闭计划|—|退出
- **输入**:从文件夹扫描|从文件选择
- **输出**:重命名
- **关于**:关于
**信息条**(DataGrid 上方):
- 左:`共 N 个条目`
- 右:命名格式选择器(歌手在前 / 歌曲在前),驱动目标文件名预览列实时刷新
**主区域 DataGrid**:
`#`|`原文件名`(只读)|`歌手`|`歌曲名`|`注释`(可编辑)|`目标文件名`(只读实时预览)|`⇄`(行内交换按钮,仅交换歌手与歌曲名,无批量操作、无重新解析)
无底部状态栏。
### 4.2 输入
- **从文件夹扫描**:系统文件夹选择器;**仅当前文件夹不递归**;按固定扩展名列表过滤:`mp3 flac wav ogg m4a aac wma opus`(代码常量)
- **从文件选择**:系统文件选择器,多选,过滤器允许任意文件(`所有文件 (*.*)`)
- **合并规则**:按完整路径判重(**区分大小写的直接字符串比较**,不做 Windows 大小写不敏感特判);出现重复条目时**新的解析结果直接丢弃,保留用户原条目(含已编辑内容)**;非重复条目追加到列表末尾
### 4.3 计划文档(XML 暂存)
**序列化实现**
- 使用 .NET 内置 `System.Xml.Serialization.XmlSerializer`
- 模型类**显式标注** `[XmlRoot]`、`[XmlElement]`、`[XmlAttribute]`、`[XmlArray]`/`[XmlArrayItem]`、`[XmlEnum]` 等 attribute 指定 node name,**不依赖类名/属性名推导**
**文件格式**(属性代替子节点,UTF-8,过滤器 `*.xml`):
```xml
<?xml version="1.0" encoding="utf-8"?>
<Plan version="1">
<Format>ArtistFirst</Format>
<Items>
<Item Path="D:\Music\A - B (Live).mp3" Artist="A" Title="B" Comment="Live"/>
<Item Path="D:\Music\C.mp3" Artist="" Title="C" Comment=""/>
</Items>
</Plan>
```
- `version`:文件版本号(`[XmlAttribute]`,当前 `1`,预留后续演进)
- `Format`:当前命名格式,以枚举名序列化(枚举成员用 `[XmlEnum]` 显式标注,如 `ArtistFirst`/`TitleFirst`)
- `Item`:`Path`/`Artist`/`Title`/`Comment` 全部为 XML 属性,空字段写空字符串(属性始终写出)
- 特殊字符(`&`、`"`、`<` 等)由 XmlSerializer 自动转义
**文档模型**
- 数据模型:全部条目(原始文件绝对路径 + 三字段当前值)+ 当前命名格式;无其他状态概念
- **加载计划时恢复主窗口格式选择器**;**切换格式也计入脏标记**
- **新建/关闭计划**:清空列表、清除路径
- **保存计划**:已有路径则原路径覆盖,否则走另存为;**另存为计划**:选择新路径保存
- **脏提示**:新建/打开/关闭计划及退出时,若有未保存修改弹窗三选:保存 / 不保存 / 取消
- 加载时**不校验**文件存在性(重命名关卡统一拦截)
### 4.4 弹窗基础设施
Avalonia 无内置 MessageBox,项目自建**通用 MessageBox 样式对话框组件**(标题 + 多行正文 + 可配置按钮组合,如 `确定`、`是/否`、`保存/不保存/取消`),全项目共用:
- 重命名前确认弹窗(防误触)
- 脏提示弹窗(保存 / 不保存 / 取消)
- 校验错误报告(正文为全部问题的多行清单)
- 重命名结果报告
(「关于」对话框为独立窗口,不套用 MessageBox)
## 5. 项目结构
```
SongTidy/
├── SongTidy.csproj # net10.0;Avalonia 11.3.22、Semi.Avalonia 11.3.14、
│ # IconPacks.Avalonia.Lucide 2.0.0、Avalonia.ReactiveUI
├── Program.cs / App.axaml(.cs) / ViewLocator.cs # 模板骨架(App.axaml 换用 Semi 主题)
├── Models/
│ ├── SongPlan.cs # [XmlRoot("Plan")]:version 属性、Format、Items
│ ├── PlanItem.cs # [XmlArrayItem("Item")]:Path/Artist/Title/Comment 均为 [XmlAttribute]
│ └── RenameFormats.cs # 格式预设注册表(可扩展)+ 带 [XmlEnum] 的格式枚举
├── Services/
│ ├── FilenameParser.cs # 3.2 解析算法(纯静态)
│ ├── AudioFileScanner.cs # 文件夹扫描(非递归、扩展名过滤)
│ ├── RenameValidator.cs # 3.4 全量校验
│ ├── RenameExecutor.cs # 批量重命名
│ └── PlanXmlIO.cs # 计划 XML 读写(XmlSerializer)
├── ViewModels/
│ ├── ViewModelBase.cs
│ ├── MainWindowViewModel.cs # 菜单命令、计划文档状态(路径/脏标记)、格式选择、条目集合
│ └── SongEntryViewModel.cs # 行 VM:三字段、交换命令、目标名预览
├── Views/
│ ├── MainWindow.axaml(.cs)
│ └── Dialogs/
│ └── MessageBoxWindow.axaml(.cs) # 通用 MessageBox 组件 + 轻量调用辅助
SongTidy.Tests/ # MSTest:解析、格式生成、校验、XML IO
```
响应式要点:`ReactiveObject` + `ReactiveCommand`;预览列与信息条计数用 `WhenAnyValue`/`ObservableAsPropertyHelper` 派生;脏标记由字段编辑、集合变更、格式切换驱动;文件与文件夹选择器走 `StorageProvider`。
## 6. 实施步骤
1. `dotnet new avalonia.mvvm` 生成骨架,升级包版本至 Avalonia 11.3.22 / Semi.Avalonia 11.3.14 / IconPacks.Avalonia.Lucide 2.0.0 / Avalonia.ReactiveUI,`App.axaml` 接入 Semi 主题;建 `SongTidy.Tests`(MSTest)
2. 实现 `FilenameParser` + 解析单测(覆盖落单括号、多注释、多连字符、无连字符、全半角混合、嵌套括号等边界)
3. 实现 `RenameFormats` 注册表与生成逻辑 + 单测(四种组合 × 两种格式)
4. 实现 `PlanXmlIO` + 单测(格式枚举读写、空字段、路径含空格/特殊字符、`version` 保留)
5. 实现 `RenameValidator` + 单测(五类问题、冲突链)
6. 实现 ViewModel 层(文档模型、命令、脏跟踪、合并规则)
7. 实现主窗口视图(菜单栏、信息条、DataGrid 绑定、交换按钮、Lucide 图标)
8. 实现 `RenameExecutor`、MessageBox 通用组件与确认/报告/错误/脏提示弹窗
9. 「关于」对话框、窗口标题图标等收尾;全量测试通过
## 7. 非目标
- 不读写音频元数据标签(ID3 等)
- 不递归扫描子文件夹
- 无撤销功能(以执行前校验 + 确认弹窗兜底)
- 无批量交换、无重新解析
- 单窗口单计划,不支持多计划并行