Skip to content

feat: classify ROMs by release type (homebrew, hack, translation, ...) - #5367

Draft
gantoine wants to merge 1 commit into
masterfrom
feat/rom-release-types
Draft

gantoine wants to merge 1 commit into
masterfrom
feat/rom-release-types

Conversation

@gantoine

@gantoine gantoine commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Description

Note

Re-lands #5363 (08acaa9ea), which was reverted in #5365. It is the same change, cherry-picked onto master with Sam Dornan kept as the commit author.

First step toward better metadata for ROM hacks and homebrew: a fixed release type vocabulary, stored as a list per ROM so a release can sit in several categories at once (a color patch that is also a hack, for example). Provider integrations (Homebrew Hub, Romhack Plaza, ...) are follow-ups that will map their own categories onto this list.

Vocabulary (utils/release_types.py, ReleaseType): homebrew, hack, color_patch, translation, fan_game, total_conversion, randomizer, demake, port, prototype, utility, tech_demo, unlicensed.

Where the values come from

  • Derived from signals RomM already parses:
    • (Hack), GoodTools [h1] → hack
    • [T+Eng] and other translation tags → translation
    • (Beta 2), (Proto), (Alpha) → prototype
    • (Homebrew) → homebrew
    • ScreenScraper's hack/trad/beta dump flags, which already land in tags, map the same way.
    • (Unl) → unlicensed. The filename parser files (Unl) under regions (GoodTools lists UNL with the country codes), so regions are a source too.
  • Manual: a non-empty manual_metadata.release_types list replaces the derived types. An empty list falls back to them, the same as genres and the other manual lists.

How it is stored and kept current

  • roms.release_types is a plain JSON column, computed in Python like min_age: mapper events cover ORM writes, and update_rom recomputes it for its bulk UPDATE. update_rom now walks one _DERIVED_ROM_COLUMNS table for both min_age and release_types, instead of repeating that block.
  • The value is mirrored into roms_facets by the existing triggers, so it gets the standard filter treatment: release_types / release_types_logic (any / all / none) on GET /api/roms, filter values, smart collection criteria and URL sync (?releaseTypes=hack).
  • Migration 0155_rom_release_types: adds both columns (the roms one through the roms_columns catalog), rebuilds the facets triggers, and fills every existing row from its tags, regions and any saved manual list. Rows with no type get [], so the "none" operator never meets a NULL.

API

  • RomSchema.release_types: list[ReleaseType]
  • ManualMetadata.release_types
  • RomFiltersDict.release_types
  • Frontend types regenerated.

UI

  • A Release type section in the gallery filter drawer, with translated option labels.
  • A multi-select in Edit ROM, under Additional details.
  • A Release types row on the game overview. Its chips link to the filtered search.
  • Labels in all 18 locales.

Gallery filter URLs (all filters)

A URL that names filter values without an operator now means "any", for every multi-select filter. Before, it kept whatever operator the store last held: after a "none" filter, a game-overview chip (?genres=RPG, ?releaseTypes=hack) excluded exactly what it linked to. When the URL names no values for a filter, the stored operator is left alone, so a "none" picked before any values isn't reset when another filter changes the URL. This was raised in review; it predates this PR but affects the new chips too.

flowchart LR
    scan[Scan / edit] -->|tags, regions, manual_metadata| rom[(roms)]
    rom -->|compute_release_types: mapper event or update_rom| rt[roms.release_types]
    rt -->|AFTER INSERT/UPDATE trigger| facets[(roms_facets.release_types)]
    facets -->|release_types filter + filter values| api[GET /api/roms]
    rt -->|RomSchema.release_types| ui[Game overview / Edit ROM]
Loading

Tests

  • Backend:
    • Unit tests for the derivation.
    • Mapper-event and update_rom tests.
    • A facets mirror test.
    • Endpoint tests for the filter (including none) and for manual overrides, including invalid values being rejected.
    • A reverse/replay/fill test for 0155, including a ROM whose manual list must survive the downgrade and replay. The 0148 replay tests now replay 0155 afterwards, so the shared triggers keep mirroring release_types.
  • Frontend:
    • releaseTypes helper tests (every type has an English label).
    • Smart collection criteria serialization and summary labels.
    • useGalleryFilterUrl tests: values without an operator match any, an explicit operator applies, and a pending operator survives another filter's URL change.

Notes for reviewers

  • ScreenScraper's unl dump flag is still dropped (test_ss_handler.py pins that). Mapping it onto the Unlicensed region would feed unlicensed from hash matches too. That's left for a separate change in case the exclusion was deliberate.
  • Locally, 3 streaming/sync tests fail on my MariaDB because it rejects a 2099 TIMESTAMP. That's unrelated to this change, and CI's newer MariaDB passes them.
  • AI assistance: this PR was written with Claude Code (Claude). It designed and implemented the change, the tests and the translations, and ran the verification below. The non-English locale strings are machine translations and would benefit from a native speaker's pass.

Checklist
Please check all that apply.

  • I've tested the changes locally: trunk fmt/check, mypy, affected pytest (migrations, database handlers, ROM endpoints, release types), npm run typecheck, app unit tests, npm run build, i18n checks, and a browser pass in both themes with a scanned mock library (filter, filtered gallery, Edit ROM override + save, game overview chips)
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

Screenshots (if applicable)

Screenshots were captured during the browser pass: the filter drawer, a filtered gallery, the Edit ROM field and the game overview, in dark and light. They'll be attached here in a follow-up edit (this session can't upload images to GitHub).


Generated by Claude Code

🤖 Generated with Claude Code

Re-lands #5363 (08acaa9), reverted in #5365.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gantoine gantoine added the on-hold Pending further research or blocked by another issue label Oct 27, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on-hold Pending further research or blocked by another issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants