Media Data Management for a Handheld Launcher: SteamGridDB Covers, Image Specs and Metadata Sources

The iiSU launcher on my handheld is a visually heavy front end: the game library is a grid of square covers, selecting a game swaps the whole background for a landscape hero image, and a text logo floats over the centre of that image. My library has more than forty Switch games. At first most covers were missing, and the ones that existed differed in size, aspect ratio and transparency; titles and release information were missing too. Finding and cropping images by hand, then filling in metadata one entry at a time, got old by the tenth game, so I turned it into a batch pipeline. Covers and metadata look like two jobs, but they are the same kind of job: turning outside media data into the format the launcher can actually use.

The launcher wants three images, each covering a different part of the screen

My first mistake was treating the three images as one kind of cover. They map to different regions and have different specs.

icon.png is the square cover in the grid and the game card’s main image: 512×512, opaque RGB. hero_1.jpg is the full-screen background shown when a game is selected: landscape 16:9, which I standardise to 1280×720, center-cropped to avoid stretching. title.png is the easiest to get wrong — it is not a cover but the text logo overlay that floats above the background, so it must be a transparent PNG (RGBA) that is opaque only where the logo is, again 1280×720.

There is a simple way to tell them apart: view the image on a white background. If it fills the frame with a background colour, it is a cover; if it shows one line of a game title with transparency around it, it is a logo overlay. Getting this distinction right is what keeps the interface from showing a square logo smeared across the middle of the background or a background stretched into a square. Some games have no landscape logo on SteamGridDB at all, and forcing a square one in just produces a smeared blob in the centre, so for those I delete title.png and let no overlay appear.

The three SteamGridDB endpoints

The three image types come from three resource endpoints, requested with the same game ID. Square covers use grids/game/{id} with types=static&dimensions=512x512, which returns something close to the target. Transparent text logos use logos/game/{id}, and here I pick the one with a landscape ratio above 1.8; otherwise the result is a nearly square logo that looks wrong floating over the background. Hero banners use heroes/game/{id}, where the source is usually 1920×620 and has to be cropped to 16:9.

The endpoints themselves are simple; the error handling is what costs time. When an asset is missing, SteamGridDB returns a solid black placeholder of around 18KB instead of an error, so every download needs a black-image check or the background silently turns black. The free key has a request quota and batch fetches occasionally 404. For the same URL, curl succeeds where Python’s urllib returns 404, so I switched to pulling images with curl to sidestep the difference. Game IDs come from searching SteamGridDB by name first, and it is worth confirming the match before fetching — picking the wrong game and attaching its covers is harder to notice than having no cover at all.

Normalising everything to fixed specs in Python

Downloading is only the first step; the real work is cropping wildly different sources into one consistent set. A square cover can be any square source (a native 1024×1024, say), resized to 512×512; with no native square, a portrait image (commonly 600×900) is center-cropped to a square and then resized. Hero banners are cropped to 16:9 from the centre and scaled to 1280×720. Transparent logos keep their RGBA channel and are only scaled proportionally, never background-cropped.

from PIL import Image, ImageStat

def center_crop(im, tw, th):
    w, h = im.size
    scale = max(tw / w, th / h)
    im = im.resize((round(w * scale), round(h * scale)))
    w, h = im.size
    left, top = (w - tw) // 2, (h - th) // 2
    return im.crop((left, top, left + tw, top + th))

def is_black(path):
    p = Image.open(path).convert("RGB").resize((32, 32))
    mean = ImageStat.Stat(p).mean          # three-channel mean near 0 = placeholder black
    return max(mean) < 4

Processed square covers are saved as opaque PNG (RGB), logo overlays as PNG with alpha (RGBA), backgrounds as JPEG. The three names are fixed as icon.png, title.png and hero_1.jpg, placed in the launcher’s expected game folder, assets/media/roms/consoles/<platform>/<ROM name>/. The same logic needs a different spec on 3DS: its logo overlay is a landscape transparent banner, sometimes close to 4998×2332, so the Switch 1280×720 does not carry over. I ended up parameterising the specs per platform rather than hard-coding one size.

