Commit Graph

9684 Commits

Author SHA1 Message Date
Georges-Antoine Assi
cd101758fd cleanup for simplicity 2026-06-08 11:13:15 -04:00
Georges-Antoine Assi
036cf108f4 sinmplify siblings 2026-06-08 10:25:27 -04:00
nendo
cfdeab589a Derive sibling_ids from expanded siblings 2026-06-08 11:26:15 +09:00
nendo
f722d9234f Remove comment on files field 2026-06-08 10:57:54 +09:00
nendo
37f0feab8c Add opt-in files/siblings expansion to GET /api/roms 2026-06-07 16:12:21 +09:00
Zurdi
bb82cf470a Merge pull request #3489 from rommapp/copilot/fix-auth-header-handling
Handle malformed Authorization headers in HybridAuthBackend without 500s
2026-06-07 00:36:24 +02:00
copilot-swe-agent[bot]
a2775ca2b8 fix: handle malformed authorization header in hybrid auth backend
Co-authored-by: zurdi15 <34356590+zurdi15@users.noreply.github.com>
2026-06-06 22:22:41 +00:00
copilot-swe-agent[bot]
27725c26b3 Initial plan 2026-06-06 22:14:01 +00:00
Georges-Antoine Assi
56f65bb266 Merge pull request #3484 from rommapp/claude/busy-keller-gd2Ue
Fix Docker volume mounts to enable hardlink optimization
2026-06-06 10:31:39 -04:00
Georges-Antoine Assi
21f60b64aa add smol comment 2026-06-06 10:28:02 -04:00
Georges-Antoine Assi
a69f09b7f8 cleanup 2026-06-06 10:24:08 -04:00
Claude
6d533e0a25 fix(docker): declare /romm as single volume to enable asset hardlinks
PR #3388 added hardlink-based asset import/export (os.link with a
shutil.copy2 fallback) to avoid duplicating disk space. The Dockerfile
VOLUME instruction listed each /romm subdirectory (resources, library,
assets, config, sync) separately, which makes Docker create an
independent mount point for each one — even when the user bind-mounts a
single parent at /romm. Each mount point is its own st_dev, so every
cross-directory os.link() failed with EXDEV and silently fell back to a
full copy, defeating the optimization.

Declare the parent /romm directory instead so all subdirectories share a
single filesystem and hardlinks can succeed when paths reside on the same
underlying host filesystem.
2026-06-06 14:20:29 +00:00
Georges-Antoine Assi
84d4bf1235 Merge pull request #3482 from DevYukine/feat/playmatch-rate-limit
feat(playmatch): add ratelimiting
2026-06-06 10:10:22 -04:00
Georges-Antoine Assi
ec172dfeb1 use const 2026-06-06 09:58:17 -04:00
DevYukine
8006e4391d feat(playmatch): pre-emptive 4 req/s rate limiting with best-effort lookups 2026-06-06 04:16:21 +02:00
Georges-Antoine Assi
031fb57947 Merge pull request #3479 from tmgast/fix/save-device-attribution
Expose per-device save sync attribution and origin device
2026-06-05 19:37:47 -04:00
Georges-Antoine Assi
7e7368f3c2 Merge pull request #3480 from rommapp/fix/unauthenticated-asset-exposure
fix: stop serving private user assets via unauthenticated nginx static route
2026-06-05 19:20:44 -04:00
Georges-Antoine Assi
619e03ab32 fix: stop serving private user assets via unauthenticated nginx static route
The default Docker image symlinked /romm/assets into the nginx static web
root (/assets/romm/assets), where it was served by an unauthenticated
`location /assets { try_files ... }` block. /romm/assets holds private user
data (save files, save states, screenshots, avatars) that is meant to be
accessible only through the authenticated /api/raw/assets/{path} route
(Scope.ASSETS_READ). The static symlink bypassed that protection, letting any
unauthenticated caller read another user's files given a (guessable) path.
Avatar URLs leaked the hex user ID through the same static route, making path
construction straightforward.

Fix:
- Drop the /romm/assets symlink from the Docker image build and both
  entrypoint scripts; only /romm/resources (public cover art, screenshots,
  manuals) remains statically served.
- Point the frontend avatar URLs at the authenticated /api/raw/assets/ route
  instead of /assets/romm/assets/. Browser <img> loads authenticate via the
  existing session cookie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:34:30 -04:00
nendo
842bb29718 add device_id tiebreaker to device_syncs ordering 2026-06-05 20:43:14 +09:00
nendo
96534add05 address CI/CD failures 2026-06-05 20:30:14 +09:00
nendo
287c487308 feat(saves): expose per-device sync attribution and origin device
saves responses now include one device_syncs entry per device that has
synced a save, not just the caller's, so clients can tell which devices
hold a save. is_current is computed per entry and the caller's own entry
is ordered first for backward compatibility.

