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.
Files changed (54) hide show
  1. retrolibx-0.1.0/.github/workflows/ci.yml +41 -0
  2. retrolibx-0.1.0/.github/workflows/publish.yml +76 -0
  3. retrolibx-0.1.0/.gitignore +10 -0
  4. retrolibx-0.1.0/AGENTS.md +324 -0
  5. retrolibx-0.1.0/CHANGELOG.md +73 -0
  6. retrolibx-0.1.0/LICENSE +21 -0
  7. retrolibx-0.1.0/PKG-INFO +99 -0
  8. retrolibx-0.1.0/README.md +83 -0
  9. retrolibx-0.1.0/assets/logo.png +0 -0
  10. retrolibx-0.1.0/assets/repo-card.png +0 -0
  11. retrolibx-0.1.0/pyproject.toml +59 -0
  12. retrolibx-0.1.0/specs/v1/design.md +398 -0
  13. retrolibx-0.1.0/specs/v1/requirements.md +140 -0
  14. retrolibx-0.1.0/specs/v1/tasks.md +38 -0
  15. retrolibx-0.1.0/src/retrolibx/__init__.py +3 -0
  16. retrolibx-0.1.0/src/retrolibx/adapters/__init__.py +5 -0
  17. retrolibx-0.1.0/src/retrolibx/adapters/base.py +49 -0
  18. retrolibx-0.1.0/src/retrolibx/adapters/emulationstation/__init__.py +3 -0
  19. retrolibx-0.1.0/src/retrolibx/adapters/emulationstation/adapter.py +136 -0
  20. retrolibx-0.1.0/src/retrolibx/adapters/emulationstation/gamelist.py +174 -0
  21. retrolibx-0.1.0/src/retrolibx/adapters/esde/__init__.py +3 -0
  22. retrolibx-0.1.0/src/retrolibx/adapters/esde/adapter.py +28 -0
  23. retrolibx-0.1.0/src/retrolibx/adapters/pegasus/__init__.py +3 -0
  24. retrolibx-0.1.0/src/retrolibx/adapters/pegasus/adapter.py +262 -0
  25. retrolibx-0.1.0/src/retrolibx/adapters/pegasus/metadata.py +48 -0
  26. retrolibx-0.1.0/src/retrolibx/adapters/registry.py +70 -0
  27. retrolibx-0.1.0/src/retrolibx/adapters/retroarch/__init__.py +3 -0
  28. retrolibx-0.1.0/src/retrolibx/adapters/retroarch/adapter.py +208 -0
  29. retrolibx-0.1.0/src/retrolibx/adapters/retroarch/media.py +61 -0
  30. retrolibx-0.1.0/src/retrolibx/adapters/retroarch/playlist.py +28 -0
  31. retrolibx-0.1.0/src/retrolibx/adapters/rocknix/__init__.py +3 -0
  32. retrolibx-0.1.0/src/retrolibx/adapters/rocknix/adapter.py +39 -0
  33. retrolibx-0.1.0/src/retrolibx/application/__init__.py +3 -0
  34. retrolibx-0.1.0/src/retrolibx/application/service.py +134 -0
  35. retrolibx-0.1.0/src/retrolibx/cli.py +237 -0
  36. retrolibx-0.1.0/src/retrolibx/core/__init__.py +1 -0
  37. retrolibx-0.1.0/src/retrolibx/core/executor.py +89 -0
  38. retrolibx-0.1.0/src/retrolibx/core/models.py +140 -0
  39. retrolibx-0.1.0/src/retrolibx/core/normalize.py +56 -0
  40. retrolibx-0.1.0/src/retrolibx/core/operations.py +87 -0
  41. retrolibx-0.1.0/src/retrolibx/core/options.py +40 -0
  42. retrolibx-0.1.0/src/retrolibx/core/planner.py +165 -0
  43. retrolibx-0.1.0/src/retrolibx/core/validation.py +70 -0
  44. retrolibx-0.1.0/src/retrolibx/errors.py +33 -0
  45. retrolibx-0.1.0/src/retrolibx/registry/__init__.py +5 -0
  46. retrolibx-0.1.0/src/retrolibx/registry/systems.py +69 -0
  47. retrolibx-0.1.0/src/retrolibx/registry/systems.yaml +94 -0
  48. retrolibx-0.1.0/src/retrolibx/utils/__init__.py +5 -0
  49. retrolibx-0.1.0/src/retrolibx/utils/discovery.py +93 -0
  50. retrolibx-0.1.0/tests/test_adapters.py +226 -0
  51. retrolibx-0.1.0/tests/test_models_registry.py +38 -0
  52. retrolibx-0.1.0/tests/test_planner_executor.py +102 -0
  53. retrolibx-0.1.0/tests/test_service_cli.py +69 -0
  54. 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,10 @@
1
+ .venv/
2
+ .coverage
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ __pycache__/
7
+ *.py[cod]
8
+ dist/
9
+ build/
10
+ *.egg-info/
@@ -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
+
@@ -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.
@@ -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).