Sample configs shipped with newer releases can reference media types or
options unknown to older versions; previously the loader called
sys.exit(3) and refused to boot. Now drop unknown scan.media entries,
fall back to defaults for unknown gamelist media thumbnail/image values,
and recover from YAML parse errors with a clear critical log instead of
a traceback. Type-mismatch validation still hard-fails since those are
real misconfigurations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UPDATE scans with a known launchbox_id were calling get_rom_by_id(),
which only returns remote data and bypassed get_rom()'s local-first
merge — so local-only fields like Notes were getting clobbered and
local media matching fidelity dropped. get_rom_by_id() now optionally
takes fs_name/platform_slug and merges the local entry when its
DatabaseID matches the requested id.
Also fixes populate_rom_specific_paths writing every video as
video.mp4 regardless of source extension; since store_media_file is a
byte copy, .mkv/.webm contents would be served as .mp4 and break MIME
sniffing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds rejection + acceptance tests for update_rom, add_collection, and
update_collection artwork uploads, mirroring the existing avatar tests:
non-image content returns 400, and a real PNG uploaded under a misleading
filename like payload.html is stored with the trusted .png extension.
Also fixes two `return HTTPException(...)` → `raise` in raw.py so the 404
path actually surfaces instead of silently returning the exception object.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avatar, ROM artwork, and collection artwork uploads now sniff the file
header with libmagic and reject anything that isn't PNG/JPEG/WebP/GIF,
saving the file with an extension derived from the detected MIME rather
than the user-supplied filename. Pairs with the raw asset endpoint,
which decides inline vs attachment from the on-disk extension.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the get_platform_fs_structure, get_firmware_fs_structure,
get_platforms_directory and get_roms_fs_structure tests from mocking
os.path.exists to mocking glob.glob, matching the new STRUCTURE_PATH_B
detection logic. Rename the existing high_priority/normal_structure
variants to structure_a/structure_b for clarity, and fix
test_platform_specific_behavior so its monkeypatch wraps the calls
instead of being applied after them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renaming a rom via PUT /roms/{id} only updated fs_name and its
derivatives, leaving regions/languages/tags/revision/version stale
against the new filename. Re-parse them whenever fs_name changes so
edits like "patapon (Fr En)" -> "Patapon (Fr, En)" are reflected in
the database.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sample_metadata.zip test fixture used invented tag names
(<Description>, FileName ending in .zip). Real LaunchBox Mame.xml
(see backend/tasks/fixtures/launchbox/mame.xml) uses <Name> as the
full title and keys MameFile entries by the stem (e.g. wrlok_l3),
no extension. Read Name instead of Description, and fall back to
the stem when the raw fs_name lookup misses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
remote_media_req built a MediaRequest with platform_name=None, which
made _build_local_media_context bail and never search on-disk
Images/Manuals/Videos. Any rom that matched only via Metadata.xml (no
local XML entry) ended up with images: [] even though LaunchBox art
was sitting on disk. Thread platform_name and fs_name into
remote_media_req, falling back to the remote entry's Platform field
when no slug is available (e.g. lookups by database id).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LaunchBox stores videos under /romm/launchbox/Videos/<Platform>/<Game>.<ext>
but the handler only extracted YouTube IDs; LAUNCHBOX_VIDEOS_DIR was
defined and unused. Add _get_video() to surface local videos as
launchbox-file:// URLs, thread a populate_rom_specific_paths() through
the scan pipeline to set video_path once the rom is known, and mirror
the SS/gamelist branch in the scan socket so store_media_file actually
copies the video into the rom's resource directory. Rom.path_video now
also reads from launchbox_metadata.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The scheduled task populated LAUNCHBOX_MAME_KEY from Mame.xml but no
handler code ever read it, so arcade ROMs like pacman.zip never matched
against Metadata.xml (which keys on full titles like "Pac-Man"). Add
RemoteSource.get_mame_entry() and an ARCADE branch in get_rom() that
resolves the MAME filename to its Description before name lookup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Starlette's TestClient (httpx-based) does not expose body kwargs on the
delete() convenience method; client.request(\"DELETE\", ..., json=...) is
the correct approach. Also switch datetime.utcnow() to
datetime.now(timezone.utc) to silence Python 3.13 deprecation warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch content= to data= for DELETE requests (Starlette TestClient is
requests-based and does not accept content= keyword argument)
- Fix test_bumps_updated_at to record time before the API call and use >=
comparison, avoiding false failures when MariaDB truncates DATETIME to
whole seconds and creation/update land in the same second
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace client.delete(json=...) with content=json.dumps()+Content-Type header
(Starlette TestClient does not forward json= on DELETE requests)
- Adjust duplicate-name test to expect HTTP 500 matching CollectionAlreadyExistsException
- Add description="" to collections created without it to satisfy Pydantic schema
- Strip tzinfo before comparing updated_at to avoid offset-naive/aware TypeError
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace full rom_ids list replacement with atomic POST/DELETE endpoints
that add or remove individual ROMs from a collection. This prevents
concurrent rapid clicks from overwriting each other (last-write-wins).
Also fix missing session.flush() in add_rom_user() and add collection
endpoint tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Callers now pass the full platform dict and rom.fs_extension; the service
normalizes the extension (optional leading dot, case-insensitive) before
checking the compressed-archive skip set, so ROMs stored with bare
extensions like "zip" correctly hit the skip path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RAHasher was being spawned for every hashable ROM regardless of file
type. When the source file is a zip/7z/tar and the RA platform needs
an on-disk disc image (PSX, PS2, PSP, Saturn, Dreamcast, Sega CD,
3DO, PC-FX, Neo Geo CD, TurboGrafx CD, Atari Jaguar CD, Wii), the
subprocess fails with "Unsupported console for buffer hash: {id}"
after paying full process-spawn overhead per ROM — a serious slowdown
when indexing large zipped collections (e.g. myrient PS2/PSP sets).
calculate_hash now short-circuits those combinations with a debug log
and no subprocess. Raw disc images (.iso, .chd, .cue/.bin) and
archives on cartridge platforms still go through RAHasher as before.
Also centralize COMPRESSED_FILE_EXTENSIONS in utils/filesystem.py so
roms_handler (is_compressed_file / hashing), rahasher (skip logic),
and feeds (PKGi passthrough) share one source of truth. The shared
set adds .rar, which is_compressed_file now recognizes too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get_rom now also fetches Named_Snaps, Named_Titles, and Named_Logos
when the matching MetadataMediaType (SCREENSHOT, TITLE_SCREEN, LOGO)
is in SCAN_MEDIA. Box art is still fetched unconditionally — it drives
url_cover and libretro_id. Matching extras are appended to
url_screenshots so the scan_handler artwork priority loop picks them
up without further changes. All enabled listings are fetched
concurrently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SGDB's url_cover used to clobber whatever the artwork priority loop
picked, which meant user-set priorities (e.g. libretro > sgdb) and
manually uploaded covers were silently overridden. SGDB now only
replaces the current url_cover when it outranks every other source
that produced one under SCAN_ARTWORK_PRIORITY, and never over a
manual cover preserved on UPDATE/UNMATCHED scans. Default artwork
priority gains sgdb at the top so existing "SGDB wins" behavior is
preserved for default configs.
On the /search/roms endpoint, libretro is now an enrichment source
alongside SGDB instead of a primary match source: it decorates
entries resolved by IGDB/Moby/SS/Flashpoint/Launchbox with
libretro_id and libretro_url_cover, mirroring how SGDB works.
get_matched_roms_by_name is removed from the libretro handler since
nothing else calls it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Collapse the two parallel id lists and their mirrored chunked-update
loops into a `flips: dict[bool, list[int]]` keyed by desired state, and
drop unused rom assignments in the related tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>