Commit Graph

9632 Commits

Author SHA1 Message Date
Spinnich
9ba4e12fa8 Match IGDB regional-twin platforms in scans (#3462)
IGDB catalogues a console and its regional twin as two separate
platforms (SNES/Super Famicom, NES/Famicom). RomM locked each IGDB
search to a single platform id, so a region-exclusive title catalogued
under only the twin — e.g. the Japan-only Super Famicom game
"Rudra no Hihou" (platform 58) scanned from an `snes` folder
(platform 19) — was filtered out before name matching ran and never
matched.

Include a platform's regional twin in the IGDB platform filter so both
are searched. A non-twin platform keeps the exact existing query
(`platforms=[19]`); a twin produces an OR group
(`(platforms=[19] | platforms=[58])`), leaving all other platforms and
recorded cassettes unchanged.

Written primarily by Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:40:42 +00:00
Georges-Antoine Assi
85789466d1 Merge pull request #3461 from Spinnich/fix/igdb-localized-name-match
fix(igdb): match ROMs by localized/alternative titles in scan
2026-05-31 13:05:58 -04:00
Georges-Antoine Assi
588c784d62 Merge pull request #3459 from rommapp/romm-3457
[ROMM-3457] Add tgcd and supergrafx to emulatorjs platform list
2026-05-31 09:26:51 -04:00
Georges-Antoine Assi
3ca45fafc1 [ROMM-3457] Add tgcd and supergrafx to emulatorjs platform list 2026-05-31 09:23:53 -04:00
Spinnich
7e08a43e12 fix(igdb): match ROMs by localized/alternative titles in scan
IGDB scans dropped games whose filename uses a localized (non-English)
title even when that title exists in IGDB's alternative_names. The
alternative_name wildcard search surfaced the correct game, but
_search_rom() rebuilt its name->game candidate dict using only the
primary English name, so the Jaro-Winkler re-check scored the localized
term below threshold and discarded the match (issue #3435).

Add _index_games_by_searchable_name(), which indexes each game by its
primary name plus alternative_names and game_localizations titles, and
use it for both candidate-building passes in _search_rom(). Primary
names keep precedence (lowest-igdb-id tiebreak); alternative/
localization titles fill in only names not already claimed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 02:31:46 +00:00
Georges-Antoine Assi
2137adc429 Merge pull request #3454 from rommapp/feat/emulatorjs-new-cores
feat(player): add genesis_plus_gx_wide, bsnes, freeintv, azahar EJS cores
4.9.0-alpha.8
2026-05-30 21:58:22 -04:00
Georges-Antoine Assi
9e5f49a6fc update jsdoc 2026-05-30 21:53:44 -04:00
Georges-Antoine Assi
96ac7ac5e8 only add new cores in nightly 2026-05-30 20:53:52 -04:00
Georges-Antoine Assi
ff2a71e114 Merge pull request #3455 from Spinnich/fix/ra-hash-missing-for-archives
fix(hashing): compute RA hash for archive ROMs on cartridge platforms
2026-05-30 20:38:30 -04:00
Georges-Antoine Assi
ad576909d3 changes from bot review 2026-05-30 20:26:29 -04:00
Georges-Antoine Assi
5144e78767 Merge branch 'master' into fix/ra-hash-missing-for-archives 2026-05-30 20:24:29 -04:00
Georges-Antoine Assi
f6e38859d7 Merge pull request #3453 from tmgast/fix/savesync-negotiate-thrash
Fix save-sync negotiate thrash and unblock content_hash recompute
2026-05-30 16:44:10 -04:00
Georges-Antoine Assi
368281a53f Merge pull request #3451 from Spinnich/fix/romnom-single-file-archive
fix(screenscraper): use internal filename as romnom for single-file archive games
2026-05-30 16:34:09 -04:00
Spinnich
1d9963ac63 fix(hashing): compute RA hash for archive ROMs on cartridge platforms
The archive branch of get_rom_files (introduced in #3412) was missing
the RAHasherService.calculate_hash call that exists in the non-archive
branch, causing all archive-format ROMs to produce an empty ra_hash
during scanning regardless of platform.

The RA hash call is now made for archive ROMs, mirroring the existing
non-archive behaviour. The RA_BUFFER_HASH_UNSUPPORTED skip logic in
RAHasherService already handles disc-based platforms (PSX, PS2, PSP,
Saturn, Dreamcast, etc.) so those continue to be excluded automatically.

Also improves handling of folder-based multi-file ROMs whose directories
contain compressed files. RAHasher cannot process archives via the /*
glob and fails with "Could not open file". The fix mirrors the existing
CHD folder logic: for cartridge platforms the largest archive in the
folder is passed directly to RAHasher for buffer hashing; for disc
platforms the call is skipped as buffer hashing is unsupported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 14:55:42 +00:00
Georges-Antoine Assi
77de623834 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-30 04:34:15 -04:00
Georges-Antoine Assi
be15ab4513 feat(player): add genesis_plus_gx_wide, bsnes, freeintv, azahar EJS cores
- genesis_plus_gx_wide added alongside genesis_plus_gx for all Sega
  MD/MS/GG/CD platforms
- bsnes added alongside snes9x for SNES platforms
- freeintv added for Intellivision
- azahar added for Nintendo 3DS (3ds, new-nintendo-3ds) and marked as
  requiring threads

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 04:30:47 -04:00
nendo
ca036e78fe SaveSync: guard recompute Job.exists() in try; assert full job_id contract 2026-05-30 10:11:45 +09:00
Georges-Antoine Assi
69e2373453 cleanup archive member 2026-05-29 20:54:32 -04:00
nendo
771a8084b7 SaveSync: fix recompute backfill enqueue (colon job_id, bogus unique kwarg)
RQ 2.x Job.set_id rejects ':' in a job_id, so the bootstrap enqueue raised
ValueError that the broad except swallowed -- the content_hash recompute task
never ran, leaving legacy saves with stale/NULL hashes. Drop the colon, remove
the unsupported unique=True (would TypeError the worker once enqueued), and
replace the dead DuplicateJobError branch with a real Job.exists guard.
2026-05-30 07:39:52 +09:00
nendo
f1d44c3321 SaveSync: pair negotiate on (rom_id, slot), not tagged file_name
The server datetime-tags every slot upload's filename (archival spec), so a
slot accrues many rows and the stored file_name never equals the client's
untagged canonical name. Keying negotiate's server-save map on file_name meant
every client save missed -> perpetual "upload", and every tagged server row
went unmatched -> perpetual "download", with save rows growing unbounded.

Pair on (rom_id, slot), collapsing each slot to its newest row, so
compare_save_state actually runs and content hashes decide the action.

Tests: real upload->negotiate round-trip (lets _apply_datetime_tag run, client
reports the untagged name) and a 3-device convergence test; both fail against
the old file_name keying.
2026-05-30 07:39:51 +09:00
Spinnich
19d50e86b9 fix(screenscraper): use internal filename as romnom for single-file archives
When sending a hash lookup to ScreenScraper, romnom was always set to the
archive filename on disk (e.g. Mario.zip). For single-file archives, the hash
is computed from the internal file (e.g. mario.n64), so sending the archive
name sends slightly incorrect info to ss.fr during a KO scrape.

When archive_members has exactly one entry, romnom now uses that member's
name. Multi-file archives and non-archive files continue to use the filesystem
filename unchanged.

Closes #3444

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:34:40 +00:00
Georges-Antoine Assi
f58fdfe1e1 Merge pull request #3412 from Spinnich/feat/composite-hashing-archives
Composite hashing for compressed archives
4.9.0-alpha.7
2026-05-29 13:36:23 -04:00
Georges-Antoine Assi
10d731d823 cleanup 2026-05-29 11:58:53 -04:00
Georges-Antoine Assi
ae60d14f81 Merge branch 'master' into feat/composite-hashing-archives 2026-05-29 11:50:17 -04:00
Georges-Antoine Assi
29f90c027f Merge pull request #3448 from tmgast/fix-save-sync-hash-and-archival
Fix save-sync hash drift, archival save leak, and dedupe scoping
2026-05-29 11:47:52 -04:00
Georges-Antoine Assi
04a9c95df1 Merge pull request #3450 from rommapp/copilot/bugfix-error-500-fetching-roms
Fix HTTP 500 on ROM list when platform has ROMs with NULL names
2026-05-29 07:43:58 -04:00
copilot-swe-agent[bot]
54dc059e15 Fix 500 error when char_index contains None key from NULL ROM names
Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
2026-05-29 11:23:55 +00:00
copilot-swe-agent[bot]
10bcd12183 Initial plan 2026-05-29 11:18:58 +00:00
nendo
db0f714b4f SaveSync: use pathlib joins for asset content-hash paths
FSAssetsHandler.compute_content_hash and _compute_zip_hash were
building full paths via f"{self.base_path}/{file_path}". self.base_path
is already a pathlib.Path (resolved by FSHandler.__init__), so the
f-string forced it to str, hard-coded the separator, and re-parsed --
fine on Linux but a footgun if a caller ever sneaks a leading slash or
the path needs Path semantics elsewhere.

Switch both spots to self.base_path / file_path, which is what every
other FSHandler subclass in this module already does (e.g.
FSRomsHandler, FSResourcesHandler, FSSyncHandler all join Path objects
directly).
2026-05-29 17:40:56 +09:00
nendo
41c91fdd5b SaveSync: push null-slot exclusion into the SQL query
Three sync callsites (endpoints/sync.py, sync_watcher.py, and both
branches of tasks/sync_push_pull_task.py) ran get_saves(...) and then
discarded archival null-slot rows in a Python list comprehension. On
libraries with many archival/web-UI uploads that's a strict waste:
those rows are pulled from MariaDB, hydrated into Save model instances,
and then immediately filtered out.

Add a slot_not_null bool kwarg to DBSavesHandler.get_saves and apply
the filter in the SQL query. Update all four callsites to use it and
drop the Python-side comprehension. Default stays False so unrelated
callers keep the current behavior.
2026-05-29 17:40:18 +09:00
nendo
5bb10dacd1 SaveSync: paginate recompute task scan by primary key
get_all_saves() materialized every Save row across all users into a
single .all() list. On instances with very large libraries that's a
real RAM ceiling and pins every row for the lifetime of the recompute
run.

Replace it with get_saves_after_id(after_id, limit) and have the
recompute task drive keyset pagination in PAGE_SIZE-row chunks. SQLAlchemy
streaming via .execution_options(yield_per=...) is incompatible with the
per-call session lifetime that @begin_session enforces (the session
exits before the consumer iterates), so keyset paging from the caller is
the cleanest fit.

Behavior is unchanged: same row coverage, same idempotency, same
counters. Memory usage drops from O(all saves) to O(PAGE_SIZE).
2026-05-29 17:38:49 +09:00
nendo
ec50f75d77 SaveSync: dedupe bootstrap recompute job across API restarts
Pass a deterministic job_id and unique=True to low_prio_queue.enqueue
so a restart while a previous bootstrap recompute is still queued or
running no-ops the second enqueue. Without this, every API restart with
a NULL-hash row left would push another duplicate job onto the
low-priority queue; RQ would happily run both back-to-back, redoing the
same scans and content-hash reads against the filesystem.

RQ raises rq.exceptions.DuplicateJobError when unique=True hits an
existing job ID. Swallow it with a log line and let other enqueue
failures fall through to the generic exception path so they still get
logged with a traceback.
2026-05-29 17:35:32 +09:00
nendo
edb5d15420 Fix save-sync hash drift, archival save leak, and dedupe scoping
Cleanup pass on save-sync addressing three independent failure modes
that interact in production data: content_hash drift between client
and server, null-slot archival saves leaking into sync flows, and
content-hash dedupe collapsing legitimately-distinct slots.

Bug fixes
- compute_content_hash dispatched on zipfile.is_zipfile(relative_path),
  which silently returned False whenever the process's CWD wasn't
  ASSETS_BASE_PATH. Every zip save fell through to the raw-MD5 branch,
  persisting hashes that disagreed with clients computing the intended
  per-entry zip-hash. Resolve to a full path before the dispatch.
- _build_negotiate_plan, sync_push_pull_task, and sync_watcher all
  treated null-slot saves as sync-eligible. Null-slot saves represent
  web-UI / archival uploads; including them in negotiate plans matched
  them against device pushes by filename and overwrote archival data.
  Filter null-slot saves at all three call sites.
- get_save_by_content_hash matched on (rom_id, user_id, content_hash)
  only, so identical bytes uploaded to different slots collapsed into
  one record. Scope the lookup by slot when provided so clone-save-
  to-new-slot creates a distinct row per slot.
- get_save_by_filename matched on (rom_id, user_id, file_name) only.
  When two uploads to different slots happened in the same wall-clock
  second (the datetime tag is per-second), the second upload UPDATED
  the first record's slot instead of creating a distinct row. Scope
  the filename lookup by slot too.

One-shot recovery
- New recompute_save_content_hashes manual task walks every Save row,
  recomputes via the fixed dispatch, and updates rows whose values
  differ. Idempotent; safe to re-run.
- Backend startup runs a COUNT(content_hash IS NULL) query and, if
  any rows exist, enqueues the recompute task on the low-priority
  RQ queue. The API process moves on; the worker handles the
  recompute out-of-band. Subsequent restarts find zero NULL hashes
  and skip. Admins can also trigger the task manually.

Test infrastructure
- Added tests/_zipfile_shim.reload_zipfile() mirroring the pattern
  from utils/zip_cache.py for the same zipfile-inflate64 + CPython
  3.13.5 incompatibility. Test fixtures that build ZIPs call it
  immediately before opening the archive.
2026-05-29 17:00:01 +09:00
Georges-Antoine Assi
b39feaf7c6 Merge pull request #3446 from rommapp/copilot/feature-allow-selecting-miximage-version
feat: add `miximage_v2` media type to explicitly select SS.fr mixrbv2
4.9.0-alpha.6
2026-05-28 20:13:50 -04:00
Georges-Antoine Assi
8f08769670 run fmt 2026-05-28 20:05:24 -04:00
copilot-swe-agent[bot]
d29ed39a6a Add miximage_v2 media type mapping to SS.fr mixrbv2
Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
2026-05-28 20:15:40 +00:00
copilot-swe-agent[bot]
71cb3e4f04 Initial plan 2026-05-28 20:06:17 +00:00
Georges-Antoine Assi
0ee32ef4b3 add generated files 2026-05-28 09:43:47 -04:00
Georges-Antoine Assi
207d0dc4c6 feat(hashing): persist per-member hashes on archive RomFile
Internal members of multi-file archives (zip/tar/7z/rar) are now hashed
individually (crc/md5/sha1) and stored in a new `archive_members` JSON
column on the archive's RomFile, alongside the existing composite hash
used for hash-database matching. Only the archive itself is surfaced as
a RomFile so full_path keeps pointing at a file that exists on disk,
which is the constraint that previously forced us to choose between
composite-only or broken downloads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:41:04 -04:00
Georges-Antoine Assi
9111f70d0a refactor(filesystem): merge archive_7zip.py into archives.py
Consolidate all archive readers (zip/tar/7z/rar) and 7z-internal helpers
into a single utils/archives.py module to keep the archive surface area
in one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:10:01 -04:00
Georges-Antoine Assi
a1194dc5e0 changes from bot review 2026-05-28 09:02:26 -04:00
Zurdi
52a3d3df21 Merge pull request #3443 from VewDev/patch-1
chore: remove obsolete 'version' attribute
2026-05-28 11:53:08 +02:00
Cool Contributor
cc13c9efd7 chore: remove obsolete 'version' attribute
Remove the now obsolete 'version' attribute in the example docker compose file.
2026-05-28 11:50:45 +02:00
Georges-Antoine Assi
e5d48dc401 Merge pull request #3442 from rommapp/dependabot/uv/asyncssh-2.23.0
chore(deps): bump asyncssh from 2.22.0 to 2.23.0
2026-05-27 21:58:43 -04:00
Georges-Antoine Assi
deaffb9395 Merge pull request #3441 from rommapp/claude/intelligent-allen-aIK8u
Add DNS resolution check to URL validation
2026-05-27 21:58:25 -04:00
Georges-Antoine Assi
7509707fe7 fix test 2026-05-27 21:50:56 -04:00
Georges-Antoine Assi
a170649fe6 fix(hashing): emit single RomFile for multi-file archives
Per-internal-member RomFiles produced full_paths that didn't exist on
disk, breaking downloads and zip-building. Stream entries into the
composite hash only and emit one RomFile pointing at the archive itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:35:01 -04:00
Georges-Antoine Assi
0bfe369425 run fmt 2026-05-27 21:03:08 -04:00
Georges-Antoine Assi
5f63668996 cleanup 2026-05-27 20:58:33 -04:00
Georges-Antoine Assi
c3adbd3f71 fix(ssrf): bound DNS lookup by caller timeout; clear lint findings
The async backend's `loop.getaddrinfo` ran without any timeout, so a
slow or hanging resolver could outlive the timeout the caller passed —
the previous code only bounded the TCP connect inside the inner
backend. Wrap the resolution in `asyncio.timeout(timeout)` and surface
the timeout as `httpcore.ConnectTimeout`.

Also tidy the test stubs (mypy func-returns-value) and add explicit
type annotations to the `calls` lists (mypy var-annotated). A targeted
`# noqa: ASYNC109` sits on the `timeout` parameter of `connect_tcp` /
`connect_unix_socket` with an explanatory comment: the rule advises
against `timeout` parameters on async APIs we author, but here we're
implementing `AsyncNetworkBackend`, and the timeout is consumed in the
asyncio-native pattern the rule endorses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:31:42 -04:00