Skip to content

feat(games): find a game's cover on IGDB - #183

Merged
Bakhomious merged 3 commits into
mainfrom
step/igdb-search
Oct 30, 2026
Merged

Bakhomious merged 3 commits into
mainfrom
step/igdb-search

Conversation

@Bakhomious

Copy link
Copy Markdown
Contributor

Brief

Goal: the games service can find a game's cover on IGDB with a filtered search and an exact-name fallback (decision 68), within IGDB's 4 requests a second: the sixth of games-exophase's eleven steps.

Scope: a new package, services/games/internal/igdb. Its client asks IGDB's games endpoint for a title with the app token that Twitch gives (decision 304) and returns the address of the title's 264×374 cover. Decision 305 and the step's rows in plan §0 and §8.2 come with it.

Out of scope:

  • the cover in the games' answer and the matches kept (games-covers);
  • the Twitch settings, the secret and their docs (games-covers);
  • the laptop (homelab-games).

Acceptance criteria:

  • Cover sends POST /v4/games with Client-ID, Authorization: Bearer and the token, Accept: application/json, and the query as text/plain in the body.
  • The first query searches for the title among main games, expansions, standalone expansions, remakes, remasters, expanded games and ports that aren't versions of another game, and takes the top result.
  • When the top result's name is the title, ignoring case, it's the match, and nothing else is asked. Otherwise an unfiltered search for a game named exactly the title, ignoring case, runs; its result wins when it finds one, and the filtered top result stays when it doesn't.
  • The answer is https://images.igdb.com/igdb/image/upload/t_cover_big/<image_id>.jpg, or "" when there's no match or the match has no cover. An empty title gives "" without asking.
  • A title's backslashes and quotes are escaped in the queries, and its control characters dropped.
  • Requests go one at a time, and each starts at least 250 ms after the previous one started. A call whose context ends while it waits returns the context's error.
  • When IGDB answers 401, the token is dropped and the query is asked once more with a new one.
  • Errors start with find the cover of "<title>":, then ask IGDB: and the status or the transport's error, read IGDB's answer:, or the token's error. Each request gives up after 10 seconds, and an answer over 64 KiB is refused.
  • No error shows the token.
  • Coverage is 70% or more; mise run check is green.

What changed

  • services/games/internal/igdb/client.go: New gives the Client that asks IGDB as a Twitch app. Cover matches the title with the two queries and returns its cover's address. Tokens is what the client needs of *twitch.Tokens.
  • services/games/internal/igdb/ask.go: one request at a time, at least 250 ms apart, with a new token once after a 401, and the answer read.
  • Tests:
    • client_test.go: the request, the match, the escaping and the empty title, against an httptest stand-in for IGDB;
    • fail_test.go: the refused token and the errors;
    • ask_internal_test.go: the 250 ms, one request at a time, the waiting calls' contexts and the 10 seconds, against an in-memory IGDB in synctest bubbles;
    • standin_test.go: the stand-ins.
  • docs/plan.md: decision 305, the §0 row and the §8.2 bullet.

QA

  • Tests:
    • Every criterion has a test, and the tests pass with -race.
    • Coverage: 98.6%. The one statement left is http.NewRequestWithContext's error, which a parsed URL and a fixed method never hit.
  • Mutants: I ran 50 hand mutants, and all 50 are killed. They covered:
    • each part of the two queries, the cover's address and the match's four outcomes;
    • the escaping, the 250 ms, the turn and both waits' contexts;
    • the 401 retry and Drop, each header, the status, the 64 KiB and the 10 seconds;
    • each error's prefix and the body's close. synctest catches a body left open as a leaked goroutine.
  • IGDB's docs:
    • The game type ids match IGDB's enum: main game 0, expansion 2, standalone expansion 4, remake 8, remaster 9, expanded game 10, port 11, and update 14, which the filter leaves out.
    • category is deprecated for game_type, which the query uses.
    • IGDB keeps an image that it removes or replaces for 30 days.
  • Live:
    • With a made-up client ID and token, the real IGDB answers 401 with "Authorization Failure".
    • Our client against it drops both tokens and gives find the cover of "Moon \"Garden\"": ask IGDB: 401 Unauthorized.
    • A real search waits for the user's Twitch app.
  • Gates: mise run check passed on this branch's tree, merged with main after feat(notes): save a note from its editor #180:
    • golangci-lint: 0 issues;
    • govulncheck: no vulnerabilities;
    • the Go coverage gates: 70% per package, 80% in total, at 96.5% in total and 98.6% for igdb;
    • Vitest: 1223 tests;
    • Playwright: 386 passed, 8 skipped.

Review

  • Blocking: none.
  • Fixed in review: the field with the endpoint's address was games, like the slices of games around it; it's now endpoint.
  • Non-blocking:
    • The httptest tests spend about 3 seconds of real time in the 250 ms gap, since a bubble can't hold a network server.
    • When several games are named exactly the title, the exact-name search takes IGDB's first, in an order that the query doesn't set. It runs only when the filtered top result has another name.
    • The gap holds within one Client. games-covers makes one per process, but the Mac and the laptop share IGDB's limit when they use the same Twitch app, so a 429 could fail a title until a later fetch.
    • Nothing uses New yet: games-covers will.

Merged under the user's standing OK once CI's check is green.

The games service will show each game's cover from IGDB (decision 68).
This adds its IGDB client: Cover searches for the title among main
games, expansions, remakes, remasters and ports, falls back to a game
named exactly the title, and returns the cover's address. It asks one
request at a time, 250 ms apart, under IGDB's four a second, and asks
again once with a new Twitch token after a 401. Nothing uses it yet:
games-covers wires it in (decision 305).
@Bakhomious
Bakhomious merged commit dcf3670 into main Oct 30, 2026
2 checks passed
@Bakhomious
Bakhomious deleted the step/igdb-search branch October 30, 2026 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant