Files

11 KiB
Raw Permalink Blame History

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 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 等)
  • 不递归扫描子文件夹
  • 无撤销功能(以执行前校验 + 确认弹窗兜底)
  • 无批量交换、无重新解析
  • 单窗口单计划,不支持多计划并行