Commit Graph
13347 Commits
Author SHA1 Message Date
Georges-Antoine Assi d5301ac8fd Merge pull request #4854 from rommapp/fix/rom-file-category-anchor
fix(scan): anchor rom file category to the folder below the rom root
2026-09-28 08:12:17 -04:00
Georges-Antoine Assi a12450d0fe Merge pull request #4853 from rommapp/fix/pdf-viewer-tabs
fix(v2): mount one PDF viewer at a time in the Media tab
2026-09-28 08:10:03 -04:00
Georges-Antoine Assi 1027cc3832 Merge pull request #4852 from rommapp/fix/7z-hash-timeout-scales-with-size
fix(scan): raise the 7-Zip timeout default and keep partial bytes out of the fallback hash
2026-09-28 08:08:07 -04:00
Georges-Antoine AssiandClaude Opus 5.5 c276b62134 test(rom_files): seed folder categories like a scan would
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 08:07:02 -04:00
Georges-Antoine Assi e2224ba858 Merge pull request #4856 from rommapp/fix/setup-checkbox-scroll-blank
fix(v2): keep the checkbox's native input inside its label
2026-09-28 08:06:50 -04:00
Georges-Antoine AssiandClaude Opus 5.5 abe5b85a06 test(v2): drive MediaTab test with a real memory router
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 08:05:24 -04:00
Georges-Antoine Assi 005bf42577 Merge remote-tracking branch 'origin/master' into pr-4854 2026-09-28 08:03:58 -04:00
Georges-Antoine AssiandClaude Opus 5.5 d6530463f6 fix(scan): raise the default SEVEN_ZIP_TIMEOUT to 180 seconds
Sixty seconds cut off extraction of mid-sized disc images, so their scan
fell back to hashing the archive itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 07:55:49 -04:00
Georges-Antoine Assi 41c553020d Merge remote-tracking branch 'origin/master' into merge/pdf-viewer-tabs
# Conflicts:
#	frontend/src/utils/rom.fixtures.ts
2026-09-28 07:54:56 -04:00
Georges-Antoine AssiandClaude Opus 5.5 847b222b3f fix(scan): keep partial 7z member bytes out of the raw-archive hash
When extracting the largest member of a .7z failed partway, the bytes already
streamed stayed in the file and ROM-level hashers, and the raw-archive
fallback was appended on top, giving a hash that matched neither. The hashers
are now restored to their pre-extraction state before the fallback runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 07:53:39 -04:00
Georges-Antoine Assi 5f244afe8f Merge pull request #4851 from rommapp/fix/validate-emulator-path-segment
fix(assets): reject emulator names that are not a single folder on upload
2026-09-28 07:48:30 -04:00
Georges-Antoine Assi c868d15827 Merge pull request #4849 from rommapp/fix/win9x-screenscraper-id
fix(ss): map Win9x to ScreenScraper's generic Windows system
2026-09-27 20:47:52 -04:00
Georges-Antoine Assi 1d52da5e93 Merge pull request #4848 from rommapp/fix/player-default-descriptor-file
fix(player): boot cue/gdi/ccd/mds disc sets whole by default
2026-09-27 20:46:54 -04:00
Sam DornanandClaude Opus 5.5 68e9204f46 fix(v2): stop URL query writes from dropping history entries; bump vue-router to 5.3.1 (#4861)
* chore(deps): bump vue-router to 5.3.1 and vue to 3.5.43

vue-router 5 needs vue ^3.5.34. The runtime API RomM uses is unchanged:
typecheck, the unit suite and the production build pass without code
changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* fix(v2): stop URL query writes from dropping history entries

Query-only router.replace calls resolved against currentRoute, which is
still the page being left while a navigation waits on async guards (the
view-transition capture, the ROM fetch in /rom/:id's beforeEnter). A
write landing in that window cancelled a pending push, so the click was
swallowed, or, on Back/Forward, replaceState'd the entry the browser had
already moved to with the old page's URL, deleting it from history.

patchQuery now binds each write to the path it was made on, waits for an
in-flight navigation to settle (tracked by installQueryNavigationGuard,
installed in AppLayout) and drops the write if the navigation left that
page. The gallery filter sync, the platform, collection, admin, library
management and notification tabs, and the notes selection now write
through it, and the gallery's debounced write is cancelled on unmount.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* style(v2): replace em-dashes in comments of files this PR touches

trunk check reports every no-emdash-in-comment hit in a modified file,
not only on changed lines. Comment text only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* fix(v2): rewrite a ?subtab= the details tab can't show

GameDetails swaps currentRom before a rom-to-rom navigation commits, so a
tab that resets its subtab for the new game writes into the pending
navigation and that write is dropped. The URL then kept a subtab the new
game doesn't have. useSubtabQuery now rewrites an invalid ?subtab= to the
subtab on screen on mount and after every path change, which also fixes
stale deep links.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* refactor(v2): key the details ROM by the route's :rom param

GameDetails read a single currentRom slot that the route guards filled
before the navigation committed, so the page being left briefly
rendered the next game under its own URL, and anything reacting to that
wrote into a navigation still in flight.

stores/roms now keeps a bounded, id-keyed cache of DetailedRoms.
setCurrentRom and update() feed it, so the shared /rom/:rom guard and
every SimpleRom write keep it current, and v1 keeps reading currentRom
unchanged. v2 reads through useRouteRom(), which looks the record up by
the committed route's :rom param: a guard can prefetch the next game
without the current page seeing it. The dialogs, useRomSync, the scan
and pending-asset refreshes, FilesTab and the player seeds all go
through the cache or the route id. useSubtabQuery's rewrite is narrowed
to stale links, the case it still covers.

Also replaces em-dashes in comments of the files this touches, which
trunk check flags per modified file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* style(v2): sort useRomScanRefresh imports for trunk's prettier

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* fix(v2): keep the shown game cached through a burst of rom hops (code-review)

Each superseded rom-to-rom navigation still cached its game, so ten
quick hops could evict the record the committed page renders and blank
it until the last hop landed. Re-cache the shown game before the next.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* refactor(v2): simplify the query-write and ROM cache plumbing (simplify)

- routeQuery: track the in-flight navigation as a single marker and
  schedule one flush per router instead of a waiter queue; add
  replaceQuery for whole-query writes and route Logs through it, so no
  v2 query writer bypasses the guard.
- Tab, subtab and gallery-filter writers use syncQueryParam, which only
  writes keys that changed.
- useSubtabQuery shares one owned-subtab helper and drops the route.path
  trigger the id-keyed cache made redundant.
- The ROM cache holds 3 records (nothing reads it to skip a fetch) with a
  single-step eviction; GameDetails re-caches the shown game via
  useRouteRom; FilesTab uses refetchRom's result; Stream parses :rom
  with romIdFromRoute.
- Trim comments to the two-line rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* style(v2): trim the roms.update merge comment (review-polish)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* style(v2): tidy comment grammar left by the em-dash pass (humanizer)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* fix(v2): address review findings on query writes and the ROM cache

- syncQueryParam compares against the queued write, so a control
  switched back before the first write lands still gets written.
- Only the tracked navigation (or an untracked duplicate) settles the
  query guard, so an older navigation aborting after a newer one
  started can't release writes into it.
- roms.reset() (run on logout) clears the detailed ROM cache, and
  deleting a game drops its record once the page has moved off it.
- Shared makeDetailedRom fixture beside makeRom; the CollectionsIndex
  mock carries a route path.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* fix(v2): settle query writes only for the tracked redirect chain

- A duplicate navigation settles the query guard only when it began as
  the tracked navigation (a guard redirect back onto this page, followed
  through redirect chains via redirectedFrom). An unrelated duplicate no
  longer releases a held write while the tracked navigation is pending;
  that navigation settles itself when vue-router cancels it.
- DeleteRomDialog logs a failed post-delete redirect instead of
  reporting the delete as failed, and keeps the record of a game the
  page still shows so an aborted redirect can't blank it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

* style(v2): drop the routeQuery header paragraph the guard docs already cover

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CB3a8xbmnmuMeddGnRgVse

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-27 19:43:59 -05:00
Sam DornanandClaude Opus 5.5 d828706ef9 perf(backend): pipeline multi-command Redis writes (#4858)
* perf(backend): pipeline multi-command Redis writes

- upload_chunk records a chunk with SADD, EXPIRE and SCARD in one
  transaction instead of three round trips, so a failure between them
  can no longer leave the chunk set without a TTL.
- bind_to_login_session does its SADD and EXPIRE in one transaction for
  the same reason.
- close_login_sessions reads and deletes a session's socket set in one
  transaction, so a socket binding in between is not dropped unseen.
- Logout deletes the session and removes it from the user's set in one
  pipeline.
- complete_chunked_upload reads the chunk set once instead of SCARD then
  SMEMBERS; cancel and invalidate_filter_values_cache delete their keys
  in one call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T2WxB8t3Shv4TZmHFdSMHv

* fix(backend): refuse a socket bind that lands after its session's revocation

- bind_to_login_session checks the login session still exists in the
  same transaction as its SADD. Revocation deletes the session before it
  reads the socket set, so a bind that finds it gone removes itself and
  authenticate treats the socket as anonymous.
- complete_chunked_upload keeps its SCARD fast path and reads the
  members only to name the missing chunks.
- cancel_chunked_upload removes the temporary files before touching
  Redis.
- Tests check each pipelined write executes as one MULTI/EXEC.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T2WxB8t3Shv4TZmHFdSMHv

* fix(backend): close an upload session before removing its files on cancel

A chunk PUT that passes the session check after the temporary directory
is gone fails with a 500; deleting the session first turns it into a 404.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T2WxB8t3Shv4TZmHFdSMHv

* fix(backend): answer a chunk write cut short by a cancel with 404

A PUT that passed the session check before the cancel removed its
directory fails writing; it now re-reads the session and returns the
same 404 as a PUT that arrives after the cancel, instead of a 500.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T2WxB8t3Shv4TZmHFdSMHv

* chore(backend): remove a stray Redis snapshot

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T2WxB8t3Shv4TZmHFdSMHv

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-27 19:41:49 -05:00
Georges-Antoine Assi e1098a6867 Merge pull request #4859 from LoneAngelFayt/feat/streaming-retroarch-core-override
feat(streaming): per-platform RetroArch core override
2026-09-27 20:36:02 -04:00
Georges-Antoine AssiandClaude Opus 5.5 4db3caf040 test(v2): address Greptile review on the RCheckbox test comment
Trim the comment to two lines and drop the issue reference.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:26:33 -04:00
Georges-Antoine AssiandClaude Opus 5.5 0e35dc8d17 fix(scan): address Greptile review on the rom file category anchor
Recompute the folder category for rows a Quick scan reuses, so files
categorized before the anchor fix are corrected without a Complete scan,
and annotate the new tests' parameters.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:23:51 -04:00
Georges-Antoine AssiandClaude Opus 5.5 c10ae9815b fix(mypy): type the spec path helper in streaming tests
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:21:56 -04:00
Georges-Antoine AssiandClaude Opus 5.5 9d3e8e6cc4 fix(v2): address Greptile review on the PDF viewer subtabs
Hold the pdf.js app until its container is laid out, so a viewer that
loads while its subtab is hidden no longer sizes to zero. Build the
MediaTab ROM through a typed makeDetailedRom factory, and replace the
em-dashes in PdfViewer's comments.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:20:01 -04:00
Georges-Antoine Assi 96ede4f04b Merge remote-tracking branch 'origin/master' into feat/streaming-retroarch-core-override 2026-09-27 20:19:32 -04:00
Georges-Antoine Assi d2b49eeea6 Update examples/config.example.yml 2026-09-27 20:13:04 -04:00
Georges-Antoine AssiandClaude Opus 5.5 837da4c1a0 chore(ss): address Greptile review on the Win9x comment
Reword the mapping comment so the platform is the subject.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:12:50 -04:00
Sam Dornan 29220cd59e fix(backend): restart the log forwarder and harden three Redis call sites (#4847) 2026-09-27 20:10:06 -04:00
Zach Clendenen 21c5b65d17 fix(streaming): address Greptile review of the RetroArch core override
- Retry the exit once when a broker boots the wrong core and the first exit
  fails: the broker refuses every activate while that game runs, and the
  released claim leaves nothing behind to stop it.
- Stamp the launch's core tier on the session and report it through the
  status poll, so a tab that missed launch-ready still warns about an
  untested or broken core, once.
- Translate the core-untested and core-blocked warnings in every locale.
2026-09-27 16:41:38 -05:00
Georges-Antoine Assi b40919b0c5 Merge pull request #4835 from tmgast/feat/device-push-install
feat(devices): push downloads from the web UI to a device's download queue
2026-09-27 16:32:57 -04:00
Georges-Antoine AssiandClaude Opus 5.5 2b4dfebab5 fix(mypy): drop redis generic args after the stubs upgrade
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 16:28:47 -04:00
Georges-Antoine Assi 41767a6a44 Merge remote-tracking branch 'origin/master' into feat/device-push-install 2026-09-27 16:13:58 -04:00
Georges-Antoine AssiandClaude Opus 5.5 e7e3b4b2b6 refactor(device): move device auth and tests into the device packages
- endpoints/device_auth.py -> endpoints/device/auth.py (and its response module)
- Device and install handler tests mirror the new package layout
- The install router is included by the device router, so main.py mounts one
  /devices router plus the /auth/device one
- GET /api/devices/online moves to the install routes and 404s while
  DEVICE_INSTALL_ENABLED is off, like the rest of them

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 16:06:45 -04:00
Georges-Antoine AssiandClaude Opus 5.5 673c1b1052 refactor(device): nest device endpoints, responses and install handler in packages
- endpoints/device.py, device_install.py -> endpoints/device/__init__.py, install.py
- endpoints/responses/device.py, device_install.py -> endpoints/responses/device/__init__.py, install.py
- handler/device_install_handler.py, device_install_policy.py -> handler/device_install/__init__.py, policy.py

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 15:55:42 -04:00
Sam Dornan 452b52cbae chore(mypy): replace type-ignore defaults with typed injection sentinels (#4843) 2026-09-27 15:51:50 -04:00
Zach Clendenen 3fbb746ded test(streaming): review-polish pass, assert the core reaches the tier warning 2026-09-27 14:44:09 -05:00
Zach Clendenen 8336f127b6 refactor(streaming): simplify pass on the RetroArch core override 2026-09-27 14:39:13 -05:00
Zach Clendenen 2d25618ee2 fix(streaming): code-review pass on the RetroArch core override 2026-09-27 14:29:47 -05:00
Georges-Antoine AssiandClaude Opus 5.5 a1d448b9f7 fix(player): leave the frozen v1 player out of the default change
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 15:10:50 -04:00
Georges-Antoine AssiandClaude Opus 5.5 fc62d1f4ae style(v2): drop em-dashes from RCheckbox comments
Trunk flags every em-dash in a touched file as a new issue.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 15:08:19 -04:00
Georges-Antoine AssiandClaude Opus 5.5 96e2c2ad42 refactor(backend): pr-ready follow-ups for device installs
- Replace the inline bytes-to-text decodes with redis_handler.as_text
- Add utils.auth.token_device_id for a request's token-bound device
- Look up a group's member tokens in one query and close them in one pass

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 15:04:56 -04:00
Georges-Antoine AssiandClaude Opus 5.5 fc637f88f8 fix(v2): keep the checkbox's native input inside its label
The visually hidden native input is absolutely positioned, but the label
had no positioning, so its containing block was the nearest positioned
ancestor. In the setup wizard that is the backdrop-filtered card, which
sits outside the platform list's scroll box. Clicking a checkbox far down
the list (NES in the expanded Nintendo group) focused an input laid out
thousands of pixels below the card, and the browser scrolled the card's
overflow:hidden body to reveal it, blanking the whole step.

Positioning the label contains the input, so focus never scrolls anything
beyond the list itself.

Fixes #4223

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:55:31 -04:00
Georges-Antoine AssiandClaude Opus 5.5 1c491e0aee refactor(devices): pr-ready simplify pass for device installs
- Share one watched-transaction retry loop and live/gone split in the install handler
- Emit independent socket events concurrently
- Drop the dialog's redundant close watcher and repeated current-rom checks
- Reuse the add_device_token fixture and shared header fixtures in tests
- Fix mypy errors in the socket and handler tests

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:49:13 -04:00
Georges-Antoine AssiandClaude Opus 5.5 a1fa09b6c8 fix(scan): anchor rom file category to the folder below the rom root
category_for_path_parts matched every segment of the library-relative
path and returned the first hit in enum order, so inner dump folders
(content/Game, content/CMCmn/demo), the rom folder itself, or even the
platform folder could set a file's category, and a bare `game` segment
outranked an `update` or `dlc` folder above it.

The scanner now passes the path relative to the ROM root, and only the
top folder there decides the category; deeper files inherit it.

Fixes #4581

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:48:11 -04:00
Georges-Antoine AssiandClaude Opus 5.5 3fddda0f21 fix(v2): mount one PDF viewer at a time in the Media tab
vue3-pdf-app wraps a single global pdf.js application and looks up its
containers by fixed element ids. The Manual and Walkthrough subtabs stay
mounted via v-show, so with a PDF in each, both PdfViewers ran against the
same app: the walkthrough rendered into the manual's container (or not at
all), and pages laid out while hidden came up blank.

Only the last shown PDF subtab now mounts a PdfViewer, and only once it has
been shown.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:47:40 -04:00
Georges-Antoine AssiandClaude Opus 5.5 bb0de382bb fix(assets): reject emulator names that are not a single folder on upload
POST /api/saves and POST /api/states joined the `emulator` query parameter
straight into the asset path, so a value like `a/b` created nested
directories and landed in file_path. Both endpoints now return 400 unless
the name survives sanitize_filename unchanged, and cap it at the column
length so an over-long value is a 422 instead of a database error.

Fixes #4839

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:47:33 -04:00
Georges-Antoine AssiandClaude Opus 5.5 f40240d287 fix(ss): map Win9x to ScreenScraper's generic Windows system
ScreenScraper's dedicated "PC Win9X" system (137) holds very few games;
Win9x titles are filed under the generic "PC Windows" system (138), which
acts as a fall-through like MAME does for arcade. Searching 137 meant
Win9x games only matched when scraped by ID.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:45:50 -04:00
Sam DornanandClaude Opus 5.5 420ccbc38a fix(hasheous): match multi-file roms when one file is unknown (#4846)
* fix(hasheous): match multi-file roms when one file is unknown

Hasheous' /Lookup/ByHash returns 404 for a batch as soon as any one SHA-1
in it is unknown, even when the rest match. A multi-disc CHD set with an
.m3u, a readme or an off-set disc beside it therefore never matched, e.g.
Final Fantasy VII (USA), whose three MAMERedump CHD hashes match on their
own.

- Leave .m3u playlists out of the lookup.
- When a multi-file batch misses, retry each file on its own, largest
  first, and take the first match.
- Read the TheGamesDB id from "TheGamesDb", the spelling Hasheous returns,
  so tgdb_id is no longer always dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TZeLXgA178Biwj7eUReck4

* fix(hasheous): require per-file matches to agree on one game

The per-file retry took the first file Hasheous knew, so a folder holding
an unknown game next to a known file from another game (a demo disc, a
bonus CD) could take on that other game's ids. Every file is now asked
and the match stands only when all known files name the same game;
conflicting answers match nothing and leave the lookup inconclusive.

A failed per-file request still ends the lookup as inconclusive, since
agreement can't be vouched for without every answer. The largest-first
ordering is gone, as nothing depends on the order any more.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TZeLXgA178Biwj7eUReck4

* test(hasheous): expect the per-file retry after a missed batch

test_lookup_rom_sends_all_top_level_file_hashes mocks a miss for two
files, which now also asks Hasheous about each file alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TZeLXgA178Biwj7eUReck4

* test(hasheous): build lookup fixtures as real RomFiles

The lookup tests cast MagicMocks to RomFile, which would hand lookup_rom
attributes a real RomFile lacks. They now build RomFiles, so file_extension
and lookup_hashes come from the model itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TZeLXgA178Biwj7eUReck4

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-27 13:45:09 -05:00
Georges-Antoine AssiandClaude Opus 5.5 d4e60af445 fix(player): boot cue/gdi/ccd/mds disc sets whole by default
The EmulatorJS player defaulted to the first file of a multi-file rom, so a
BIN/CUE (or GDI, CloneCD, Alcohol) set booted either the sheet or a single
track alone and never reached the game. Default such sets to the whole-rom
download, which already ships every track with a generated playlist of the
sheets. A remembered per-rom choice still wins. The v1 player reuses the
same helper.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:44:20 -04:00
Georges-Antoine Assi 61d0ef332b Merge pull request #4845 from rommapp/claude/language-skills-humanizer-g30s9a
chore(skills): vendor humanizer and run it after pr-ready and the release drafts
2026-09-27 14:41:26 -04:00
Georges-Antoine AssiandClaude Opus 5.5 077f27e68d fix(devices): pr-ready code-review fixes for device installs
- Reject installs of a rom missing from the filesystem
- Keep a request created during a device discard indexed
- Hold a strong reference to the device socket keepalive task
- Never raise from emit_permissions_changed after a committed change
- Map only a missing install request to 404, not any KeyError

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 14:39:38 -04:00
Claude 35d21af392 chore(skills): humanize release notes and announcement drafts
Both drafting skills now apply the vendored humanizer before their handover
checks, so the character count and PR accounting run on the rewritten text.
The announcement keeps its voice from the past posts it already reads; the
release notes limit the pass to Highlights and callouts written for this tag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XXuG7JWgxP6DPLB182yD9v
2026-09-27 18:38:28 +00:00
Sam DornanandClaude Opus 5.5 146464551b perf(scan): download a rom's media files concurrently (#4842)
* perf(scan): download a rom's media files concurrently

download_rom_resources fetched the cover, manual, each screenshot, each
provider media type and every RetroAchievements badge one after another,
so a single ROM never used more than one of the ScreenScraper account's
request slots.

They now download together. ScreenScraper media still goes through its
existing thread and rate limiters, and badges share a new cap of 8 in
flight across all scans. The path columns and changed provider media
dicts are written back in one update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DAQNWjH7teMsMkFMuUrLTf

* style(tests): sort imports with the Trunk isort config

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DAQNWjH7teMsMkFMuUrLTf

* fix(scan): code-review fixes for concurrent media downloads

- Run provider media dicts one after another, since they share paths
- Await every download before raising the first failure (gather_all)
- Fetch each duplicate media or badge path once (store_ra_badges)
- Reuse store_ra_badges in update_rom
- Share the in-flight test helper and trim comments

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DAQNWjH7teMsMkFMuUrLTf

* refactor(scan): simplify pass on concurrent media downloads

- Move gather_all to utils/concurrency and unpack its results directly
- Let write_file_streamed create the badge directory instead of every badge
- Share the in-flight test helper through tests/concurrency_stubs
- Stub downloads with AsyncMock through one fixture, drop a redundant test

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DAQNWjH7teMsMkFMuUrLTf

* fix(scan): save the paths that landed when a provider download fails

Cover, manual and screenshot paths are written before a provider media or
badge failure is re-raised, so a failure there no longer loses them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DAQNWjH7teMsMkFMuUrLTf

* fix(scan): save each download group's results independently

Paths, provider media and badges each keep their own outcome, and every
change that landed is written before the first failure is re-raised. The
test builds a real Rom instead of a namespace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DAQNWjH7teMsMkFMuUrLTf

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-27 13:35:21 -05:00
Sam DornanandClaude Opus 5.5 6b13fd1698 chore(deps): bump redis to 8.1 and fakeredis to 2.38, drop types-redis (#4844)
* chore(deps): bump redis to 8.1 and fakeredis to 2.38

redis-py 6.2.0 -> 8.1.0 and fakeredis 2.30.1 -> 2.38.0. RQ 2.12
(redis!=6,>=3.5) and fakeredis (redis>=4.3) both accept redis 8.

fakeredis 2.38 types FakeServer's version argument as a tuple, so pass
(7,) instead of 7. Behaviour is unchanged since fakeredis normalises an
int to a one-element tuple.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx

* chore(deps): drop types-redis in favour of redis-py's own types

types-redis only covers redis 4.6 and shadowed the inline types that
redis 8 ships, so mypy was checking calls against a stale API. Removing
it also drops its transitive stub packages (types-cffi, types-pyopenssl,
types-setuptools), which nothing else uses.

redis-py's types differ from the stubs in three ways:
- Redis, AsyncRedis and Pipeline are not generic, so drop the [str] and
  [bytes] parameters.
- Responses are typed bytes | str regardless of decode_responses, so cast
  at the few call sites that rely on the decoded (or raw) type.
- watch, unwatch, multi, aclose and execute_command are unannotated, so
  add redis to untyped_calls_exclude, as is done for rq and apprise.

Also replace setex, deprecated in redis 8, with set(..., ex=...).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx

* fix(device-auth): replace deprecated GETSET with SET ... GET

code-review step. GETSET is deprecated since Redis 6.2 in favour of
SET with GET. Using it with ex= also sets the poll key's TTL in the same
command, instead of a separate EXPIRE that a crash could skip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-27 13:28:09 -05:00