renomate 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- renomate-0.1.0/.gitignore +29 -0
- renomate-0.1.0/CHANGELOG.md +50 -0
- renomate-0.1.0/LICENSE +31 -0
- renomate-0.1.0/PKG-INFO +279 -0
- renomate-0.1.0/README.md +242 -0
- renomate-0.1.0/pyproject.toml +113 -0
- renomate-0.1.0/src/renomate/__init__.py +7 -0
- renomate-0.1.0/src/renomate/chain.py +187 -0
- renomate-0.1.0/src/renomate/cli.py +540 -0
- renomate-0.1.0/src/renomate/config.py +461 -0
- renomate-0.1.0/src/renomate/country.py +52 -0
- renomate-0.1.0/src/renomate/data/country_aliases.toml +78 -0
- renomate-0.1.0/src/renomate/executor.py +287 -0
- renomate-0.1.0/src/renomate/journal.py +378 -0
- renomate-0.1.0/src/renomate/models.py +117 -0
- renomate-0.1.0/src/renomate/plan.py +430 -0
- renomate-0.1.0/src/renomate/platform/__init__.py +5 -0
- renomate-0.1.0/src/renomate/platform/fs.py +168 -0
- renomate-0.1.0/src/renomate/profiles.py +84 -0
- renomate-0.1.0/src/renomate/providers/__init__.py +75 -0
- renomate-0.1.0/src/renomate/providers/audiotag.py +203 -0
- renomate-0.1.0/src/renomate/providers/filename.py +150 -0
- renomate-0.1.0/src/renomate/providers/manual.py +55 -0
- renomate-0.1.0/src/renomate/providers/musicbrainz.py +177 -0
- renomate-0.1.0/src/renomate/providers/path.py +39 -0
- renomate-0.1.0/src/renomate/py.typed +0 -0
- renomate-0.1.0/src/renomate/report.py +287 -0
- renomate-0.1.0/src/renomate/safety.py +252 -0
- renomate-0.1.0/src/renomate/scanner.py +142 -0
- renomate-0.1.0/src/renomate/template.py +319 -0
- renomate-0.1.0/src/renomate/tui/__init__.py +5 -0
- renomate-0.1.0/src/renomate/tui/app.py +268 -0
- renomate-0.1.0/src/renomate/tui/screens.py +170 -0
- renomate-0.1.0/src/renomate/tui/styles.tcss +107 -0
- renomate-0.1.0/src/renomate/tui/widgets.py +111 -0
- renomate-0.1.0/tests/__init__.py +0 -0
- renomate-0.1.0/tests/conftest.py +155 -0
- renomate-0.1.0/tests/test_architecture.py +82 -0
- renomate-0.1.0/tests/test_audiotag.py +233 -0
- renomate-0.1.0/tests/test_chain.py +342 -0
- renomate-0.1.0/tests/test_cli.py +290 -0
- renomate-0.1.0/tests/test_config.py +226 -0
- renomate-0.1.0/tests/test_country.py +52 -0
- renomate-0.1.0/tests/test_e2e.py +223 -0
- renomate-0.1.0/tests/test_executor.py +410 -0
- renomate-0.1.0/tests/test_filename.py +152 -0
- renomate-0.1.0/tests/test_journal_undo.py +436 -0
- renomate-0.1.0/tests/test_musicbrainz.py +213 -0
- renomate-0.1.0/tests/test_plan.py +455 -0
- renomate-0.1.0/tests/test_platform_fs.py +128 -0
- renomate-0.1.0/tests/test_profiles.py +103 -0
- renomate-0.1.0/tests/test_report.py +191 -0
- renomate-0.1.0/tests/test_safety.py +220 -0
- renomate-0.1.0/tests/test_scanner.py +146 -0
- renomate-0.1.0/tests/test_template.py +228 -0
- renomate-0.1.0/tests/test_tui.py +318 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# 测试与缓存
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
coverage.xml
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# 编辑器 / 系统
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# 不要把用户的音乐库或日志提交上来
|
|
27
|
+
journals/
|
|
28
|
+
*.log
|
|
29
|
+
plan.json
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 格式,
|
|
4
|
+
提交信息遵循 [Conventional Commits](https://www.conventionalcommits.org/)。
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-10-09
|
|
9
|
+
|
|
10
|
+
> 首个上架 PyPI 的版本(Trusted Publisher / OIDC 零密钥发布)。
|
|
11
|
+
|
|
12
|
+
首个可用版本:音乐库批量重命名 + TUI + 一键回退。
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **核心数据模型**:`FieldValue`(值 / 来源 / 置信度 / 锁定)、`MetadataBundle`、
|
|
17
|
+
`RenameItem`、`Plan`(含状态统计与 JSON 序列化)。
|
|
18
|
+
- **配置**:TOML 配置(XDG 路径)+ 内置默认值;`[profiles]` / `[presets]` / `[sanitize]` /
|
|
19
|
+
`[safety]` / `[musicbrainz]` / `[defaults]` 全部分段可配。
|
|
20
|
+
- **扫描**:递归扫描 + 闸门 1(路径黑名单)+ 闸门 2(类型与属性白名单),
|
|
21
|
+
每个被跳过的文件都带人类可读原因。
|
|
22
|
+
- **元数据 Provider 链**:`manual → audiotag(mutagen) → path → filename → network → default`,
|
|
23
|
+
先到先得 + 高置信度可升级 + `locked` 保护。
|
|
24
|
+
- `audiotag`:`mutagen` 可选依赖 + 运行时动态 import,缺失时自动降级。
|
|
25
|
+
- `filename`:6 条解析模式 + 噪声清洗(站点水印 / 音质标记)。
|
|
26
|
+
- `network`:MusicBrainz **默认关闭**,限流 1 req/s + 本地 30 天缓存。
|
|
27
|
+
- **模板引擎**:`{field}` / `{field|filter:arg}`,过滤器
|
|
28
|
+
`upper` `lower` `title_case` `pad` `strip_feat` `default`;逐字段 sanitize、空折叠、长度截断。
|
|
29
|
+
- **计划体检**:`no_op` / `empty_name` / `missing:*` / `path_too_long` / `name_too_long` /
|
|
30
|
+
`cross_device` / 冲突自动加 ` (2)`…` (99)`,大小写不敏感冲突单独识别。
|
|
31
|
+
- **执行**:两阶段 rename(防链式覆盖)、大小写安全 rename(Windows / `/mnt/c`)、
|
|
32
|
+
逐条 try/except + 失败回滚,残留临时文件可被 `renomate recover` 清理。
|
|
33
|
+
- **回退**:append-only JSONL 事务日志(`flush` + `fsync`),`undo --last/--session/--only`,
|
|
34
|
+
回退前校验 `size` + `mtime_ns`,不一致标 `needs_manual`。
|
|
35
|
+
- **CLI**:`scan` / `preview` / `apply` / `undo` / `journals` / `recover` / `report` /
|
|
36
|
+
`config` / `profiles` / `tui`,默认 dry-run,退出码 `0/1/2/3/130`。
|
|
37
|
+
- **TUI(Textual)**:预览表格、模板编辑、预设切换、勾选/全选/反选、告警区、
|
|
38
|
+
确认屏(输入 `APPLY`)、回退屏(`u` 键)。
|
|
39
|
+
- **测试**:pytest + monkeypatch(网络全部打桩,零真实联网),覆盖率 ≥ 93%,
|
|
40
|
+
核心模块 `safety` / `plan` / `executor` / `journal` ≥ 98%。
|
|
41
|
+
|
|
42
|
+
### Security
|
|
43
|
+
|
|
44
|
+
- 路径黑名单覆盖 `/etc` `/proc` `/mnt/c/Windows` `C:\Program Files` 等系统目录,
|
|
45
|
+
盘符/挂载根目录一律拒绝。
|
|
46
|
+
- 符号链接、隐藏文件、非白名单扩展名、只读文件默认跳过。
|
|
47
|
+
- 未显式确认前绝不写盘:CLI 需 `--yes --token APPLY`,TUI 需输入 `APPLY`。
|
|
48
|
+
|
|
49
|
+
[Unreleased]: https://github.com/tochikoma777/renomate/compare/v0.1.0...HEAD
|
|
50
|
+
[0.1.0]: https://github.com/tochikoma777/renomate/releases/tag/v0.1.0
|
renomate-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tochikoma777
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Note on optional dependencies:
|
|
26
|
+
|
|
27
|
+
`mutagen` (used for reading embedded audio tags) is distributed under the
|
|
28
|
+
GPL-2.0-or-later license. It is declared as an *optional* extra
|
|
29
|
+
(`pip install "renomate[audio]"`) and imported dynamically at runtime; this
|
|
30
|
+
project does not link against or redistribute its code, and remains MIT
|
|
31
|
+
licensed. When the `audio` extra is not installed, `mutagen` is never loaded.
|
renomate-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: renomate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe, preview-first batch file renamer with one-click undo (TUI + CLI)
|
|
5
|
+
Project-URL: Homepage, https://github.com/tochikoma777/renomate
|
|
6
|
+
Project-URL: Repository, https://github.com/tochikoma777/renomate
|
|
7
|
+
Project-URL: Issues, https://github.com/tochikoma777/renomate/issues
|
|
8
|
+
Author-email: tochikoma777 <brother.777.lei@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: batch,music,musicbrainz,rename,tui,undo
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: rich>=13.0
|
|
24
|
+
Requires-Dist: textual>=0.47
|
|
25
|
+
Provides-Extra: audio
|
|
26
|
+
Requires-Dist: musicbrainzngs>=0.7; extra == 'audio'
|
|
27
|
+
Requires-Dist: mutagen>=1.47; extra == 'audio'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: musicbrainzngs>=0.7; extra == 'dev'
|
|
30
|
+
Requires-Dist: mutagen>=1.47; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Renomate
|
|
39
|
+
|
|
40
|
+
> **先给方案、你点头、才动手、随时一键后悔** 的安全批量重命名工具(TUI + CLI)。
|
|
41
|
+
|
|
42
|
+
首个场景是音乐库:把 `01 稻香.mp3`、`周杰伦 - 稻香 [320K].mp3` 这类乱七八糟的名字,
|
|
43
|
+
统一成 `稻香 - 周杰伦 - 魔杰座 - CN.mp3`。
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+

|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 1. 安全承诺(不可妥协的四条)
|
|
52
|
+
|
|
53
|
+
| # | 承诺 | 落地方式 |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| 1 | **默认 dry-run**:任何写操作都要显式 `--yes` | `apply` 默认只打印计划,不动文件 |
|
|
56
|
+
| 2 | **路径黑名单**:系统目录、挂载根目录一律拒绝 | `safety.is_blocked_path`,进入目录时立即生效 |
|
|
57
|
+
| 3 | **执行前确认**:必须看见并点头 | TUI 确认屏要求输入 `APPLY`;CLI 需要 `--token APPLY` |
|
|
58
|
+
| 4 | **一键回退**:改完随时后悔 | 事务日志 `$XDG_DATA_HOME/renomate/journals/*.jsonl` + `renomate undo` |
|
|
59
|
+
|
|
60
|
+
另外:符号链接、隐藏文件、非白名单类型、只读文件**默认全部跳过**;
|
|
61
|
+
单个文件失败绝不中断整批,且会尽力回滚。
|
|
62
|
+
|
|
63
|
+
## 2. 效果预览
|
|
64
|
+
|
|
65
|
+
<!-- TODO: 放一张 TUI 的 GIF(preview 表格 → 确认屏 → 执行 → undo) -->
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
┌─ renomate v0.1.0 ── profile: audio ── /mnt/d/Music ────────────────────┐
|
|
69
|
+
│ 模板:{title} - {artist} - {album} - {country} │
|
|
70
|
+
│ [x] 01 稻香.mp3 → 稻香 - 周杰伦 - 魔杰座 - CN.mp3 │
|
|
71
|
+
│ [x] 晴天[320K].mp3 → 晴天 - 周杰伦 - 叶惠美 - CN.mp3 │
|
|
72
|
+
│ [ ] unknown.mp3 → 未知标题 - 未知歌手 - 未知专辑 - CN.mp3 ▲缺失 │
|
|
73
|
+
│ [!!] CON.mp3 → (Windows 保留名,已拦截) │
|
|
74
|
+
│ [??] 重名.mp3 → (冲突未消解,需手工指定) │
|
|
75
|
+
│ OK 2 / -- 1 / !! 1 / ?? 1 Space 勾选 Enter 执行 u 回退 q 退出 │
|
|
76
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 3. 安装
|
|
80
|
+
|
|
81
|
+
> **推荐从 PyPI 安装**;想追最新源码或参与开发可从 GitHub 安装(见下方)。
|
|
82
|
+
|
|
83
|
+
### 从 PyPI 安装(推荐)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install "renomate[audio]" # 含 mutagen 音频标签读取;不要 [audio] 也能跑(仅文件名推断)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 从源码安装(开发 / 追最新)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/tochikoma777/renomate.git
|
|
93
|
+
cd renomate
|
|
94
|
+
|
|
95
|
+
# 方式 A:用 conda(Python 3.11+)
|
|
96
|
+
conda create -n renomate python=3.12 -y
|
|
97
|
+
conda activate renomate
|
|
98
|
+
pip install -e ".[audio]" # 含 mutagen 音频标签读取;不要 [audio] 也能跑(仅文件名推断)
|
|
99
|
+
|
|
100
|
+
# 方式 B:用系统/已有的 Python 3.11+(虚拟环境)
|
|
101
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
102
|
+
pip install -e ".[audio]"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
装完验证:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
renomate --version # 应输出版本号,如 renomate 0.1.0
|
|
109
|
+
renomate --help # 应列出所有子命令
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 开发 / 跑测试
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install -e ".[dev,audio]"
|
|
116
|
+
pytest -q
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### conda 一键复现
|
|
120
|
+
|
|
121
|
+
仓库也提供 `environment.yml`:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
conda env create -f environment.yml && conda activate renomate
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> `mutagen` 是**可选依赖**且运行时动态 import:没装也能跑,只是读不出内嵌标签
|
|
128
|
+
> (自动降级为文件名/目录推断)。音乐库场景建议带 `[audio]`。
|
|
129
|
+
|
|
130
|
+
## 4. 30 秒上手
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
renomate scan ~/Music # 看看扫到了什么
|
|
134
|
+
renomate preview ~/Music --preset sorted # 只看方案,绝不改文件
|
|
135
|
+
renomate apply ~/Music --yes --token APPLY # 真正执行(默认 dry-run)
|
|
136
|
+
renomate undo --last # 后悔了,一键还原
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
TUI:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
renomate tui
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
不想用预设模板,可换成自定义模板(语法见第 5 节):
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
renomate preview ~/Music --template "{track|pad:2}. {title} - {artist}"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 5. 模板语法速查
|
|
152
|
+
|
|
153
|
+
| 写法 | 含义 |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `{title}` | 取字段值 |
|
|
156
|
+
| `{field\|filter}` | 应用过滤器 |
|
|
157
|
+
| `{field\|filter:arg}` | 带参过滤器 |
|
|
158
|
+
| `{album\|default:未知专辑}` | 字段为空时的缺省值 |
|
|
159
|
+
| `{track\|pad:2}` | 数字补零 → `01` |
|
|
160
|
+
| `{title\|upper}` / `{lower}` / `{title_case}` | 大小写转换 |
|
|
161
|
+
| `{artist\|strip_feat}` | 去掉 `feat.` / `ft.` |
|
|
162
|
+
| `{country_zh}` | 国别代码转中文(`CN` → `中国`) |
|
|
163
|
+
| `{album}/{track\|pad:2} - {title}` | `/` 表示创建子目录 |
|
|
164
|
+
|
|
165
|
+
渲染流程(顺序固定):取值 → 过滤器 → **逐字段 sanitize** → 拼接 → 空折叠
|
|
166
|
+
→ 长度截断 → 补扩展名。
|
|
167
|
+
|
|
168
|
+
内置预设:`simple` / `sorted` / `by_album` / `with_year`(`[presets]` 可自行添加)。
|
|
169
|
+
|
|
170
|
+
## 6. 安全机制详解(五道闸门)
|
|
171
|
+
|
|
172
|
+
| 闸门 | 时机 | 行为 |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| 1 路径黑名单 | 进入目录时 | `/etc`、`/mnt/c/Windows`、`C:\Program Files`、挂载根目录 → 拒绝 |
|
|
175
|
+
| 2 类型/属性白名单 | 扫描时 | 符号链接、隐藏文件、非白名单扩展名、只读文件 → 跳过并记原因 |
|
|
176
|
+
| 3 计划体检 | 生成计划时 | `no_op` / `empty_name` / `missing:*` / `path_too_long` / `name_too_long` / `cross_device` / 冲突 → 跳过或拦截 |
|
|
177
|
+
| 4 执行前确认 | 执行前 | TUI 确认屏输入 `APPLY`;CLI `--token APPLY` |
|
|
178
|
+
| 5 执行期防御 | 执行时 | 两阶段 rename、大小写安全 rename、逐条 try/except |
|
|
179
|
+
|
|
180
|
+
文件名合规化(Windows/WSL 重点):非法字符替换、控制字符删除、保留设备名
|
|
181
|
+
(`CON.txt` → `_CON.txt`)、末尾空格与点删除、NFC 归一化、`MAX_PATH=260` 与 255 字符上限校验。
|
|
182
|
+
|
|
183
|
+
## 7. 回退怎么用
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
renomate journals # 列出所有 session
|
|
187
|
+
renomate undo --last # 回退最近一次
|
|
188
|
+
renomate undo --session 20261007-153012-8f3a
|
|
189
|
+
renomate undo --last --dry-run # 只看会做什么
|
|
190
|
+
renomate undo --last --only "稻香*" # 只回退匹配的文件
|
|
191
|
+
renomate recover ~/Music # 清理崩溃残留的 .renomate_tmp_*
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
回退前会校验 `size` + `mtime_ns`:与日志不一致就标 `needs_manual` 并跳过,
|
|
195
|
+
**绝不基于猜测乱改名**。
|
|
196
|
+
|
|
197
|
+
## 8. 配置
|
|
198
|
+
|
|
199
|
+
配置文件:`$XDG_CONFIG_HOME/renomate/config.toml`(默认 `~/.config/renomate/config.toml`)。
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
renomate config --init # 生成带注释的默认配置
|
|
203
|
+
renomate config --show
|
|
204
|
+
renomate config --set general.max_batch=8000
|
|
205
|
+
renomate profiles # 列出 Profile 与字段定义
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
要点:
|
|
209
|
+
|
|
210
|
+
- `[profiles.audio]`:扩展名白名单、字段列表、`required_fields`、默认模板、Provider 顺序;
|
|
211
|
+
- `[presets]`:命名预设;
|
|
212
|
+
- `[sanitize]`:平台(`windows`/`posix`)、策略(`replace`/`drop`/`underscore`)、长度上限、替换表;
|
|
213
|
+
- `[safety]`:额外黑名单、`allow_missing_fields`、`require_confirm`、`confirm_token`;
|
|
214
|
+
- `[musicbrainz]`:**默认关闭**(`enabled = false`),开启后限流 1 req/s 且本地缓存 30 天;
|
|
215
|
+
- `[defaults]`:字段兜底值(`未知标题` / `未知歌手` / `CN` …)。
|
|
216
|
+
|
|
217
|
+
> 目录推断(`path` Provider)只对**嵌套**文件生效:躺在扫描根目录里的文件不会把
|
|
218
|
+
> `~/Downloads` 这种目录名当成艺术家。
|
|
219
|
+
|
|
220
|
+
## 9. 扩展新领域(以 PDF 为例)
|
|
221
|
+
|
|
222
|
+
1. 写 Provider(实现 `name` / `supports` / `fill`):
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from renomate.models import FieldSource, FieldValue
|
|
226
|
+
from renomate.providers import register_provider
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class PdfTagProvider:
|
|
230
|
+
name = "pdftag"
|
|
231
|
+
|
|
232
|
+
def supports(self, profile: str, path) -> bool:
|
|
233
|
+
return path.suffix.casefold() == ".pdf"
|
|
234
|
+
|
|
235
|
+
def fill(self, bundle, cfg) -> None:
|
|
236
|
+
bundle.set("title", FieldValue("论文标题", FieldSource.AUDIOTAG, 0.9))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
register_provider(PdfTagProvider())
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
2. 注册 Profile(零代码,只改配置):
|
|
243
|
+
|
|
244
|
+
```toml
|
|
245
|
+
[profiles.pdf]
|
|
246
|
+
extensions = ["pdf"]
|
|
247
|
+
fields = ["title", "author", "year"]
|
|
248
|
+
required_fields = ["title"]
|
|
249
|
+
default_template = "{title} - {author} ({year})"
|
|
250
|
+
providers = ["pdftag", "filename", "default"]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
3. 加测试:`pytest tests/test_providers_pdf.py`。核心层(`models` / `template` / `plan` /
|
|
254
|
+
`executor` / `journal`)**零改动**。
|
|
255
|
+
|
|
256
|
+
## 10. WSL / Windows 注意事项
|
|
257
|
+
|
|
258
|
+
- `/mnt/c`、`/mnt/d` 继承 Windows 的**大小写不敏感**语义:`A.mp3 → a.mp3` 必须先改到临时名,
|
|
259
|
+
本工具已实现(`rename_case_safe`)。
|
|
260
|
+
- 盘符/挂载根目录本身(`C:\`、`/mnt/c`)拒绝批量重命名。
|
|
261
|
+
- Windows 保留设备名带扩展名同样非法(`CON.txt`)。
|
|
262
|
+
- 跨设备(EXDEV)不做 move,直接 `BLOCKED: cross_device`。
|
|
263
|
+
- 完整路径按 Windows 传统 `MAX_PATH = 260` 校验。
|
|
264
|
+
|
|
265
|
+
## 11. License
|
|
266
|
+
|
|
267
|
+
本项目 **MIT**,见 [LICENSE](LICENSE)。
|
|
268
|
+
|
|
269
|
+
> 关于 `mutagen`:它是 GPL-2.0-or-later,仅作为**可选依赖** + 运行时动态 import 使用,
|
|
270
|
+
> 本项目本身不链接、不分发它的代码,仍为 MIT;不安装 `[audio]` 额外依赖时完全不会加载它。
|
|
271
|
+
|
|
272
|
+
## 12. Roadmap
|
|
273
|
+
|
|
274
|
+
- [x] **已发布到 PyPI**:`pip install "renomate[audio]"` 即可(见第 3 节)。TestPyPI 同步有预发布验证版。
|
|
275
|
+
- [ ] PDF / 照片 / 视频 Profile(架构已预留,见第 9 节)
|
|
276
|
+
- [ ] undo 支持跨目录移动(当前仅同目录重命名)
|
|
277
|
+
- [ ] 重名智能合并策略(保留更高音质版本)
|
|
278
|
+
- [ ] 模板条件段 `{album?[...]}`
|
|
279
|
+
- [ ] TUI 单元格就地编辑
|
renomate-0.1.0/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Renomate
|
|
2
|
+
|
|
3
|
+
> **先给方案、你点头、才动手、随时一键后悔** 的安全批量重命名工具(TUI + CLI)。
|
|
4
|
+
|
|
5
|
+
首个场景是音乐库:把 `01 稻香.mp3`、`周杰伦 - 稻香 [320K].mp3` 这类乱七八糟的名字,
|
|
6
|
+
统一成 `稻香 - 周杰伦 - 魔杰座 - CN.mp3`。
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. 安全承诺(不可妥协的四条)
|
|
15
|
+
|
|
16
|
+
| # | 承诺 | 落地方式 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| 1 | **默认 dry-run**:任何写操作都要显式 `--yes` | `apply` 默认只打印计划,不动文件 |
|
|
19
|
+
| 2 | **路径黑名单**:系统目录、挂载根目录一律拒绝 | `safety.is_blocked_path`,进入目录时立即生效 |
|
|
20
|
+
| 3 | **执行前确认**:必须看见并点头 | TUI 确认屏要求输入 `APPLY`;CLI 需要 `--token APPLY` |
|
|
21
|
+
| 4 | **一键回退**:改完随时后悔 | 事务日志 `$XDG_DATA_HOME/renomate/journals/*.jsonl` + `renomate undo` |
|
|
22
|
+
|
|
23
|
+
另外:符号链接、隐藏文件、非白名单类型、只读文件**默认全部跳过**;
|
|
24
|
+
单个文件失败绝不中断整批,且会尽力回滚。
|
|
25
|
+
|
|
26
|
+
## 2. 效果预览
|
|
27
|
+
|
|
28
|
+
<!-- TODO: 放一张 TUI 的 GIF(preview 表格 → 确认屏 → 执行 → undo) -->
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
┌─ renomate v0.1.0 ── profile: audio ── /mnt/d/Music ────────────────────┐
|
|
32
|
+
│ 模板:{title} - {artist} - {album} - {country} │
|
|
33
|
+
│ [x] 01 稻香.mp3 → 稻香 - 周杰伦 - 魔杰座 - CN.mp3 │
|
|
34
|
+
│ [x] 晴天[320K].mp3 → 晴天 - 周杰伦 - 叶惠美 - CN.mp3 │
|
|
35
|
+
│ [ ] unknown.mp3 → 未知标题 - 未知歌手 - 未知专辑 - CN.mp3 ▲缺失 │
|
|
36
|
+
│ [!!] CON.mp3 → (Windows 保留名,已拦截) │
|
|
37
|
+
│ [??] 重名.mp3 → (冲突未消解,需手工指定) │
|
|
38
|
+
│ OK 2 / -- 1 / !! 1 / ?? 1 Space 勾选 Enter 执行 u 回退 q 退出 │
|
|
39
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 3. 安装
|
|
43
|
+
|
|
44
|
+
> **推荐从 PyPI 安装**;想追最新源码或参与开发可从 GitHub 安装(见下方)。
|
|
45
|
+
|
|
46
|
+
### 从 PyPI 安装(推荐)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "renomate[audio]" # 含 mutagen 音频标签读取;不要 [audio] 也能跑(仅文件名推断)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 从源码安装(开发 / 追最新)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/tochikoma777/renomate.git
|
|
56
|
+
cd renomate
|
|
57
|
+
|
|
58
|
+
# 方式 A:用 conda(Python 3.11+)
|
|
59
|
+
conda create -n renomate python=3.12 -y
|
|
60
|
+
conda activate renomate
|
|
61
|
+
pip install -e ".[audio]" # 含 mutagen 音频标签读取;不要 [audio] 也能跑(仅文件名推断)
|
|
62
|
+
|
|
63
|
+
# 方式 B:用系统/已有的 Python 3.11+(虚拟环境)
|
|
64
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
65
|
+
pip install -e ".[audio]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
装完验证:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
renomate --version # 应输出版本号,如 renomate 0.1.0
|
|
72
|
+
renomate --help # 应列出所有子命令
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 开发 / 跑测试
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e ".[dev,audio]"
|
|
79
|
+
pytest -q
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### conda 一键复现
|
|
83
|
+
|
|
84
|
+
仓库也提供 `environment.yml`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
conda env create -f environment.yml && conda activate renomate
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> `mutagen` 是**可选依赖**且运行时动态 import:没装也能跑,只是读不出内嵌标签
|
|
91
|
+
> (自动降级为文件名/目录推断)。音乐库场景建议带 `[audio]`。
|
|
92
|
+
|
|
93
|
+
## 4. 30 秒上手
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
renomate scan ~/Music # 看看扫到了什么
|
|
97
|
+
renomate preview ~/Music --preset sorted # 只看方案,绝不改文件
|
|
98
|
+
renomate apply ~/Music --yes --token APPLY # 真正执行(默认 dry-run)
|
|
99
|
+
renomate undo --last # 后悔了,一键还原
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
TUI:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
renomate tui
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
不想用预设模板,可换成自定义模板(语法见第 5 节):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
renomate preview ~/Music --template "{track|pad:2}. {title} - {artist}"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 5. 模板语法速查
|
|
115
|
+
|
|
116
|
+
| 写法 | 含义 |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `{title}` | 取字段值 |
|
|
119
|
+
| `{field\|filter}` | 应用过滤器 |
|
|
120
|
+
| `{field\|filter:arg}` | 带参过滤器 |
|
|
121
|
+
| `{album\|default:未知专辑}` | 字段为空时的缺省值 |
|
|
122
|
+
| `{track\|pad:2}` | 数字补零 → `01` |
|
|
123
|
+
| `{title\|upper}` / `{lower}` / `{title_case}` | 大小写转换 |
|
|
124
|
+
| `{artist\|strip_feat}` | 去掉 `feat.` / `ft.` |
|
|
125
|
+
| `{country_zh}` | 国别代码转中文(`CN` → `中国`) |
|
|
126
|
+
| `{album}/{track\|pad:2} - {title}` | `/` 表示创建子目录 |
|
|
127
|
+
|
|
128
|
+
渲染流程(顺序固定):取值 → 过滤器 → **逐字段 sanitize** → 拼接 → 空折叠
|
|
129
|
+
→ 长度截断 → 补扩展名。
|
|
130
|
+
|
|
131
|
+
内置预设:`simple` / `sorted` / `by_album` / `with_year`(`[presets]` 可自行添加)。
|
|
132
|
+
|
|
133
|
+
## 6. 安全机制详解(五道闸门)
|
|
134
|
+
|
|
135
|
+
| 闸门 | 时机 | 行为 |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| 1 路径黑名单 | 进入目录时 | `/etc`、`/mnt/c/Windows`、`C:\Program Files`、挂载根目录 → 拒绝 |
|
|
138
|
+
| 2 类型/属性白名单 | 扫描时 | 符号链接、隐藏文件、非白名单扩展名、只读文件 → 跳过并记原因 |
|
|
139
|
+
| 3 计划体检 | 生成计划时 | `no_op` / `empty_name` / `missing:*` / `path_too_long` / `name_too_long` / `cross_device` / 冲突 → 跳过或拦截 |
|
|
140
|
+
| 4 执行前确认 | 执行前 | TUI 确认屏输入 `APPLY`;CLI `--token APPLY` |
|
|
141
|
+
| 5 执行期防御 | 执行时 | 两阶段 rename、大小写安全 rename、逐条 try/except |
|
|
142
|
+
|
|
143
|
+
文件名合规化(Windows/WSL 重点):非法字符替换、控制字符删除、保留设备名
|
|
144
|
+
(`CON.txt` → `_CON.txt`)、末尾空格与点删除、NFC 归一化、`MAX_PATH=260` 与 255 字符上限校验。
|
|
145
|
+
|
|
146
|
+
## 7. 回退怎么用
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
renomate journals # 列出所有 session
|
|
150
|
+
renomate undo --last # 回退最近一次
|
|
151
|
+
renomate undo --session 20261007-153012-8f3a
|
|
152
|
+
renomate undo --last --dry-run # 只看会做什么
|
|
153
|
+
renomate undo --last --only "稻香*" # 只回退匹配的文件
|
|
154
|
+
renomate recover ~/Music # 清理崩溃残留的 .renomate_tmp_*
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
回退前会校验 `size` + `mtime_ns`:与日志不一致就标 `needs_manual` 并跳过,
|
|
158
|
+
**绝不基于猜测乱改名**。
|
|
159
|
+
|
|
160
|
+
## 8. 配置
|
|
161
|
+
|
|
162
|
+
配置文件:`$XDG_CONFIG_HOME/renomate/config.toml`(默认 `~/.config/renomate/config.toml`)。
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
renomate config --init # 生成带注释的默认配置
|
|
166
|
+
renomate config --show
|
|
167
|
+
renomate config --set general.max_batch=8000
|
|
168
|
+
renomate profiles # 列出 Profile 与字段定义
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
要点:
|
|
172
|
+
|
|
173
|
+
- `[profiles.audio]`:扩展名白名单、字段列表、`required_fields`、默认模板、Provider 顺序;
|
|
174
|
+
- `[presets]`:命名预设;
|
|
175
|
+
- `[sanitize]`:平台(`windows`/`posix`)、策略(`replace`/`drop`/`underscore`)、长度上限、替换表;
|
|
176
|
+
- `[safety]`:额外黑名单、`allow_missing_fields`、`require_confirm`、`confirm_token`;
|
|
177
|
+
- `[musicbrainz]`:**默认关闭**(`enabled = false`),开启后限流 1 req/s 且本地缓存 30 天;
|
|
178
|
+
- `[defaults]`:字段兜底值(`未知标题` / `未知歌手` / `CN` …)。
|
|
179
|
+
|
|
180
|
+
> 目录推断(`path` Provider)只对**嵌套**文件生效:躺在扫描根目录里的文件不会把
|
|
181
|
+
> `~/Downloads` 这种目录名当成艺术家。
|
|
182
|
+
|
|
183
|
+
## 9. 扩展新领域(以 PDF 为例)
|
|
184
|
+
|
|
185
|
+
1. 写 Provider(实现 `name` / `supports` / `fill`):
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from renomate.models import FieldSource, FieldValue
|
|
189
|
+
from renomate.providers import register_provider
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class PdfTagProvider:
|
|
193
|
+
name = "pdftag"
|
|
194
|
+
|
|
195
|
+
def supports(self, profile: str, path) -> bool:
|
|
196
|
+
return path.suffix.casefold() == ".pdf"
|
|
197
|
+
|
|
198
|
+
def fill(self, bundle, cfg) -> None:
|
|
199
|
+
bundle.set("title", FieldValue("论文标题", FieldSource.AUDIOTAG, 0.9))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
register_provider(PdfTagProvider())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
2. 注册 Profile(零代码,只改配置):
|
|
206
|
+
|
|
207
|
+
```toml
|
|
208
|
+
[profiles.pdf]
|
|
209
|
+
extensions = ["pdf"]
|
|
210
|
+
fields = ["title", "author", "year"]
|
|
211
|
+
required_fields = ["title"]
|
|
212
|
+
default_template = "{title} - {author} ({year})"
|
|
213
|
+
providers = ["pdftag", "filename", "default"]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
3. 加测试:`pytest tests/test_providers_pdf.py`。核心层(`models` / `template` / `plan` /
|
|
217
|
+
`executor` / `journal`)**零改动**。
|
|
218
|
+
|
|
219
|
+
## 10. WSL / Windows 注意事项
|
|
220
|
+
|
|
221
|
+
- `/mnt/c`、`/mnt/d` 继承 Windows 的**大小写不敏感**语义:`A.mp3 → a.mp3` 必须先改到临时名,
|
|
222
|
+
本工具已实现(`rename_case_safe`)。
|
|
223
|
+
- 盘符/挂载根目录本身(`C:\`、`/mnt/c`)拒绝批量重命名。
|
|
224
|
+
- Windows 保留设备名带扩展名同样非法(`CON.txt`)。
|
|
225
|
+
- 跨设备(EXDEV)不做 move,直接 `BLOCKED: cross_device`。
|
|
226
|
+
- 完整路径按 Windows 传统 `MAX_PATH = 260` 校验。
|
|
227
|
+
|
|
228
|
+
## 11. License
|
|
229
|
+
|
|
230
|
+
本项目 **MIT**,见 [LICENSE](LICENSE)。
|
|
231
|
+
|
|
232
|
+
> 关于 `mutagen`:它是 GPL-2.0-or-later,仅作为**可选依赖** + 运行时动态 import 使用,
|
|
233
|
+
> 本项目本身不链接、不分发它的代码,仍为 MIT;不安装 `[audio]` 额外依赖时完全不会加载它。
|
|
234
|
+
|
|
235
|
+
## 12. Roadmap
|
|
236
|
+
|
|
237
|
+
- [x] **已发布到 PyPI**:`pip install "renomate[audio]"` 即可(见第 3 节)。TestPyPI 同步有预发布验证版。
|
|
238
|
+
- [ ] PDF / 照片 / 视频 Profile(架构已预留,见第 9 节)
|
|
239
|
+
- [ ] undo 支持跨目录移动(当前仅同目录重命名)
|
|
240
|
+
- [ ] 重名智能合并策略(保留更高音质版本)
|
|
241
|
+
- [ ] 模板条件段 `{album?[...]}`
|
|
242
|
+
- [ ] TUI 单元格就地编辑
|