11 KiB
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 文件名解析算法(纯字符串操作,不用正则)
输入为不含扩展名的文件名。
步骤一 · 括号抽取(严格模式)
- 识别中文括号
()与英文括号()两类,同类才配对((a)属落单) - 栈式扫描配对;出现任何落单括号(未匹配的开或闭)→ 放弃整个抽取步骤,全串原样进入步骤二(此时注释为空)
- 每对括号:删去两侧括号符,内容
Trim()(仅标准空白,不含全角空格)作为注释素材 - 抽到多个注释素材:以单个半角空格连接
- 嵌套括号(罕见):最外层配对生效,内部括号作字面字符保留
- 已抽取的括号对从源串中移除,剩余部分进入步骤二
步骤二 · 连字符分割
- 仅认半角
-(全角-、破折号—/–等不认) - 无
-:歌手为空,整串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 重命名校验关卡(全有或全无)
输出 > 重命名 点击后先做全量校验,任一问题即弹窗列出全部问题(含行号与原因)并拒绝执行:
- 歌曲名为空
- 字段含文件名非法字符(
\ / : * ? " < > |及控制字符),或目标名以点/空格结尾(Windows 限制) - 源文件已不存在(含 XML 加载后文件丢失的情形,在此统一拦截)
- 列表中存在重复源路径(区分大小写的直接字符串比较,与合并规则一致)
- 目标名冲突:多个条目生成相同目标名;或目标与磁盘既有文件冲突(自身源文件除外)——此项保持大小写不敏感,与文件系统实际行为一致(同目录不能同时存在
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 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. 实施步骤
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)- 实现
FilenameParser+ 解析单测(覆盖落单括号、多注释、多连字符、无连字符、全半角混合、嵌套括号等边界) - 实现
RenameFormats注册表与生成逻辑 + 单测(四种组合 × 两种格式) - 实现
PlanXmlIO+ 单测(格式枚举读写、空字段、路径含空格/特殊字符、version保留) - 实现
RenameValidator+ 单测(五类问题、冲突链) - 实现 ViewModel 层(文档模型、命令、脏跟踪、合并规则)
- 实现主窗口视图(菜单栏、信息条、DataGrid 绑定、交换按钮、Lucide 图标)
- 实现
RenameExecutor、MessageBox 通用组件与确认/报告/错误/脏提示弹窗 - 「关于」对话框、窗口标题图标等收尾;全量测试通过
7. 非目标
- 不读写音频元数据标签(ID3 等)
- 不递归扫描子文件夹
- 无撤销功能(以执行前校验 + 确认弹窗兜底)
- 无批量交换、无重新解析
- 单窗口单计划,不支持多计划并行