feat(library): add IGDB editions, game links and auditor queue

Tables game_edition (IGDB "same game" id per library key), game_link
(global sets set by auditors), game_link_pair (queue of IGDB and
requested pairs), game_link_request and account_name. Fetch IGDB
editions, link and unlink with audit rows, serve the paged auditor queue
with request counts. WireMock recordings for the editions query.
This commit is contained in:
lobo committed 2026-10-02 09:10:56 +02:00
1 parent a12ed20705
commit e806670d26
43 files changed
+1877 -22

No files matched your search

+28 -1
View File
@@ -67,6 +67,10 @@ To Be Played is split into three repositories:
backgrounds (V27 widens `reward_equipped.slot`); a `BUY` row proves ownership,
`reward_equipped` holds what is worn. A platform admin owns every item (the role of the login token, or the `role`
claim of the profile ticket for visitors).
- **Editions and links**: editions, remasters and ports of one game stay separate library rows, but an AUDITOR can
link their keys as one game for every user (see "Editions of one game"). IGDB suggests pairs, users can ask for
one, and one queue holds both. Every link, unlink and dismissal goes to `admin_audit`
(`GAME_LINK`, `GAME_UNLINK`, `GAME_LINK_DISMISS`).
- **Wishlist**: synced from Steam with store price and discount.
- **Broken image repair**: the frontend reports a Steam cover or achievement icon that does not load. At 01:30
(Europe/Berlin) a job checks up to 200 reports. A cover that loads again is left alone; else the store's hashed
@@ -131,6 +135,10 @@ ArchUnit role rules (`RoleAnnotationRules`) and `src/test/resources/contract/aut
| `PATCH` | `/api/library/games/{appid}/backlog` (only the sent fields; `null` clears one) | JWT |
| `PUT`, `DELETE` | `/api/library/games/{appid}/corrections/{field}` (`{value}`; `name`, `image`, `speedrun`, `moods`, `endless`, `genres`; `204`, `400 INVALID_CORRECTION`) | JWT, AUDITOR |
| `POST` | `/api/library/games/{appid}/merge` (`{otherAppid}`; `200 {appid}` = the key that stays, `400 SAME_GAME` / `STEAM_GAMES`, `404 GAME_NOT_FOUND`) | JWT, AUDITOR |
| `POST`, `DELETE` | `/api/library/games/{appid}/link` (POST `{otherAppid}`, `204`, `400 SAME_GAME`, `404 GAME_NOT_FOUND`, `409 ALREADY_LINKED`; DELETE takes the game out of its set, `204`, `404 NOT_LINKED`) | JWT, AUDITOR |
| `GET` | `/api/library/link-queue?page=&size=` (`PageDto` of `{a, b, igdb, requests, createdAt}`: both games, most requested first; size 20, 40 or 80) | JWT, AUDITOR |
| `DELETE` | `/api/library/link-queue/{appid}/{otherAppid}` (dismiss; `204`, `404 PAIR_NOT_OPEN`) | JWT, AUDITOR |
| `POST` | `/api/library/games/{appid}/link-requests` (`{otherAppid}`, both in the caller's library; `204`, `400 SAME_GAME`, `404 GAME_NOT_FOUND`, `409 ALREADY_LINKED` / `PAIR_DISMISSED`, `429 TOO_MANY_REQUESTS`) | JWT |
| `PUT` | `/api/library/games/{appid}/status` (status only, keeps rating, notes, priority) | JWT |
| `PUT` | `/api/library/games/{appid}/finished-hint` (`{dismissed: ENDING\|PLAYTIME}`, "Not yet"; `204`) | JWT |
| `PUT` | `/api/library/games/{appid}/hidden` (`{hidden}`; `409 MANUAL_GAME` for a game added by hand) | JWT |
@@ -319,13 +327,30 @@ since the sync would bring both back. In one transaction:
current status the user chose (both: the one chosen last); a gap is filled from the other row. Source, platform and playtime stay, unless the
Steam copy is no longer owned. Both status histories stay; the merge itself logs no status change.
- The shared rows of the old key (catalog row, moods, speedrun, time to beat, corrections, broken images) are
deleted.
deleted. So are its queue pairs, its requests and its `game_edition` row; its set row goes to the kept key (see
"Editions of one game").
- `game_alias` points the old key to the new one (kept flat), so a later add of the old IGDB game lands on the new
key. `admin_audit` gets one `GAME_MERGE` row: who, both keys, both names and how many rows moved or were combined.
There is no undo. The game search shows games already in ToBePlayed first, with their catalog name and image, and
games other users added by hand that IGDB did not return; `inCatalog` marks both (only those can be merged).
### Editions of one game
IGDB tells which library keys are one game: `game_edition` (V28) holds a "same game" key per library key, refreshed
like `game_mood` (stale check, nightly sync, add, detail view; `app.library.edition-ttl-days`). The key is
`version_parent` for an edition, else `parent_game` when `game_type` is 9 (remaster) or 11 (port), else the game's own
IGDB id; remakes (8) keep their own id. `null` when IGDB does not know the game.
- `game_link(appid, set_id)`: the keys an auditor linked, for every user. A set is every row with one `set_id`.
An unlink takes one key out; a set of one is dropped. A merge puts the kept key in the merged key's set; when both have a set, the sets join.
- `game_link_pair` is the auditor queue (`OPEN`, `DISMISSED`, `LINKED`). IGDB pairs are inserted when the queue is
read; a row in any state keeps the pair out for good. An unlink dismisses the pairs of that key with the former
members. `game_link_request` holds who asked for a pair (the queue shows only the count). A user asks for a pair of
two games of their own library; at most 20 open requests per user (a per-user advisory lock makes the cap hold for parallel calls), a dismissed pair answers `409 PAIR_DISMISSED`.
- The game detail has `linked` and `editions` (the user's other listed games of the set); a game row has `editions`
(size of the set in the user's library, `null` below 2).
### Deleted accounts
Auth drives it (see Auth's readme). `POST /internal/accounts/deleted` adds a tombstone in `deleted_user`: the user
@@ -434,6 +459,8 @@ What the `local` profile does:
- The browser still loads game pictures from the Steam and IGDB image servers; offline they stay gray.
- "Sign in through Steam" still opens the real Steam page (no key needed). The dev user is already linked.
- A request to WireMock that has no recording answers 404 and shows up in Core's log.
- To see an IGDB link suggestion: add Hollow Knight by hand (game search, PS5). It shares IGDB's "same game" key with the
Steam copy, so the link queue (auditor) lists the pair with the IGDB badge.
To change the recorded answers, edit the JSON in `local/wiremock/mappings` (which request) and
`local/wiremock/__files` (the answer), then restart Core (it restarts the container). `LocalMocksParseTest` checks