Skip to content

feat(games): give each game its cover from IGDB - #186

Merged
Bakhomious merged 4 commits into
mainfrom
step/games-covers
Oct 30, 2026
Merged

Bakhomious merged 4 commits into
mainfrom
step/games-covers

Conversation

@Bakhomious

Copy link
Copy Markdown
Contributor

Brief

Goal: each game that GET /games/recent answers carries its cover from IGDB, which games asks as the Twitch app that the settings name, and each title is matched at most once a week: the seventh of games-exophase's eleven steps.

Scope:

  • a new package, services/games/internal/covers, which keeps the covers found for the latest list's titles, each for a week at most;
  • the games service's wiring: the setting BAKHYARD_TWITCH_CLIENT_ID, the secret twitch-client-secret, the Twitch and IGDB clients (decisions 304 and 305), and cover in each game of the answer;
  • docs/setup.md and mise.local.example.toml;
  • decision 306 and the step's rows in plan §0 and §8.2.

Out of scope:

  • the laptop's setting and secret (homelab-games);
  • the covers on the page (games-page-covers).

Acceptance criteria:

  • Without BAKHYARD_TWITCH_CLIENT_ID, games starts, reads no secret, asks neither Twitch nor IGDB, and its games have no cover.
  • With it, games reads the secret twitch-client-secret, and when that fails, it exits 1 with read the Twitch client secret: <why>.
  • With both, each game in the answer has cover, the address of its cover on IGDB (decision 305), or no cover when IGDB has none.
  • The titles are asked one after another, in the list's order, the last played first. A title is asked at most once per fetch, however many games bear it, and not again for a week after it was asked, whether IGDB found a cover or not. The first fetch a week or more later asks again.
  • Only the latest list's titles are kept: a title that leaves the list is asked again when it comes back.
  • At the first failure to find a cover in a fetch, games asks no more for that fetch. The games whose titles it didn't find go without covers, the games with a match kept keep theirs, and the list is answered with no failure. games logs find covers with the error, unless it's stopping. The failed title isn't kept, so the next fetch asks again.
  • Coverage is 70% or more per package; mise run check is green.

What changed

  • services/games/internal/covers/covers.go: Cache keeps each title's cover, or that IGDB has none, for a week from when it asked, for the latest list's titles only. Find asks its Finder for the titles not kept, one after another, each once, and stops at the first error.
  • services/games/covers.go:
    • coverCache builds the cache from BAKHYARD_TWITCH_CLIENT_ID, the secret twitch-client-secret, and the Twitch and IGDB clients (decisions 304 and 305), or gives none without the setting;
    • covered gives each game its cover, and logs find covers at the first failure.
  • services/games/internal/api/api.go: Game embeds exophase.Game and adds cover, left out when it's empty.
  • services/games/main.go and exophase.go: routes takes the secrets reader and the three APIs' addresses, and the poll finds the covers once Exophase has answered.
  • Tests:
    • internal/covers/covers_test.go: the order, the week in synctest bubbles, the latest titles and the failure;
    • covers_test.go: the answer with its covers, against stand-ins for Twitch and IGDB; the missing secret; the setting left out; and the log;
    • internal/api/api_test.go: the cover in the JSON;
    • main_test.go and exophase_test.go: routes' new parameters.
  • Docs:
    • docs/setup.md: the setting, and a new IGDB section on registering the Twitch app;
    • mise.local.example.toml and the process-compose description;
    • docs/plan.md: decision 306, the §0 row and the §8.2 bullet.

QA

  • Tests:

    • Every criterion has a test, and the tests pass with -race.
    • Coverage: 100% for covers and api, and 85.7% for the main package, whose main.go isn't counted.
  • Mutants: I ran 34 hand mutants, and all 34 are killed. QA added three tests first, to kill the three that had survived:

    • the Twitch and IGDB stand-ins now listen apart, so swapping the two addresses fails;
    • a fetch that finds every cover must log nothing;
    • the week counts from the asking, checked with a finder that takes a minute to answer.

    The mutants covered:

    • the week's length and bound;
    • asking past a failure, and keeping the failed title;
    • keeping nothing or every title, and the lookup within one list;
    • each error and the log;
    • the setting, the secret's account, and each client's ID, secret and address;
    • the nil cache, omitzero and the field's name;
    • the cache's use in poll and routes.
  • Live: run on port 8301 against a public reference profile, which had 43 games:

    • Without the setting: 43 games, none with cover, and no failure.
    • With the setting and no Keychain item: exit 1, with read the Twitch client secret: no Keychain item with service bakhyard and account twitch-client-secret.
    • With a made-up client ID, and a secret in a BAKHYARD_SECRETS folder:
      • The first answer took 0.8 s: 43 games, no cover, and no failure.
      • games sent one Twitch request, then stopped. The log said find covers err="find the cover of \"…\": get a Twitch token: 400 Bad Request, invalid client".
      • The second request was answered from memory in 6 ms.
    • A real cover waits for the user's Twitch app.
  • Gates: mise run check passed on this branch's tree, merged with main after feat(games): find a game's cover on IGDB #183:

    • golangci-lint: 0 issues;
    • govulncheck: no vulnerabilities;
    • the Go coverage gates: 70% per package, 80% in total, at 96.6% in total, 100% for covers and 85.7% for the main package;
    • Vitest: 1223 tests;
    • Playwright: 386 passed, 8 skipped.

Review

  • Blocking: none.
  • Non-blocking:
    • exophase_test.go still starts its two servers by hand, as serve in covers_test.go now does for the new stand-ins.
    • The first request after a start waits for every title's cover. The reference profile listed 43 games, which would take 11 to 22 seconds at IGDB's 4 requests a second. Decision 300 accepted the wait, and neither the services nor the app time a request out.
    • A title whose search alone fails stops the asking at the same place at each fetch: decision 306's known limit. The log names it.

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

Each game in GET /games/recent now carries its cover from IGDB when
BAKHYARD_TWITCH_CLIENT_ID and the Twitch app's secret are set
(decision 306). The covers package keeps each title's cover, or that
IGDB has none, for a week, for the latest list's titles only. The
first failure stops the asking until the next fetch, and a missing
cover never fails the list.
@Bakhomious
Bakhomious merged commit 078935d into main Oct 30, 2026
2 checks passed
@Bakhomious
Bakhomious deleted the step/games-covers branch October 30, 2026 03:30
Bakhomious added a commit that referenced this pull request Oct 30, 2026
The games service has sent each game's IGDB cover since #186 (decision 306). The Games page now shows it in a column between the day and the title, as the background of a framed blank, so a game without a cover, a cover still loading and one that fails to load all leave the same blank, and screen readers skip it (decision 308).

Merged under the user's standing OK once CI's check was green.
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