Adding new games and rebuilding the index

The launcher does not scan images in real time; it indexes lazily. After placing files, the rom_asset_index.fb file must be deleted and the launcher restarted so it rebuilds, otherwise new covers never appear. That index lives under assets/media/roms/consoles/<platform>/. I did not know this at first, and spent a long time restarting the launcher wondering why nothing changed.

There is also a rule tied to ROM naming: the cover folder name must match the ROM filename without its extension. After standardising names to <official English name> [<TitleID>].xci, any rename breaks the folder match and forces a reindex and refetch. Saves are unaffected — the emulator identifies games by TitleID, so changing a filename leaves saves and mods intact. That makes it worth settling the naming convention before importing, rather than after all the covers are in place.

Where the metadata comes from: four sources, four gates

Covers are only half of the media data. The other half is titles and release information, and the launcher has four metadata sources to choose from, each with its own gate. I tried all four.

IGDB, the launcher’s default, wants a Twitch key first

The source iiSU ships with is IGDB. Its data is good, but getting in requires a Twitch key first — IGDB’s API sits under Twitch’s account system, so a Twitch account, a developer app and an issued key all come before a single query. For someone who just wants a handheld’s covers to look decent, that is a heavy prerequisite, and it is why I never made it the primary.

SteamGridDB is the least painful one

SteamGridDB has the lowest barrier: a free key from the personal settings page, and then image assets are directly reachable; its three endpoints are covered above. It has traps of its own, but the point is that among the four it is the least fuss — no account review, no developer status, a key and nothing else.

ScreenScraper wants developer credentials

ScreenScraper is the veteran scraper, with a richer set of metadata fields than SteamGridDB, which suits anyone who wants release year, player count and a synopsis in the same pull. But it stops at a credential gate: its API requires a devid and a devpassword, which are developer credentials, not a normal user’s login. iiSU bundles a devid, but that one is invalid and every request comes back as Erreur de login. Registering an ordinary account is not enough — an account only logs into the website; using the API means applying for developer status and getting a personal devid/devpassword. So I file this route under “worth the trouble when fuller metadata is wanted”, not as a daily default.

TheGamesDB errors out on its free API, and has a version bug

TheGamesDB would be the natural free fallback, but it is effectively unusable. Its free API rejects requests outright (HTTP 418), so not even a normal response comes back; worse, iiSU 0.0.7.4 has a known bug on this source (issue #401 in the project), so even if the API answered, searches would still return nothing. With both problems stacked, this source can be skipped in practice.

How to choose

Put the four side by side and the trade-off is clear.

Day to day, use SteamGridDB’s free key: it covers all three asset types at the lowest application cost. For richer metadata fields, go apply for ScreenScraper’s developer credentials. Keep IGDB as a backup for when setting up a Twitch key is acceptable. TheGamesDB can be ignored for now.

When a match fails, go manual

Whichever source is in use, obscure games failing to match is normal. The launcher keeps a manual path: for a given ROM, “Get/Update (Custom) Data for ROM” lets a title be typed in and the correct entry selected, and covers and metadata are then fetched against that match. Several obscure games in my library were covered this way.

Once the pipeline ran smoothly, more than forty Switch icons were unified to SteamGridDB square art, with about thirty-six also getting a transparent logo and hero banner, and a few staying icon-only where no suitable asset existed; on the metadata side, SteamGridDB is the base and obscure titles are matched by hand. Covers and metadata are two sides of one job: the launcher does not want the files themselves, it wants them in the format and entries it recognises. The hard-won lessons are not in the APIs but in the traps — the division of labour between the three images, the black placeholder, the index rebuild, the per-platform differences, and each source’s account and permission system.

Article Link:

https://time-friend.com/en/archive/handheld-media-data-management/

# Related Articles