feat(game): add screenshot media library infrastructure#4585
Open
wescopeland wants to merge 2 commits intoRetroAchievements:masterfrom
Open
feat(game): add screenshot media library infrastructure#4585wescopeland wants to merge 2 commits intoRetroAchievements:masterfrom
wescopeland wants to merge 2 commits intoRetroAchievements:masterfrom
Conversation
wescopeland
commented
Feb 22, 2026
Member
Author
There was a problem hiding this comment.
Honestly, most of my time working on this PR was spent here in this migration.
I've done a ton of research and referenced many different sources. Still, as much as I've tried, I'm sure I've made a mistake somewhere. However, given this lives in the DB, we can always change these configuration settings without needing to do a code deploy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Games currently support a single ingame screenshot and a single title screenshot, stored as varchar columns on the
gamestable. Uploads are capped at 1MB and forcibly downscaled to 320x240 via GD. Additionally, the original file is destroyed, meaning we are deliberately incurring data loss.This PR creates the infrastructure for multiple screenshots per game using Spatie MediaLibrary, adds a dedicated Media page for
GameResourcein Filament, and preserves backward compatibility for all existing consumers of the current image assets in thegamestable.Architecture Explanation
game_screenshotstable +GameScreenshotmodelSee migration
Screenshot business logic gets its own dedicated table. Each row has a type (title / ingame / completion), primary flag, approval status, and ordering via
order_column.completionis a new type that is meant to serve as the opposite of a title screen - it's a screen showing "The End" that may eventually be used foruser2beaten showcases.The primary flag is used to determine what the "canonical" ingame image is when multiple uploads are supported in a subsequent PR.
The approval status is used to support an approval queue, facilitating player uploads of content, which will also be handled in a subsequent PR.
System resolution metadata
A JSON
screenshot_resolutionscolumn onsystemsstores valid native output resolutions per system (eg: SNES = 256x224, Saturn = 14 valid resolutions). A booleansupports_resolution_scalingcolumn flags systems whose emulators support internal resolution scaling (PSP, PS1, PS2, N64, GameCube, Wii, Dreamcast, DS, DSi, and 3DS). Upload validation checks exact resolution matches and accepts 2x/3x integer multiples for scaling-enabled systems. Systems will null resolutions (computers, arcade) allow any dimensions.Upload flow
All uploads go through a new
AddGameScreenshotAction, which validates file constraints (4MB max, 64x64 min, 1920x1080 max, PNG/JPEG/WebP only, no animation), checks SHA1 for duplicates and known placeholders, enforces per-type caps (20 ingame, 1 title, 1 completion), and validates resolution against the system.For this piece of work, every Filament upload sets the new screenshot as primary for the type, which matches current production behavior where each upload replaces the previous one. "Demoted" screenshots are set to
pendingstatus to be reviewed later when multiple files are supported in a subsequent PR.Backwards compatibility
GameScreenshotObserverautomatically syncs the primary screenshot's legacy path back to theimage_ingame_asset_pathcolumn on thegamestable. All existing consumers (V1 API, game pages, etc) continue working without changes.Conversions
Original files are now stored on S3. Spatie generates sm (320px), md (640px), and lg (1280px) conversions in both WebP and AVIF. Small images are never upscaled due to the code using
Fit::Max. These conversions aren't rendered in the React front-end yet.A subsequent PR will include a command to backfill all existing legacy images to match these conversions, and flag the ones that fail to meet system-level resolution constraints.
Media Page
As mentioned above, a new "Media" tab on
GameResourceconsolidates all game media management: badge, box art, title screenshot, banner, and ingame screenshots. The media section has been removed from the Edit page.How to test this PR
Similar to the banners PR, ensure these are in your .env:
Navigate to any game in Filament and click the Media tab, ie: http://localhost:64000/manage/games/4874/media

In the In-Game Screenshot section, upload a file and click the Save Changes button:

You should now be able to verify:
game_screenshotsrow is created withis_primary = 1andstatus = approved.mediarow is created withcollection_name = screenshotsandcustom_propertiescontainingsha1andlegacy_path.image_ingame_asset_pathcolumn is updated to the new/Images/NNNNNN.pngpath.Remember, Minio is accessed at http://localhost:64041/buckets and the credentials are
retroachievements / retroachievements.Visit the game page for the game. The ingame screenshot should render correctly (everything else being broken is expected given our env vars changes):

Upload a second in-game screenshot for the same game. Verify the first screenshot is demoted via

is_primary = 0andstatus = pending. The game page should now show the new screenshot:What's next
This is the first of several phases for this work.
Tentatively, I envision the following phases looking like this:
The second phase will be for backfilling all existing legacy screenshots into the new system, migrating the game title screenshots to Media Library, and adding gallery management in Filament (view existing screenshots, set screenshots to primary, delete stuff, reorder stuff).
The third phase will be for viewing multiple ingame screenshots on the game page.
The fourth phase will be for supporting player uploads, which go into a Filament-powered review queue (really need to think through this).