add a saves.origin_device_id column (migration 0081) recording the
device that created a save, set on initial upload only, surfaced as
origin_device_id on the save schema.
2026-06-05 20:25:01 +09:00
Georges-Antoine Assi
064a65698c add non-global IPs as forbidden 4.9.0-beta.1 2026-06-03 16:21:27 -04:00
Georges-Antoine Assi
96efc36d52 Merge pull request #3475 from Spinnich/fix/ss-jeuinfos-romnom-unhashed
fix(screenscraper): utilize ss.fr jeuinfos.php endpoint for non-hashable platforms
2026-06-03 16:07:25 -04:00
Georges-Antoine Assi
5cf67cd87c Merge pull request #3473 from Spinnich/fix/rom-content-404-on-stale-file-ids
fix(roms): return 404 when content file_ids match no files
2026-06-03 15:52:28 -04:00
Georges-Antoine Assi
20bae48ea9 add ot HEAD 2026-06-03 15:41:55 -04:00
Georges-Antoine Assi
fd78c7927a Merge pull request #3476 from rommapp/copilot/fix-network-error-emulatorjs
Invalidate EmulatorJS ROM cache when a ROM filename changes
2026-06-03 15:40:27 -04:00
Georges-Antoine Assi
1a1effd315 Merge pull request #3472 from Spinnich/test/update-rom-region-tag-reparse
test(roms): cover region-tag re-parse on rename (#3471)
2026-06-03 15:15:36 -04:00
Georges-Antoine Assi
ceaab1875b Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-03 15:08:51 -04:00
copilot-swe-agent[bot]
fd95a29551 fix: invalidate EmulatorJS ROM cache when rom filename changes
Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
2026-06-03 19:06:29 +00:00
Georges-Antoine Assi
357e5e0d51 Merge branch 'master' into fix/rom-content-404-on-stale-file-ids 2026-06-03 14:57:24 -04:00
copilot-swe-agent[bot]
12d304e4dd Initial plan 2026-06-03 18:56:17 +00:00
Georges-Antoine Assi
99f1fefedf Merge pull request #3468 from Spinnich/fix/ss-name-search-double-encoding
Fix double URL-encoding of ScreenScraper name-search term
2026-06-03 14:30:23 -04:00
Spinnich
2b23e69b7c Try SS jeuInfos by filename when files are un-hashed (#3474)
ScreenScraper matching skipped the stronger jeuInfos (romnom + systemeid)
lookup for any file without a hash, falling straight through to the weaker
jeuRecherche name search. Files are un-hashed for NON_HASHABLE_PLATFORMS
(PS3/4/5, Switch, Wii U, Xbox, etc.) and whenever SKIP_HASH_CALCULATION is
set, so those platforms matched worse than they could.

The transport already supports a hash-less jeuInfos?romnom=...&systemeid=...
request, so relax lookup_rom's early-return: only bail when there is neither
a hash nor a filename to match on. jeuRecherche stays the last-resort
fallback, keeping this quota-neutral.

Written primarily by Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:02:59 +00:00
Georges-Antoine Assi
fb8e4da435 Merge pull request #3465 from Spinnich/fix/screenscraper-new-3ds-platform-map
fix(ss): map New Nintendo 3DS to ScreenScraper system 17
2026-06-03 11:17:44 -04:00
Georges-Antoine Assi
566b53c86b Merge pull request #3466 from rommapp/claude/multi-file-rom-500-error-Z91md
fix(roms): repair multi-file ROM downloads broken by deferred file stats
2026-06-03 11:17:27 -04:00
Georges-Antoine Assi
ce8c1ed049 perf(scan): fetch rom files on demand for metadata matching
The scan loaders no longer eager-load `Rom.files` (#3425 + follow-ups), so
the hash-based metadata lookups can't rely on `rom.files` being populated —
`hasheous`/`ss` `lookup_rom` read `RomFile.is_top_level`, which dereferences
`RomFile.rom.full_path` and would raise `DetachedInstanceError` once the
session closed.

Add `DBRomsHandler.rom_files_for_rom_id`, which loads a ROM's files on demand
with the `RomFile.rom` backref eager-loaded (`load_only(fs_path, fs_name)`).
The scan path uses it as a fallback only when the filesystem walk yielded no
files (e.g. an unchanged rescan), behind a per-ROM `functools.cache` helper so
the playmatch/hasheous/ss lookups share a single DB fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:34:44 -04:00
Spinnich
00b894733d fix(roms): return 404 when content file_ids match no files
Renaming a ROM gives its file a new internal id, but the EmulatorJS
player keeps a remembered file id ("disc") in localStorage and reuses
it on the next launch. After a rename that id is stale, so the content
download endpoint matched zero files and fell through to its multi-file
ZIP path, producing a download whose only entry was an empty .m3u
playlist. nginx's mod_zip decode step rejects the blank value (HTTP
400) and aborts the response, sending 0 bytes — which EmulatorJS
surfaces as a generic "network error" (issue #3470).

The frontend half (validating the remembered disc against the ROM's
current files) already landed on master in d1696cd04. This is the
backend half: when no files match the request, raise a clean 404
instead of building a broken empty-.m3u ZIP. This also covers a ROM
with zero files.

Add endpoint tests (auth, single-file, valid file id, stale file id
-> 404, missing rom -> 404) plus a `rom_file` fixture.

Written primarily by Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:14:25 +00:00
Spinnich
c57c7b986b test(roms): cover region-tag re-parse on rename (#3471)
The update_rom path already re-parses filename tags when fs_name
changes (master commit d7a896b5da), but the headline scenario from
issue #3471 — an untagged ROM renamed to add (Europe) so the region
flag appears — was never asserted; existing coverage only exercised
the tag-removal direction.

Add a test that renames the untagged rom fixture to "test_rom
(Europe).zip" and asserts regions == ["Europe"], locking down the
add-region direction described in the issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:45:38 +00:00
Claude
3da3765ea3 test(feeds): give pkgj feed tests RomFile rows so they exercise output
The five pkgj feed tests created a ROM but no RomFile, so the per-file
feeds emitted only a header and the "in response.text" assertions never
actually verified output (pre-existing failures, also red on master).

Add a top-level `.pkg` GAME file (games feeds) or a DLC-category file
(dlc feeds), mirroring the pkgi_ps3 test, so the feeds produce rows.
This also gives real coverage of the new `include_files=True` path that
these feeds rely on.

https://claude.ai/code/session_01PSXKmejPRzdxLFMN6P2QQ4
2026-06-02 18:40:45 +00:00
Claude
c4dd922491 perf(roms): make filter_roms file-loading opt-in for the gallery query
filter_roms feeds both the gallery/list endpoint (SimpleRomSchema, no
files) and the feed endpoints (which iterate rom.files / is_top_level).
The cleanup commit's unconditional selectinload(Rom.files) + joinedload
made the gallery/list and filter-value paths pay for files they never
serialize.

Gate the files load behind a new `include_files` flag (default False),
mirroring the existing `include_file_stats` opt-in, and plumb it through
get_roms_scalar. The 9 feed endpoints that actually read rom.files opt
in; the gallery/list, filter-values, identifiers, smart-collection, and
the three feeds that don't touch files (webrcade, fpkgi, kekatsu) skip
the load entirely — keeping the gallery query at zero file cost.

https://claude.ai/code/session_01PSXKmejPRzdxLFMN6P2QQ4
2026-06-02 12:56:02 +00:00
Georges-Antoine Assi
36c8f388f9 run fmt 2026-06-02 08:40:05 -04:00
Georges-Antoine Assi
f680dd6ca8 remove 2026-06-02 08:37:54 -04:00
Spinnich
83b11a3370 Encode ScreenScraper name-search term only once (#3467)
The SS metadata handler pre-encoded the name-search term with quote()
before handing it to the service layer, which percent-encodes the query
again via yarl's with_query(). This double-encoded any character that
needs URL-encoding (e.g. "+" -> "%2B" -> "%252B"), so the request URL
carried a doubly-escaped term.

Pass the raw (unidecode-transliterated but un-percent-encoded) term to
search_games() in both _search_rom() and get_matched_roms_by_name() and
let the URL builder encode it exactly once. The scan now sends e.g.
recherche=...%2B... instead of ...%252B...

This is a request-correctness fix. It does not, on its own, make every
previously-unmatched title match: ScreenScraper's jeuRecherche normalizes
punctuation and applies its own relevance ranking, so some titles still
return no results for the full filename-derived term (verified directly
against the API). Improving name-search robustness is a separate concern.

Add TestSearchTermEncoding regression tests covering the un-pre-encoded
term, preserved unidecode transliteration, and a single-encoded request
URL (%2B, never %252B).

Written primarily by Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:34:34 +00:00
Claude
10a6df585f perf(scan): stop eager-loading files in get_roms_by_fs_name
Restore the "platform only" contract of `get_roms_by_fs_name` (per its
docstring) by dropping the `selectinload(Rom.files)` + `joinedload`. That
load only existed for `scan_rom`'s rare `fs_rom["files"] or rom.files`
fallback, but it forced files (and a per-file join back to roms) for every
ROM in a scan batch — expensive on large platforms, and only used when the
filesystem scan yielded no files.

Instead, fetch the persisted files on demand: `scan_rom` now resolves match
files via a small helper that returns the filesystem-scanned files, falling
back to `db_rom_handler.get_rom_files_by_rom_id(rom.id)` only when there are
none. The new getter eager-loads the `RomFile.rom` backref so `is_top_level`
keeps working on the detached results (the rare path was already latently
broken on master, which loaded files without the backref).

https://claude.ai/code/session_01PSXKmejPRzdxLFMN6P2QQ4
2026-06-02 12:31:46 +00:00
Georges-Antoine Assi
895f495ee6 cleanup 2026-06-02 08:02:54 -04:00
Claude
fbd703cccb fix(roms): eager-load RomFile.rom in detail/scan queries instead of a hook
Replace the `_link_rom_files_to_parent` post-fetch hook with the
declarative loader pattern PR #3425 originally removed, restoring
`joinedload(RomFile.rom).load_only(Rom.fs_path, Rom.fs_name)` on the two
queries that still load `Rom.files` (`with_details` and
`get_roms_by_fs_name`).

#3425 dropped that joinedload everywhere as part of denormalizing file
stats into the `multi_file` / `top_level_file_count` column properties.
But `is_top_level` / `file_name_for_download` (multi-file downloads, 3DS
QR codes, metadata matching) still read `RomFile.rom.full_path`, so the
two file-loading paths were over-cleaned, causing a `DetachedInstanceError`
(500) on multi-file downloads once the session closed.

The gallery query (`filter_roms`) dropped `Rom.files` entirely and is
untouched, so the performance win from #3425 is preserved; the restored
join only adds an index-backed PK lookup of two columns to the existing
files `selectin` on the detail/scan paths.

https://claude.ai/code/session_01PSXKmejPRzdxLFMN6P2QQ4
2026-06-02 11:59:36 +00:00
Georges-Antoine Assi
b51e03a9d0 remove unnecesary tests 2026-06-02 07:22:53 -04:00
Claude
824ce185fe test(roms): add fixture-driven multi-file ROM download test
Add a shared `multi_file_rom` fixture (a game folder with multiple
RomFile rows) and an endpoint-level test that downloads it via
`GET /api/roms/{id}/content/{file_name}`. This exercises the multi-file
download path end-to-end, which builds each mod_zip manifest entry from
`file.rom.full_path` after the handler session has closed — the exact
path that 500'd with `DetachedInstanceError` before the backref fix.

The download endpoint had no test coverage for multi-file ROMs (the
`rom` fixture has no RomFile rows), which is why the regression slipped
through. Reuse the new fixture in the handler-level regression test too.

https://claude.ai/code/session_01PSXKmejPRzdxLFMN6P2QQ4
2026-06-02 09:13:34 +00:00
Claude
342857b14b fix(roms): repair multi-file ROM downloads broken by deferred file stats
PR #3425 dropped `lazy="joined"` from `RomFile.rom` and removed the
`joinedload(RomFile.rom)` from the ROM loaders to speed up the gallery
query. That left the `RomFile.rom` backref unpopulated. Single-file
downloads only read `RomFile.full_path` (built from `file_path`/
`file_name`), so they kept working, but multi-file (game folder)
downloads call `file_name_for_download()` / `is_top_level`, which read
`self.rom.full_path`. With no eager-loaded backref, that triggered a
lazy load on a detached instance once the handler session closed,
raising `DetachedInstanceError` and returning a 500.

Rather than reverting the loader changes (and the gallery gains), wire
the `RomFile.rom` backref up in Python from the parent ROM we already
hold in memory, via `set_committed_value`. This is zero extra DB cost
and only runs on the detail/download paths (`with_details` and
`get_roms_by_fs_name`); the optimized `filter_roms` gallery query is
untouched.

https://claude.ai/code/session_01PSXKmejPRzdxLFMN6P2QQ4
2026-06-02 08:52:25 +00:00
Spinnich
3be7d9040f fix(ss): map New Nintendo 3DS to ScreenScraper system 17
New Nintendo 3DS games never matched ScreenScraper because the platform
was missing from SCREENSAVER_PLATFORM_LIST. With no entry, get_platform()
returns ss_id=None and scan_handler skips the entire ScreenScraper lookup
(hash and filename) for the platform, reporting everything as unmatched.

ScreenScraper has no separate New 3DS system; New 3DS games live under the
regular Nintendo 3DS system (ID 17). Alias New Nintendo 3DS to that system,
matching the existing Famicom->NES, Super Famicom->SNES, and DSi->DS aliases.

Fixes #3464

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 00:21:48 +00:00