Sort, total playtime, most played, genres, moods and the public
profile use the set total of an entry, as the library card does.
New view user_game_entry_playtime (V45) is the one SQL rule.
Store PSN play per title. A PS title on a Steam row keeps its minutes.
Steam rtime_last_played now fills last played.
Dropped? questions read the newest PSN last played too.
Gitea #34 slice 2 of 3. Lists and stats count a set once.
V43 views map an entry to its editions and platforms.
Remove the hideDuplicates param; GameDto gets platforms[].
One entry per game across platforms (Gitea #34, slice 1 of 3).
Same IGDB game is linked; a user can split one key off its set.
V42 keeps status, rating and notes per set; rewards pay once per set.
Add open questions per game: Start it?, Finished?, Dropped?, Rate it?.
A "No" is saved in user_game_question (V41) and snoozes the question.
Account cleanup clears the new table too. Refs Gitea #32.
Sets map by exact title id, else by name and platform.
Trophy gains count as achievements; platinum or all earned is perfect.
Max 30 PSN calls per run; trophy lists load only when changed.
Hidden trophies stay hidden (name and detail) until earned.
Exact IGDB matches are added; unsure titles wait in a review import.
Playtime and last played come from PSN: summed per game, not editable.
PSN games give no Pile Points. Users with only a PSN link sync too
(V39 drops steam_id NOT NULL). Linking starts a sync, or schedules one
for the end of the cooldown. Inbox notes 2 days before the PSN link
ends and when it has ended.
The NPSSO is used once to get a refresh token; only that token is
stored, sealed with AES-GCM (SecretBox, key APP_PSN_TOKEN_KEY; no key
= feature off). One-time import of played PS4/PS5 titles through the
import pipeline as source PSN. Unlink keeps the games. One PSN call
per user at a time. IGDB search now sends UTF-8, so non-ASCII names
like "Yōtei" match again.
Backlog games a followed user finished or played >= 5 h. One game per
friend; strongest signal first (finished, then playtime). Requires an
active follow of a public profile that shows SUMMARY or STATUS. Skips
DROPPED games, blocked and deleted users, hidden and removed games.
New V37 tables game_credit and game_credit_check store IGDB
collections and developer/publisher companies per game.
A scheduled job backfills existing games (250 per 30 s);
sync, import, manual add, merge and detail page refresh them.
SERIES row: next part of a collection the user played.
STUDIO row: backlog games from developers of top played games.
Picker returns categories in fixed order: NEXT_UP, SIMILAR, LONGEST,
RANDOM, each with up to 3 games. Empty rows are left out.
Param `count` is removed; new optional `category` loads one row (reroll).
Budget and exclude apply to all rows; mood only to RANDOM.
SIMILAR takes the 3 most played games, one similar game each.
Remove PickMix.
Picker slots rotate three sources: backlog games that share moods with
one of the top 5 played games, oldest first seen, and random. Each game
carries a reason (source, anchor game, first seen). Empty slots fall
back to random. Issue #28.
V32 stores the last role change per user, sent by Auth. A token
issued before that change, or of a blocked user, gets 401 within
about 30 s instead of working until it expires.
The number and list share the same SQL query, so they cannot drift.
StatKey enum maps each statistic to its query, validation rules, and visibility.
StatGamesService reads games per key with 40-game pages; repositories share the
same set logic as the stat counts. Three endpoints open games: own library stats,
public profile stats (with ticket checks), and public landing-page stats (cached
10 min). Rewards track and bonus rows support drill-down too.
Five progress tracks: games finished, completed, weekly finish
streaks, pile-down months and play-more months. Settle books UNLOCK
rows once, also retroactively, when a track reaches an item's target.
12 unlock-only items (incl. the Steam skin) and 3 skins to buy. The
wallet returns progress; buying an unlock item answers NOT_FOR_SALE.
V29 adds game_merge_undo and game_merge_undo_user: every new merge saves
a copy of what it changed. POST /api/library/merges/{mergeId}/undo
(auditors, newest merge first) puts it back; users keep later status
and rating, nothing is paid twice. A nightly job drops copies after 30
days. Old keys follow game_alias to the kept game. Manual games can now
be hidden and shown like Steam games.
GET /api/library/catalog finds any game a user has, by name, for
auditors; GET /api/library/catalog/{appid} loads it for the correction
dialog. New correction field TIME_TO_BEAT (normal + completionist,
1..60000 min, completionist >= normal) that survives the IGDB refresh.
Every correction and reset writes GAME_CORRECT / GAME_CORRECTION_RESET
to admin_audit.
GET /api/library/audit-log for auditors and admins, paged, filter by
action. The account state push stores displayName in account_name so
the log shows names. Contract test updated.
All game counts read the counted_user_game view, which keeps only the
most advanced edition of a linked set (older edition wins a tie). Add
the hideDuplicates filter to the library query.
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.
Add endpoints to buy items, equip looks by slot and unequip. The shop has
8 whitelisted looks: avatar borders, badges, accents and banner frames.
Ownership is a BUY ledger row; PLATFORM_ADMIN owns every catalog item
without buying. The public profile endpoint shares only the equipped looks
filtered by ownership.
New module rewards tracks Pile Points in an append-only ledger. A settle
derives rows from the status log, playtime days and achievements, once per
user, kind and game. The balance is the sum of all rows. Settle runs on
every wallet read and nightly at 02:30. The unique key (user_id, kind, ref)
makes every run idempotent. V26 also pays a one-time starter grant for
finishes from before the status log.
Add PLATFORMS stat group to user stats: games and playtime per platform.
Add platforms, genres, and finished-per-year statistics to public stats
for the landing page.
C14 C16 C17 C18 C20 A11 A15 X10 X12 X15: dead Steam count code, GameNames, steamIdText, one query per game for the finished estimate, no API versioning config, open-in-view off, CORS from APP_FRONTEND_URL, non-root image, prune only up to the highest known id.
A blocked user gets no sync and leaves the landing stats. A new or removed Steam ID stops the nightly sync of the old one. No token check in Core: the token lives until it expires.
Every @Async names a pool: with the scheduler there are two TaskExecutor beans, and a bare @Async ran on an unbounded executor. Page views queue only stale games; the long runs after a sync get their own pool.
Profile "local" points Steam, Twitch/IGDB and speedrun.com to a WireMock
container from compose.yaml (recordings in local/wiremock) and checks
logins against the local Auth.
The IGDB lookups now keep only the ids they asked for: an answer with
another game made tags and release years drop the whole batch.
An auditor merges a game added by hand into its Steam game or another
game added by hand (V21). Every user's row moves; a user with both rows
keeps one, with the status, rating and notes set by hand. game_alias
sends later adds to the kept key, and admin_audit logs who merged what.
The game search shows games already in ToBePlayed first.
An AUDITOR corrects name, header image, speedrun link, moods, endless and genres of a game for every user (V20 game_correction, PUT/DELETE /api/library/games/{appid}/corrections/{field}). The value is also written into the cached row, so reads stay unchanged; the Steam sync, the image repair, the IGDB add and the mood and speedrun upserts skip corrected fields. SteamGame gets @DynamicUpdate so a sync no longer writes back columns it did not change.
Every service method a controller calls carries @RequiresRole, @Public or @Internal; a Spring Security method interceptor checks it while a request is served. Core now grants the active role from the token and every lower one. ArchUnit fails the build for an unmarked method, two markers, a controller calling an @Async method, or a service reaching a method that needs more. The background refreshes moved from LibraryController into the services, because an @Async method is never checked.