retrolibx 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.
- retrolibx-0.1.0/.github/workflows/ci.yml +41 -0
- retrolibx-0.1.0/.github/workflows/publish.yml +76 -0
- retrolibx-0.1.0/.gitignore +10 -0
- retrolibx-0.1.0/AGENTS.md +324 -0
- retrolibx-0.1.0/CHANGELOG.md +73 -0
- retrolibx-0.1.0/LICENSE +21 -0
- retrolibx-0.1.0/PKG-INFO +99 -0
- retrolibx-0.1.0/README.md +83 -0
- retrolibx-0.1.0/assets/logo.png +0 -0
- retrolibx-0.1.0/assets/repo-card.png +0 -0
- retrolibx-0.1.0/pyproject.toml +59 -0
- retrolibx-0.1.0/specs/v1/design.md +398 -0
- retrolibx-0.1.0/specs/v1/requirements.md +140 -0
- retrolibx-0.1.0/specs/v1/tasks.md +38 -0
- retrolibx-0.1.0/src/retrolibx/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/adapters/__init__.py +5 -0
- retrolibx-0.1.0/src/retrolibx/adapters/base.py +49 -0
- retrolibx-0.1.0/src/retrolibx/adapters/emulationstation/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/adapters/emulationstation/adapter.py +136 -0
- retrolibx-0.1.0/src/retrolibx/adapters/emulationstation/gamelist.py +174 -0
- retrolibx-0.1.0/src/retrolibx/adapters/esde/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/adapters/esde/adapter.py +28 -0
- retrolibx-0.1.0/src/retrolibx/adapters/pegasus/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/adapters/pegasus/adapter.py +262 -0
- retrolibx-0.1.0/src/retrolibx/adapters/pegasus/metadata.py +48 -0
- retrolibx-0.1.0/src/retrolibx/adapters/registry.py +70 -0
- retrolibx-0.1.0/src/retrolibx/adapters/retroarch/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/adapters/retroarch/adapter.py +208 -0
- retrolibx-0.1.0/src/retrolibx/adapters/retroarch/media.py +61 -0
- retrolibx-0.1.0/src/retrolibx/adapters/retroarch/playlist.py +28 -0
- retrolibx-0.1.0/src/retrolibx/adapters/rocknix/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/adapters/rocknix/adapter.py +39 -0
- retrolibx-0.1.0/src/retrolibx/application/__init__.py +3 -0
- retrolibx-0.1.0/src/retrolibx/application/service.py +134 -0
- retrolibx-0.1.0/src/retrolibx/cli.py +237 -0
- retrolibx-0.1.0/src/retrolibx/core/__init__.py +1 -0
- retrolibx-0.1.0/src/retrolibx/core/executor.py +89 -0
- retrolibx-0.1.0/src/retrolibx/core/models.py +140 -0
- retrolibx-0.1.0/src/retrolibx/core/normalize.py +56 -0
- retrolibx-0.1.0/src/retrolibx/core/operations.py +87 -0
- retrolibx-0.1.0/src/retrolibx/core/options.py +40 -0
- retrolibx-0.1.0/src/retrolibx/core/planner.py +165 -0
- retrolibx-0.1.0/src/retrolibx/core/validation.py +70 -0
- retrolibx-0.1.0/src/retrolibx/errors.py +33 -0
- retrolibx-0.1.0/src/retrolibx/registry/__init__.py +5 -0
- retrolibx-0.1.0/src/retrolibx/registry/systems.py +69 -0
- retrolibx-0.1.0/src/retrolibx/registry/systems.yaml +94 -0
- retrolibx-0.1.0/src/retrolibx/utils/__init__.py +5 -0
- retrolibx-0.1.0/src/retrolibx/utils/discovery.py +93 -0
- retrolibx-0.1.0/tests/test_adapters.py +226 -0
- retrolibx-0.1.0/tests/test_models_registry.py +38 -0
- retrolibx-0.1.0/tests/test_planner_executor.py +102 -0
- retrolibx-0.1.0/tests/test_service_cli.py +69 -0
- retrolibx-0.1.0/uv.lock +834 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: uv sync --locked
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: uv run pytest --cov=retrolibx --cov-fail-under=80
|
|
33
|
+
|
|
34
|
+
- name: Run lint and type checks
|
|
35
|
+
run: |
|
|
36
|
+
uv run ruff check .
|
|
37
|
+
uv run ruff format --check .
|
|
38
|
+
uv run mypy src
|
|
39
|
+
|
|
40
|
+
- name: Build package
|
|
41
|
+
run: uv build
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Verify and build release
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --locked
|
|
29
|
+
|
|
30
|
+
- name: Verify tag matches package version
|
|
31
|
+
run: |
|
|
32
|
+
package_version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
|
33
|
+
tag_version="${GITHUB_REF_NAME#v}"
|
|
34
|
+
test "$package_version" = "$tag_version" || {
|
|
35
|
+
echo "Tag version $tag_version does not match package version $package_version"
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
- name: Run tests
|
|
40
|
+
run: uv run pytest --cov=retrolibx --cov-fail-under=80
|
|
41
|
+
|
|
42
|
+
- name: Run lint and type checks
|
|
43
|
+
run: |
|
|
44
|
+
uv run ruff check .
|
|
45
|
+
uv run ruff format --check .
|
|
46
|
+
uv run mypy src
|
|
47
|
+
|
|
48
|
+
- name: Build release distributions
|
|
49
|
+
run: uv build
|
|
50
|
+
|
|
51
|
+
- name: Upload release distributions
|
|
52
|
+
uses: actions/upload-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: python-package-distributions
|
|
55
|
+
path: dist/
|
|
56
|
+
if-no-files-found: error
|
|
57
|
+
|
|
58
|
+
publish:
|
|
59
|
+
name: Publish release to PyPI
|
|
60
|
+
needs: build
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
environment:
|
|
63
|
+
name: pypi
|
|
64
|
+
url: https://pypi.org/project/retrolibx/
|
|
65
|
+
permissions:
|
|
66
|
+
id-token: write
|
|
67
|
+
steps:
|
|
68
|
+
- name: Download release distributions
|
|
69
|
+
uses: actions/download-artifact@v4
|
|
70
|
+
with:
|
|
71
|
+
name: python-package-distributions
|
|
72
|
+
path: dist/
|
|
73
|
+
|
|
74
|
+
- name: Publish distributions to PyPI
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
76
|
+
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## 项目概述
|
|
6
|
+
|
|
7
|
+
RetroLibX(Retro Library Exchange)是一个 Python 3.12+ 命令行工具,用于在不同复古游戏前端和游戏库格式之间迁移:
|
|
8
|
+
|
|
9
|
+
- ROM 与多文件游戏引用
|
|
10
|
+
- 游戏元数据
|
|
11
|
+
- 封面、截图、标题图、视频和手册
|
|
12
|
+
- Collection / Playlist
|
|
13
|
+
- Core、Emulator 与启动配置
|
|
14
|
+
- 收藏状态和游玩统计
|
|
15
|
+
|
|
16
|
+
V1 支持 RetroArch、通用 EmulationStation、ROCKNIX、ES-DE 和 Pegasus。
|
|
17
|
+
|
|
18
|
+
所有转换必须遵循:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
Source → RetroLibX IR → Target
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
禁止新增格式之间的点对点转换器,例如 `RetroArchToPegasusConverter`。
|
|
25
|
+
|
|
26
|
+
## 常用命令
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 安装锁定依赖
|
|
30
|
+
uv sync --locked
|
|
31
|
+
|
|
32
|
+
# 查看 CLI
|
|
33
|
+
uv run retrolibx --help
|
|
34
|
+
|
|
35
|
+
# 检测和扫描游戏仓库
|
|
36
|
+
uv run retrolibx detect /path/to/library
|
|
37
|
+
uv run retrolibx scan /path/to/library
|
|
38
|
+
uv run retrolibx scan /path/to/library --json
|
|
39
|
+
|
|
40
|
+
# 非标准 RetroArch playlist:指定游戏名字段
|
|
41
|
+
uv run retrolibx scan /path/to/library --game-name-field core_name
|
|
42
|
+
|
|
43
|
+
# 转换前预览(不得写文件)
|
|
44
|
+
uv run retrolibx convert /path/to/source \
|
|
45
|
+
--to rocknix \
|
|
46
|
+
--output /path/to/target \
|
|
47
|
+
--dry-run
|
|
48
|
+
|
|
49
|
+
# 执行转换
|
|
50
|
+
uv run retrolibx convert /path/to/source \
|
|
51
|
+
--to rocknix \
|
|
52
|
+
--output /path/to/target
|
|
53
|
+
|
|
54
|
+
# 完整验证
|
|
55
|
+
uv run pytest --cov=retrolibx --cov-fail-under=80
|
|
56
|
+
uv run ruff check .
|
|
57
|
+
uv run ruff format --check .
|
|
58
|
+
uv run mypy src
|
|
59
|
+
|
|
60
|
+
# 运行单个测试
|
|
61
|
+
uv run pytest tests/test_adapters.py::test_retroarch_import_and_render -v
|
|
62
|
+
|
|
63
|
+
# 构建 PyPI 包
|
|
64
|
+
uv build
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
若当前网络访问官方 PyPI 较慢,可仅对当前命令使用中科大镜像,不要擅自修改用户全局配置:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
UV_DEFAULT_INDEX=https://mirrors.ustc.edu.cn/pypi/simple uv sync --locked
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 核心架构约束
|
|
74
|
+
|
|
75
|
+
项目必须长期维持三个边界。
|
|
76
|
+
|
|
77
|
+
### 1. 统一 IR
|
|
78
|
+
|
|
79
|
+
`src/retrolibx/core/models.py` 定义 RLX IR:
|
|
80
|
+
|
|
81
|
+
- `Library` — 来源格式、系统、Collection、全局元数据和诊断
|
|
82
|
+
- `System` — canonical system ID、展示名和游戏列表
|
|
83
|
+
- `Game` — 游戏名、ROM、媒体、基础元数据、游玩状态和启动配置
|
|
84
|
+
- `Rom` — 文件路径、大小、hash、碟号和来源元数据
|
|
85
|
+
- `Media` — 语义化媒体字段,不保存平台特有文件夹名称
|
|
86
|
+
- `Collection` — 通过稳定 ID 引用游戏
|
|
87
|
+
- `LaunchConfig` — emulator、core、command、工作目录和参数
|
|
88
|
+
|
|
89
|
+
Adapter 之间不能互相依赖。所有来源先导入 RLX IR,再由目标 Adapter 渲染。
|
|
90
|
+
|
|
91
|
+
Pydantic 模型中的可变字段必须使用 `Field(default_factory=...)`,禁止共享列表或字典默认值。
|
|
92
|
+
|
|
93
|
+
### 2. Adapter 与 Profile 分离
|
|
94
|
+
|
|
95
|
+
`src/retrolibx/adapters/base.py` 定义 `LibraryAdapter`:
|
|
96
|
+
|
|
97
|
+
- `detect(path)` — 返回格式、0~1 置信度和识别证据
|
|
98
|
+
- `import_library(path, options)` — 来源格式转换为 RLX IR
|
|
99
|
+
- `render_library(library, target, options)` — RLX IR 转为声明式 `ExportIntent`
|
|
100
|
+
- `capabilities` — 声明目标可表达的数据能力
|
|
101
|
+
|
|
102
|
+
内置 Adapter 注册在 `src/retrolibx/adapters/registry.py`,格式名称和别名必须通过注册表解析,不要添加长 `if/elif` 分支。
|
|
103
|
+
|
|
104
|
+
格式和平台约定需要区分:
|
|
105
|
+
|
|
106
|
+
- EmulationStation Adapter 负责 `gamelist.xml` 语法
|
|
107
|
+
- ROCKNIX 复用 EmulationStation XML codec,但维护独立平台目录约定
|
|
108
|
+
- ES-DE 复用 XML 基础能力,但保持独立 Adapter、检测和媒体规则
|
|
109
|
+
|
|
110
|
+
添加新平台时优先组合共享 codec,不要复制完整 parser,也不要让通用格式承担发行版特有规则。
|
|
111
|
+
|
|
112
|
+
### 3. Plan 与 Execute 分离
|
|
113
|
+
|
|
114
|
+
写文件流程必须保持:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
Library
|
|
118
|
+
↓
|
|
119
|
+
Target Adapter → ExportIntent
|
|
120
|
+
↓
|
|
121
|
+
ConversionPlanner → ConversionPlan
|
|
122
|
+
↓
|
|
123
|
+
PlanExecutor → Target
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
关键文件:
|
|
127
|
+
|
|
128
|
+
- `core/operations.py` — ExportIntent、操作模型和执行报告
|
|
129
|
+
- `core/planner.py` — 目标路径、冲突策略、Manifest 和操作计划
|
|
130
|
+
- `core/executor.py` — 唯一允许修改目标文件系统的组件
|
|
131
|
+
|
|
132
|
+
Adapter 不得调用 `shutil.copy`、`Path.write_text` 或直接创建目标目录。`--dry-run` 必须只创建计划,不能产生任何目标文件。
|
|
133
|
+
|
|
134
|
+
## 仓库与文件发现
|
|
135
|
+
|
|
136
|
+
`src/retrolibx/utils/discovery.py` 提供统一递归发现和文件索引。
|
|
137
|
+
|
|
138
|
+
元数据从用户给定的来源根目录递归查找:
|
|
139
|
+
|
|
140
|
+
- `*.lpl`
|
|
141
|
+
- `gamelist.xml`
|
|
142
|
+
- `metadata.pegasus.txt`
|
|
143
|
+
|
|
144
|
+
忽略 `.git`、`.retrolibx`、`.venv` 和 `__pycache__`。不要重新引入只扫描直属目录或固定一层目录的限制。
|
|
145
|
+
|
|
146
|
+
ROM 和媒体路径按以下顺序解析:
|
|
147
|
+
|
|
148
|
+
1. 已存在的绝对路径
|
|
149
|
+
2. 相对元数据目录或已识别前端根目录
|
|
150
|
+
3. 相对用户传入的仓库根目录
|
|
151
|
+
4. 最长路径尾部唯一匹配,用于处理 `/storage/roms` 等失效设备根路径
|
|
152
|
+
5. 唯一文件名匹配,并结合 ROM、封面、截图、视频等语义目录提示
|
|
153
|
+
|
|
154
|
+
多个候选仍有歧义时不得猜测,应保留未解析引用,让 validation 输出诊断。
|
|
155
|
+
|
|
156
|
+
内部成功解析的路径统一保存为绝对 `Path`,但来源元数据可以使用绝对路径、相对路径或已失效的设备路径。
|
|
157
|
+
|
|
158
|
+
## RetroArch
|
|
159
|
+
|
|
160
|
+
实现位于 `src/retrolibx/adapters/retroarch/`:
|
|
161
|
+
|
|
162
|
+
- `playlist.py` — `.lpl` JSON codec
|
|
163
|
+
- `media.py` — `Named_Boxarts`、`Named_Snaps`、`Named_Titles` 解析
|
|
164
|
+
- `adapter.py` — detection、IR 映射和导出意图
|
|
165
|
+
|
|
166
|
+
标准字段映射:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
label → Game.name
|
|
170
|
+
path → Rom.path
|
|
171
|
+
core_name → LaunchConfig.core
|
|
172
|
+
core_path → LaunchConfig.metadata["core_path"]
|
|
173
|
+
crc32 → Rom.crc32
|
|
174
|
+
db_name → source_metadata
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
部分第三方 playlist 会把游戏名错误存入 `core_name`。`ImportOptions.game_name_field` 和 CLI `--game-name-field` 允许用户指定来源字段:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv run retrolibx scan SOURCE --game-name-field core_name
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
注意:自定义游戏名字段只改变 `Game.name`。缩略图仍使用原始 `label` 匹配,否则 `GBA 225.png` 等资源会失配。原始资源标签和所选字段保存在 `source_metadata["retroarch"]`。
|
|
184
|
+
|
|
185
|
+
## EmulationStation、ROCKNIX 与 ES-DE
|
|
186
|
+
|
|
187
|
+
`src/retrolibx/adapters/emulationstation/gamelist.py` 是共享 XML codec。
|
|
188
|
+
|
|
189
|
+
XML 解析必须:
|
|
190
|
+
|
|
191
|
+
- 禁止网络访问
|
|
192
|
+
- 禁止 DTD 加载和外部实体解析
|
|
193
|
+
- 保持确定性的元素顺序和 UTF-8 输出
|
|
194
|
+
- 未知来源字段保留到 namespaced `source_metadata`,但未经 allowlist 不写回标准 XML
|
|
195
|
+
|
|
196
|
+
通用 EmulationStation 不应隐含 ROCKNIX 或 ES-DE 的固定目录规则。
|
|
197
|
+
|
|
198
|
+
## Pegasus
|
|
199
|
+
|
|
200
|
+
`src/retrolibx/adapters/pegasus/metadata.py` 负责 stanza 格式:
|
|
201
|
+
|
|
202
|
+
- 支持重复字段
|
|
203
|
+
- 支持缩进续行
|
|
204
|
+
- Collection 默认值和 game 字段分开处理
|
|
205
|
+
- 输出字段及游戏排序必须确定
|
|
206
|
+
|
|
207
|
+
`adapter.py` 负责系统映射、ROM、媒体、基础元数据和 launch command 转换。
|
|
208
|
+
|
|
209
|
+
## System Registry
|
|
210
|
+
|
|
211
|
+
`src/retrolibx/registry/systems.yaml` 是 canonical system ID 的唯一配置来源。Adapter 不应硬编码平台别名和目录映射。
|
|
212
|
+
|
|
213
|
+
每条系统配置包含:
|
|
214
|
+
|
|
215
|
+
- canonical ID 和显示名
|
|
216
|
+
- aliases
|
|
217
|
+
- ROM extensions
|
|
218
|
+
- RetroArch playlist 名称
|
|
219
|
+
- ROCKNIX / ES-DE 目录
|
|
220
|
+
- Pegasus shortname
|
|
221
|
+
|
|
222
|
+
新增系统时同时添加 registry 测试,特别关注别名冲突。含糊别名必须报错,不能静默选择第一个结果。
|
|
223
|
+
|
|
224
|
+
## 安全原则
|
|
225
|
+
|
|
226
|
+
- 默认不删除、覆盖或修改来源库
|
|
227
|
+
- source 与 target 相同默认拒绝,除非显式、安全地使用 `--in-place`
|
|
228
|
+
- 所有目标路径必须限制在 target root 内,并在计划和执行阶段重复验证
|
|
229
|
+
- 冲突必须在执行前按 `skip`、`overwrite`、`rename`、`error` 或 `newer` 解析
|
|
230
|
+
- 元数据和 Manifest 使用临时同级文件后原子替换
|
|
231
|
+
- Manifest 最后写入,用于表示受管导出已经完成
|
|
232
|
+
- 导入的 launch command 只作为数据保存,绝不执行
|
|
233
|
+
- 单个游戏的解析错误尽量转为带上下文的 `Diagnostic`,不要无条件中止整个库
|
|
234
|
+
|
|
235
|
+
## CLI 与错误处理
|
|
236
|
+
|
|
237
|
+
入口为 `retrolibx.cli:run`,命令包括:
|
|
238
|
+
|
|
239
|
+
- `detect`
|
|
240
|
+
- `scan`
|
|
241
|
+
- `convert`
|
|
242
|
+
- `inspect`
|
|
243
|
+
- `validate`
|
|
244
|
+
|
|
245
|
+
人类输出使用 Rich,`--json` 输出可机器读取的结构。用户来源文本不得作为 Rich markup 直接解释。
|
|
246
|
+
|
|
247
|
+
预期业务错误继承 `RetroLibXError`,CLI 映射稳定退出码:
|
|
248
|
+
|
|
249
|
+
- `1` — validation error 或部分执行失败
|
|
250
|
+
- `2` — CLI 用法错误
|
|
251
|
+
- `3` — detection / parse failure
|
|
252
|
+
- `4` — 不安全计划或未解决冲突
|
|
253
|
+
|
|
254
|
+
除非启用 debug,不应向普通用户显示 traceback。
|
|
255
|
+
|
|
256
|
+
## 测试
|
|
257
|
+
|
|
258
|
+
测试目录:
|
|
259
|
+
|
|
260
|
+
- `tests/test_models_registry.py` — IR、normalization 和 system registry
|
|
261
|
+
- `tests/test_planner_executor.py` — dry-run、安全边界、冲突和文件模式
|
|
262
|
+
- `tests/test_adapters.py` — 五种 Adapter、递归发现、路径回退和自定义字段
|
|
263
|
+
- `tests/test_service_cli.py` — application service、CLI 和端到端转换
|
|
264
|
+
|
|
265
|
+
每次变更至少运行与修改范围相关的测试;提交或发布前必须运行完整验证。覆盖率门槛是 80%。
|
|
266
|
+
|
|
267
|
+
涉及 Adapter 时,应至少覆盖:
|
|
268
|
+
|
|
269
|
+
- detection
|
|
270
|
+
- Source → RLX IR
|
|
271
|
+
- RLX IR → ExportIntent
|
|
272
|
+
- 非标准或缺失字段
|
|
273
|
+
- 失效绝对路径和相对路径
|
|
274
|
+
- 同格式语义 round-trip(适用时)
|
|
275
|
+
- 确定性输出或 golden file
|
|
276
|
+
|
|
277
|
+
涉及 Planner/Executor 时必须证明 dry-run 零写入、来源安全和路径不可逃逸。
|
|
278
|
+
|
|
279
|
+
## 发布流程
|
|
280
|
+
|
|
281
|
+
`.github/workflows/ci.yml` 在 main push 和 PR 上运行:
|
|
282
|
+
|
|
283
|
+
- pytest + coverage ≥ 80%
|
|
284
|
+
- Ruff lint 与格式检查
|
|
285
|
+
- strict mypy
|
|
286
|
+
- `uv build` 构建验证(该产物不作为正式发布包)
|
|
287
|
+
|
|
288
|
+
`.github/workflows/publish.yml` 仅在 `v*` tag 上运行:
|
|
289
|
+
|
|
290
|
+
- 校验 tag 与 `pyproject.toml` 版本一致
|
|
291
|
+
- 对 tag 指向的源码重新运行测试、Ruff、格式和 strict mypy
|
|
292
|
+
- 对 tag 指向的源码重新构建正式 wheel 和 sdist
|
|
293
|
+
- 通过 Artifact 将正式构建产物传递给独立 publish job
|
|
294
|
+
- 通过 PyPI Trusted Publishing 发布
|
|
295
|
+
|
|
296
|
+
每次 release:
|
|
297
|
+
|
|
298
|
+
1. 更新 `pyproject.toml` 中的 `project.version`
|
|
299
|
+
2. 将 `CHANGELOG.md` 的用户可见变更从 `Unreleased` 移入带 ISO 日期的版本章节
|
|
300
|
+
3. 运行完整验证和 `uv build`
|
|
301
|
+
4. 提交版本号和 changelog:`git commit -m "chore: release x.y.z"`
|
|
302
|
+
5. 创建并推送匹配标签:`git tag vx.y.z && git push origin vx.y.z`
|
|
303
|
+
|
|
304
|
+
工作流会校验 tag 去掉 `v` 后必须与包版本完全一致。Tag 必须指向包含版本号更新的 commit。
|
|
305
|
+
|
|
306
|
+
PyPI Trusted Publisher 配置应使用:
|
|
307
|
+
|
|
308
|
+
```text
|
|
309
|
+
Owner: tiancheng91
|
|
310
|
+
Repository: RetroLibX
|
|
311
|
+
Workflow: publish.yml
|
|
312
|
+
Environment: pypi
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## 设计与任务文档
|
|
316
|
+
|
|
317
|
+
V1 的需求、技术方案和完成记录位于:
|
|
318
|
+
|
|
319
|
+
- `CHANGELOG.md` — 遵循 Keep a Changelog,记录用户可见版本变化
|
|
320
|
+
- `specs/v1/requirements.md`
|
|
321
|
+
- `specs/v1/design.md`
|
|
322
|
+
- `specs/v1/tasks.md`
|
|
323
|
+
|
|
324
|
+
架构或范围发生实质变化时同步更新这些文档,不要只修改代码。
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to RetroLibX will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Introduced the typed RetroLibX intermediate representation (RLX IR) for libraries, systems,
|
|
15
|
+
games, multi-file ROMs, semantic media, collections, launch configuration, and diagnostics.
|
|
16
|
+
- Added detection, import, and export support for RetroArch, generic EmulationStation, ROCKNIX,
|
|
17
|
+
ES-DE, and Pegasus libraries.
|
|
18
|
+
- Added the `detect`, `scan`, `convert`, `inspect`, and `validate` CLI commands with Rich output
|
|
19
|
+
and machine-readable JSON modes.
|
|
20
|
+
- Added a versioned canonical system registry for platform aliases, ROM extensions, playlist
|
|
21
|
+
names, target directories, and Pegasus short names.
|
|
22
|
+
- Added recursive discovery for `.lpl`, `gamelist.xml`, and `metadata.pegasus.txt` files across
|
|
23
|
+
repositories with non-standard directory layouts.
|
|
24
|
+
- Added repository-wide ROM and media resolution using direct paths, metadata-relative paths,
|
|
25
|
+
repository-relative paths, trailing-path matching, and unambiguous filename fallback.
|
|
26
|
+
- Added `--game-name-field` to read game titles from non-standard RetroArch playlist fields while
|
|
27
|
+
retaining the original `label` for thumbnail matching.
|
|
28
|
+
- Added conversion planning with dry-run reports, conflict policies (`skip`, `overwrite`,
|
|
29
|
+
`rename`, `error`, and `newer`), and configurable ROM and media transfer modes.
|
|
30
|
+
- Added versioned `.retrolibx/manifest.json` output containing conversion provenance and selected
|
|
31
|
+
file policies.
|
|
32
|
+
- Added adapter capability declarations and data-loss diagnostics when a target format cannot
|
|
33
|
+
represent source metadata.
|
|
34
|
+
- Added opt-in CRC32, MD5, and SHA-1 ROM hashing with `scan --hash`.
|
|
35
|
+
- Added unit, adapter, round-trip, safety, CLI, and end-to-end tests with an 80% coverage gate.
|
|
36
|
+
- Added GitHub Actions checks for pytest, Ruff, formatting, strict mypy, and package builds.
|
|
37
|
+
- Added trusted PyPI publishing for matching `v*` tags, including tag/package version validation.
|
|
38
|
+
- Added project artwork, repository guidance for coding agents, and MIT licensing.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Separated format adapters from platform profiles so ROCKNIX and ES-DE can reuse
|
|
43
|
+
EmulationStation XML primitives without inheriting platform-specific behavior.
|
|
44
|
+
- Separated adapter rendering, conversion planning, and filesystem execution so adapters remain
|
|
45
|
+
side-effect free and dry-run never writes target files.
|
|
46
|
+
- Standardized resolved IR paths as absolute `Path` values without requiring source metadata to
|
|
47
|
+
use absolute or currently valid device paths.
|
|
48
|
+
- Made metadata writers deterministic through stable system, game, field, and path ordering.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Fixed RetroArch detection when `retroarch/playlists` is nested below a game collection root.
|
|
53
|
+
- Fixed ROM lookup for playlists containing stale device paths such as `/storage/roms/...` or
|
|
54
|
+
`/ROM/...` when the files exist elsewhere in the supplied repository.
|
|
55
|
+
- Fixed thumbnail lookup for repositories that store artwork outside the conventional RetroArch
|
|
56
|
+
directory while preserving semantic distinctions between box art, screenshots, and titles.
|
|
57
|
+
- Fixed Hatch package configuration so wheel and source distributions include the complete Python
|
|
58
|
+
package as well as the system registry and license.
|
|
59
|
+
|
|
60
|
+
### Security
|
|
61
|
+
|
|
62
|
+
- Kept source libraries read-only by default and rejected source/target path equality unless
|
|
63
|
+
explicitly allowed.
|
|
64
|
+
- Constrained every planned and executed destination to the selected target root and rechecked
|
|
65
|
+
filesystem state immediately before writes.
|
|
66
|
+
- Disabled XML network access, DTD loading, and external entity resolution.
|
|
67
|
+
- Used atomic metadata writes and wrote the RetroLibX manifest only after preceding operations
|
|
68
|
+
completed.
|
|
69
|
+
- Treated imported launch commands as metadata only; RetroLibX never executes them.
|
|
70
|
+
|
|
71
|
+
[Unreleased]: https://github.com/tiancheng91/RetroLibX/compare/v0.1.0...HEAD
|
|
72
|
+
[0.1.0]: https://github.com/tiancheng91/RetroLibX/releases/tag/v0.1.0
|
|
73
|
+
|
retrolibx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tiancheng91
|
|
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.
|
retrolibx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: retrolibx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Retro Game Library Converter
|
|
5
|
+
Author: RetroLibX contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: lxml>=5.3
|
|
10
|
+
Requires-Dist: platformdirs>=4.3
|
|
11
|
+
Requires-Dist: pydantic>=2.10
|
|
12
|
+
Requires-Dist: pyyaml>=6.0
|
|
13
|
+
Requires-Dist: rich>=13.9
|
|
14
|
+
Requires-Dist: typer>=0.15
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/tiancheng91/RetroLibX/main/assets/repo-card.png" alt="RetroLibX — Universal Retro Game Library Converter" width="100%">
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://github.com/tiancheng91/RetroLibX/actions/workflows/ci.yml"><img src="https://github.com/tiancheng91/RetroLibX/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
23
|
+
<a href="https://github.com/tiancheng91/RetroLibX/actions/workflows/publish.yml"><img src="https://github.com/tiancheng91/RetroLibX/actions/workflows/publish.yml/badge.svg" alt="Publish"></a>
|
|
24
|
+
<a href="https://pypi.org/project/retrolibx/"><img src="https://img.shields.io/pypi/v/retrolibx?label=pypi&color=blue" alt="PyPI version"></a>
|
|
25
|
+
<a href="https://pypi.org/project/retrolibx/"><img src="https://img.shields.io/pypi/pyversions/retrolibx" alt="Python versions"></a>
|
|
26
|
+
<a href="https://github.com/tiancheng91/RetroLibX/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/retrolibx" alt="License"></a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# RetroLibX
|
|
32
|
+
|
|
33
|
+
**Universal Retro Game Library Converter**
|
|
34
|
+
|
|
35
|
+
RetroLibX migrates ROM references, metadata, artwork, videos, collections, and launch settings between RetroArch, generic EmulationStation, ROCKNIX, ES-DE, and Pegasus. All conversions pass through a typed, platform-neutral intermediate representation rather than point-to-point converters.
|
|
36
|
+
|
|
37
|
+
## Install and run
|
|
38
|
+
|
|
39
|
+
Python 3.12+ and [uv](https://docs.astral.sh/uv/) are required.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv sync
|
|
43
|
+
uv run retrolibx detect /path/to/library
|
|
44
|
+
uv run retrolibx scan /path/to/library
|
|
45
|
+
uv run retrolibx convert /path/to/source --to rocknix --output /path/to/target --dry-run
|
|
46
|
+
uv run retrolibx convert /path/to/source --to rocknix --output /path/to/target
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For a non-standard RetroArch playlist whose title is stored in another field, select it
|
|
50
|
+
explicitly. The original `label` is still used to match thumbnails:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run retrolibx scan /path/to/library --game-name-field core_name
|
|
54
|
+
uv run retrolibx convert /path/to/library --to rocknix --output /path/to/target \
|
|
55
|
+
--game-name-field core_name
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The source is read-only by default. ROM modes are `copy`, `move`, `symlink` (`link` alias), `hardlink`, and `none`; media modes are `copy`, `symlink`, and `hardlink`. Conflict policies are `skip`, `overwrite`, `rename`, `error`, and `newer`.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
- `detect`: rank supported source formats.
|
|
63
|
+
- `scan`: import and summarize a library; `--json` outputs RLX IR and `--hash` calculates ROM hashes.
|
|
64
|
+
- `convert`: plan and execute a conversion; `--dry-run` never writes.
|
|
65
|
+
- `inspect`: show systems, games, ROMs, and media.
|
|
66
|
+
- `validate`: report broken paths, unknown systems, duplicates, and malformed metadata.
|
|
67
|
+
|
|
68
|
+
Aliases: `ra` → `retroarch`, `es` → `emulationstation`, `esde` → `es-de`.
|
|
69
|
+
|
|
70
|
+
## Repository discovery
|
|
71
|
+
|
|
72
|
+
RetroLibX does not require one fixed repository layout. It recursively discovers `.lpl`,
|
|
73
|
+
`gamelist.xml`, and `metadata.pegasus.txt` below the supplied source root (excluding tool and
|
|
74
|
+
VCS directories). Referenced ROM and media paths are resolved in this order:
|
|
75
|
+
|
|
76
|
+
1. an existing absolute path;
|
|
77
|
+
2. a path relative to the metadata file or detected frontend root;
|
|
78
|
+
3. a path relative to the supplied repository root;
|
|
79
|
+
4. a unique trailing-path match, which handles stale device roots such as `/storage/roms`;
|
|
80
|
+
5. a unique filename match, with semantic directory hints for ROMs, covers, screenshots,
|
|
81
|
+
videos, and manuals.
|
|
82
|
+
|
|
83
|
+
Ambiguous filename matches are intentionally left unresolved and reported by validation instead
|
|
84
|
+
of silently selecting the wrong game or artwork.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run pytest --cov
|
|
90
|
+
uv run ruff check .
|
|
91
|
+
uv run ruff format --check .
|
|
92
|
+
uv run mypy src
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Architecture and acceptance criteria are documented in [`specs/v1`](specs/v1). Adapters implement detection/import/render only. Rendering returns an `ExportIntent`; the planner resolves all conflicts and paths; the executor is the sole filesystem writer.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
RetroLibX is released under the [MIT License](LICENSE).
|