
GitHub: https://github.com/rommapp/romm
RomM 是把「扫描 ROM → 12 源元数据富集 → 跨设备同步 → 浏览器内即时玩」全链路打通、跑在一个 Docker Compose 里的自托管游戏云,由 ~134 位贡献者、3 年 4 个月密集开发(10,441 commit,月均 261)支撑,已经成为开源自托管场景的「事实标准游戏元数据服务器」。

— 二维码 + 同心圆的品牌标识

— 桌面端:游戏库 + 封面 + 元数据的完整视觉语言

— 移动端:跨端同步叙事的视觉证据
架构图:FastAPI + Vue 3 全栈,浏览器内 EmulatorJS(控制台+街机)+ Ruffle(Flash)+ DOSBox(MS-DOS)三套 WebAssembly 引擎;后端通过 nginx X-Accel-Redirect 处理 ROM 文件的 Range/HEAD/CORS;Redis pub/sub 桥接 RQ worker 与 Socket.IO 主进程。详见 https://deepwiki.com/rommapp/romm 的模块依赖视图。
维度 | 数据 |
|---|---|
GitHub | https://github.com/rommapp/romm |
Star / Fork | 10,518 / 503 |
Watcher | 42 |
Open PRs / Issues | 8 / 148 |
代码行数 | 414,543(Python 22.4%、JSON 47.9%、YAML 15.9%、TypeScript 6.4%、Vue 3.0%、SVG 3.7%) |
文件数量 | 2,147 |
依赖数量 | 136 个运行时依赖(pyproject.toml) |
项目年龄 | 40 个月(2023-03-08 → 2026-07) |
License | AGPL-3.0 |
总 commit | 10,441 |
近 30 天 commit | 529(≈17.6 次/天) |
近 90 天 commit | 1,315(≈14.6 次/天) |
开发阶段 | 密集开发(毫无衰减迹象,2026-05/06 单月 466/448 接近历史新高) |
开发模式 | 职业项目(周末 21.7% + 深夜 19.8% = 41.5%,远超典型业余项目 15-20%) |
贡献模式 | 双创始人主导(gantoine 31.7% + zurdi15 ~32% ≈ 64%,Top3 占 ~70%) |
热度定位 | 大众热门(10k+ stars 级) |
最新版本 | v2.3.1(共 193 tag / 100 release) |
质量评级 | 代码优秀 / 文档优秀 / 测试充分 |
RomM 归 Organization rommapp(「The RomM Project」,2024-03 创立,账号比仓库晚约 1 年——先有项目后品牌化)。两位核心创始人 gantoine(Georges-Antoine Assi,3,315 commits)和 zurdi15(zurdi,~3,330 commits)合计占 ~64% 的提交,第三核心 adamantike(389 commits)补到三人组。bot 群(copilot-swe-agent 206 + dependabot 174 + Claude 49 = 429 commits)说明 AI coding agent 已深度融入日常开发流——与 CONTRIBUTING.md 里的 AI 强制披露规则呼应。
从生态布局推断:作者深度参与 RetroAchievements / libretro / RAHasher 生态(Hasheous/Playmatch 哈希索引接入),熟悉 Unraid / TrueNAS / Synology 自托管平台(11 个部署仓配套),命名 grout 这个 Go 掌机客户端(grout = 灌浆/填缝剂)暗喻「跨端 sync 把缝填上」。这是夫妻店 / 小工作室级全职开源商业化项目的典型节奏——双创始人 + 跨端矩阵 + 完整产品矩阵 + Open Collective 募资。
作者看到了自托管模拟游戏场景里长期存在的三段断链:
.iso/.bin/.cue/.chd/.gb/.nes/.nds/...,文件名在 No-Intro / Redump / TOSEC / GoodSets / MAME 不同 dump 命名规范间穿梭;传统方案要么只解一段,要么把全段硬塞到一个 Windows-only GUI 里。RomM 的选择是把「服务器侧的全部价值」做成一个产品:扫描/识别/补全/分发/同步/浏览器内玩都做,客户端只做「取一份清单 + 拉文件 + 收同步状态」。
代码里反复出现三个原则:
MetadataHandler 抽象(backend/handler/metadata/base_handler.py:133),扫描器只对一份「优先级序列」说话(backend/handler/scan_handler.py:104)。新增一个外部源的成本是「再加一个 adapter + 在 config 的优先级列表里加一行」。明确不做什么:不替代 libretro 模拟器核心(EmulatorJS 也是 libretro 衍生)、不替代 LaunchBox 的本地收藏体验(RomM 把 LaunchBox 当 metadata 源)、不做移动端原生游戏运行(客户端只负责拉清单和同步状态)。
marketing-site + docs + unraid-templates + Open Collective 募资 + 「Built for its users, not for shareholders」 周边文案——走的是「开源核心 + 周边托管服务」路线。v2 时代是「工具型产品」,v3/v4 已经演进到「家庭中枢」(OIDC SSO + RBAC + 多用户 + 设备 sync 协议),下一步大概率是「小型社区云」(一个家庭/LAN 群体的私有游戏云)。Issue #540(OpenID Connect)40 评论的关单 Issue 是这条演进路径的标志事件。
按新颖度 × 实用性排序:
AsyncRedisManager(write_only=True) 反向 emit:RQ worker 进程没有 Socket.IO server 实例,但通过 Redis pub/sub 把事件写回主进程转发给浏览器(backend/endpoints/sockets/scan.py:135)。这是「任何后台任务 + 实时 UI」通用解。backend/handler/metadata/ss_handler.py:46 的 add_ss_auth_to_url):ScreenScraper 媒体 URL 在存库前剥掉 ssid/sspassword(strip_sensitive_query_params),下载时再注入;hostname 白名单(_is_screenscraper_host)防 URL 篡改泄露。解决了「自托管产品用第三方凭据下载敏感资源」的标准难题。scan_handler.py:399-477),命中率从 ~70% 提到 ~95%。_id 列 + _metadata JSON 列 + 一行优先级配置。asyncio.gather(..., return_exceptions=True) 保证「一个 provider 抛错不影响其他」。SCAN_METADATA_PRIORITY 决定文本字段、SCAN_ARTWORK_PRIORITY 决定封面,二者独立。base_handler.py:318-784):400+ 平台用单一枚举表达,外部源 slug 通过 lookup table 映射——把平台识别从散落的 if-else 提到语言层。_should_scan_rom 决策表(backend/endpoints/sockets/scan.py:170)把「ROM 是否需要扫」的判定从主循环抽出。ScanType 6 种 × ROM 6 种状态 = 36 个决策点集中在一处;Redis scan:stop flag + Worker.all(...).job.cancel() 实现「中途停止 → 下次只扫上次没扫到的」。backend/handler/metadata/igdb_handler.py:57):SNES/Super Famicom、NES/Famicom 互为区域孪生,IGDB 把它们当两个平台——_build_platforms_where 自动生成 OR 条件覆盖区域版本。has_structure_path_b 切换「按平台分目录」 vs 「按内容分目录」(backend/handler/filesystem/roms_handler.py:172),迁移老库的关键。AsyncRedisManager(write_only=True) 反向 emit → Redis flag 中断 → Worker.all(...).cancel() 取消正在跑的 job。任何媒体扫描/批量导入/异步报表场景都能套。X-Accel-Redirect 处理 Range/HEAD/CORS。Content-Disposition 用 DB 记录派生而不靠 URL 参数(防 content-sniffing/XSS)。任何「在浏览器里处理大文件」场景(音视频预览、PDF 流式阅读、IDE in browser)都能借鉴。asyncio.gather(return_exceptions=True) 容错。任何多 SaaS 同步、多 RSS 抓取、多模型聚合项目都能套。backend/conftest.py:11):CI 多进程跑测试时给每个 worker 一个独立 DB(romm_test_gw0、romm_test_gw1...),避免 autouse fixture 互相踩。这个细节很难想到但极其实用。backend/endpoints/responses/ 维护专门 schema 文件(与 SQLAlchemy ORM 模型分离),前端 npm run generate 跑 openapi-typescript-codegen 生成 typed service。改 schema 不破坏前端契约。watchfiles 文件监听(RESCAN_ON_FILESYSTEM_CHANGE_DELAY 防抖)+ 定时 cron 兜底;tasks_scheduler.enqueue_in 前查 get_pending_scan_jobs() 排重。任何「目录型资源库」(相册、书库、音乐库、播客下载器)都能直接套。问题:扫描 10k+ ROM 可能耗时 30 分钟到数小时,请求线程会超时,SSE/HTTP 长轮询浏览器关闭就断。 方案:客户端 socket.emit(「scan」, ...) → 服务端推 high_prio_queue → RQ worker 在独立进程跑 → worker 通过 AsyncRedisManager(write_only=True) 反向 emit 给主进程 → 主进程 fan-out 给浏览器。三个粒度事件:scan:update_stats(计数)、scan:scanning_platform(当前平台)、scan:scanning_rom(当前 ROM 详情)。 Trade-off:Redis pub/sub 成为强制依赖;多 worker 时同一 ROM 可能被并发扫(IntegrityError 兜底);RQ worker 里跑的代码不能依赖 FastAPI request context。 可迁移性:高(任何「长任务 + 实时进度」场景)。
问题:12 个外部元数据源各有 ID 体系、字段集、rate limit、区域策略、匹配逻辑。 方案:MetadataHandler 抽象基类定义 is_enabled() / find_best_match() / _normalize_search_term();每个 provider 实现自己的 TypedDict,字段集剪到 BaseRom;扫描时 fan-out 用 asyncio.gather(..., return_exceptions=True),失败用「空 ID」 fallback 配对;Hash 优先匹配(Playmatch/Hasheous)跳过文件名误匹配;区域/语言策略在 IGDB get_igdb_preferred_locale 和 ScreenScraper add_ss_auth_to_url 各实现。 Trade-off:模型表必须为每个 provider 开一列 _id + _metadata(12 × 2 = 24 列),优先级反转 + 字段集冲突必须靠测试覆盖;平台 slug 在 12 套同时维护。 可迁移性:高(任何多 SaaS 同步/多 RSS 抓取场景)。关键约定:把「数据源 ID 体系」和「业务实体 ID」在 schema 层解耦。
问题:扫描触发有三种场景(手动、文件拖入、定时对账),不能打架,且要避免「批量拖文件时每动一个文件都触发一次扫描」。 方案:watchfiles 在 watcher.py 进程启 → 过滤 excluded patterns → 平台目录变更排 UPDATE 扫描、单文件变更排 QUICK 扫描 → tasks_scheduler.enqueue_in 前查 get_pending_scan_jobs() 排重 → ScanLibraryTask 定时 cron 用 QUICK 扫兜底。ScanType 6 种 + _should_scan_rom 决策表把判定从主循环抽出。 Trade-off:三层调度必须共享 STOP_SCAN_FLAG Redis key 协调;RESCAN_ON_FILESYSTEM_CHANGE_DELAY 默认值需要文档化;排除规则必须分层维护。 可迁移性:高(任何「目录型资源库」)。
方案:DEV_MODE 直接 FileResponse;生产走 nginx X-Accel-Redirect(backend/endpoints/roms/files.py:101 的 FileRedirectResponse),让 nginx 处理 Range/CORS。Content-Disposition 用 DB 记录派生(注释里强调「never from the client-supplied file_name path param」)。 Trade-off:nginx 成为生产部署强依赖(docker-compose 必须带 nginx 容器)。 可迁移性:高(浏览器内大文件处理的通用方案)。
维度 | RomM | LaunchBox/BigBox | Playnite | RetroArch/Lakka | Gaseous/GameVault |
|---|---|---|---|---|---|
形态 | Web + 自托管 | Windows GUI 闭源 | Win/Mac/Linux GUI 开源 | 跨平台 C/C++ 引擎 + 前端 | Web + 自托管开源 |
扫描富集 | 12 provider 优先级融合 | 内置 + 社区数据库(行业最完整) | 插件元数据 | 无 | 1-2 provider |
跨设备 sync | 多设备 OIDC + SSH + WebSocket + 文件夹 | 无 | 仅本机 | RetroArch Cloud(限定) | 无 |
浏览器内玩 | EmulatorJS + Ruffle + DOSBox | 无 | 无 | Lakka 浏览器受限 | 仅 EmulatorJS 部分 |
多用户治理 | OIDC + RBAC + 邀请链接 | 无 | 无 | 无 | 无 |
元数据 API | 完整 OpenAPI + client token | 无 | 无 | 无 | 部分 |
生态位 | Web 优先 + 多设备 sync + 多用户 | PC 本地收藏 | PC 本地收藏 + 插件 | 跨平台模拟引擎 | 轻量自托管 |
在整个自托管/游戏云生态里扮演 「事实标准游戏元数据服务器」——填补了「模拟器前端都是单机工具」的空白,把 RetroArch/LaunchBox/Playnite/IGDB/EmulatorJS 这些原本散落的工具靠一个 FastAPI + Vue 全栈项目集成到一个 Docker Compose 里。是 Self-Hosted 叙事下的标志性产品。
AsyncRedisManager(write_only=True) 反向 emit —— 任何「后台任务推 UI」项目都该用xdist 多 worker DB 隔离 —— 任何 Pytest 矩阵项目的必加 fixture*_id + *_metadata 列(12 × 2 = 24 列),未来到 20 个 provider 会到 40 列——届时可能要重构成 external_ids JSON map(牺牲索引查询)。backend/handler/scan_handler.py(扫描编排核心)、backend/endpoints/sockets/scan.py(Socket.IO 反向 emit)、backend/handler/metadata/base_handler.py(provider 抽象)docs/BACKEND_ARCHITECTURE.md(1000+ 行完整架构)、backend/conftest.py(xdist 多 worker 隔离 fixture)、backend/handler/metadata/ss_handler.py(凭据安全注入)backend/watcher.py(三层扫描触发)、backend/handler/auth/hybrid_auth.py(多认证后端)*_id 列重构成 external_ids JSON map + JSONB 索引查询(视使用频率分热/冷列)资源 | 链接 |
|---|---|
DeepWiki | https://deepwiki.com/rommapp/romm |
Zread.ai | 未收录 |
官方文档 | https://docs.romm.app/latest/ |
官网 | https://romm.app |
关联论文 | 无 |
在线 Demo | 无(必须 self-host;docker-compose 启动有完整 examples/) |