You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During the scheduled RA progress sync, a user's rom_user.status is automatically derived from their RetroAchievements award and kept in sync on every run, so progress changes (e.g. INCOMPLETE → FINISHED → COMPLETED_100) are always reflected.
Award → Status mapping
RA HighestAwardKind
RomUserStatus
mastered / completed
COMPLETED_100
beaten-hardcore / beaten-softcore
FINISHED
(none — started but not beaten)
INCOMPLETE
Changes
ra_handler.py — Added highest_award_kind: NotRequired[str | None] to RAUserGameProgression. Populated from HighestAwardKind in the completion-progress API response; refreshed even on cache-hit paths so the field stays current without extra API calls.
sync_retroachievements_progress.py — Added get_rom_user_status_from_ra_award_kind() mapping helper and _sync_rom_user_statuses() which, after each successful user progression update, looks up each RA game ID in the local ROM database and writes the derived status whenever it differs from the currently stored value. The DB write is skipped only when the status already matches.
test_sync_retroachievements_progress.py — Full coverage of the mapping helper, the always-update behaviour across sync runs, and the no-op guard when the status is already up-to-date.
Original prompt
This section details on the original issue you should resolve
<issue_title>[Feature] Update game status via Retroachievements</issue_title>
<issue_description>Hey there! Just found this neat project and set it up, so apologies if this possible through some config somewhere.
I'm happy to see RA integration in this project. It would be neat if a game's Status was automatically updated based on the progress in RA:
That is, if a game is:
"Unfinished" in RA, set the status to "Incomplete"
"Beaten" in RA, set the status to "Finished"
"Mastered" in RA, set the status to "Completed 100%".
And of course, the RA result should only auto-apply if the existing state of the game is unset to avoid overwriting manually-set statuses.</issue_description>
Comments on the Issue (you are @copilot in this section)
CopilotAI
changed the title
[WIP] Update game status based on Retroachievements progress
feat: Auto-update game status from RetroAchievements award kind during progress sync
Mar 11, 2026
This PR auto-derives and persists rom_user.status from a user's RetroAchievements award kind during the scheduled progress sync, mapping mastered/completed → COMPLETED_100, beaten-* → FINISHED, and no award → INCOMPLETE. It also adds highest_award_kind to the RAUserGameProgression cache structure so the field stays fresh even on cache-hit paths without extra API calls.
Key changes:
ra_handler.py: new highest_award_kind: NotRequired[str | None] field on RAUserGameProgression; populated from HighestAwardKind in the RA completion-progress response; cache-hit branch always refreshes this field from the live API value.
sync_retroachievements_progress.py: adds _get_rom_user_status_from_ra_award_kind() mapping helper and _sync_rom_user_statuses() which writes the derived status after each successful progression update, skipping updates when the status already matches or is RETIRED/NEVER_PLAYING.
test_sync_retroachievements_progress.py: covers the mapping helper and five status-sync scenarios, though the RETIRED/NEVER_PLAYING guard itself is not directly tested.
Issues found:
_sync_rom_user_statuses is invoked in the else block of the try/except, which means any exception it raises (DB error, etc.) is unhandled and will abort the sync for all remaining users in the current run — the existing error-and-continue pattern used for get_user_progression failures should be applied here too.
No test covers the RETIRED/NEVER_PLAYING preservation logic, which is the primary safety mechanism requested in the original issue.
Confidence Score: 3/5
PR is mostly correct but has an unhandled exception path that can abort the entire sync run for all remaining users.
The mapping logic and cache-refresh behaviour are sound, and the RETIRED/NEVER_PLAYING guard addresses the core concern from the original issue. However, placing _sync_rom_user_statuses in the unprotected else block is a real bug: a single DB exception aborts the loop for all subsequent users, which undermines the resilience pattern already present in the task. The missing test for the preservation guard also reduces confidence. These are fixable but need to be addressed before merging.
Pay close attention to backend/tasks/scheduled/sync_retroachievements_progress.py around the _sync_rom_user_statuses call and its error handling.
Adds _get_rom_user_status_from_ra_award_kind mapping helper and _sync_rom_user_statuses to auto-update rom_user statuses. Critical issue: _sync_rom_user_statuses is called in the else block without error handling, so any DB exception terminates the entire sync loop.
backend/handler/metadata/ra_handler.py
Adds highest_award_kind to RAUserGameProgression TypedDict and populates it from HighestAwardKind in the RA completion-progress API response. Cache-hit path correctly refreshes the field from the live response so it stays current without extra API calls.
Good coverage of the mapping helper and common sync scenarios, but missing a test for the RETIRED/NEVER_PLAYING preservation guard, which is the central safety mechanism of this feature.
Sequence Diagram
sequenceDiagram
participant Task as SyncRetroAchievementsProgressTask
participant RA as RAHandler
participant UserDB as DBUsersHandler
participant RomDB as DBRomsHandler
Task->>UserDB: get_users(has_ra_username=True)
loop For each user
Task->>RA: get_user_progression(username, current_progression)
RA-->>Task: RAUserProgression (with highest_award_kind)
Task->>UserDB: update_user(id, {ra_progression})
Note over Task: else block (unprotected)
Task->>Task: _sync_rom_user_statuses(user, progression)
loop For each game in progression
Task->>Task: _get_rom_user_status_from_ra_award_kind(highest_award_kind)
Task->>RomDB: get_rom_by_metadata_id(ra_id=rom_ra_id)
RomDB-->>Task: Rom or None
Task->>RomDB: get_rom_user(rom.id, user.id)
RomDB-->>Task: RomUser or None
alt RomUser is None
Task->>RomDB: add_rom_user(rom.id, user.id)
RomDB-->>Task: new RomUser
end
alt status != new_status AND status not RETIRED/NEVER_PLAYING
Task->>RomDB: update_rom_user(id, {status: new_status})
end
end
end
Loading
Comments Outside Diff (1)
backend/tasks/scheduled/sync_retroachievements_progress.py, line 133-137 (link)
Unprotected call can terminate the entire sync loop
_sync_rom_user_statuses is placed in the else block of the try/except, but Python's else block is not protected by the except clause — exceptions raised there propagate directly to the caller. If any DB operation inside _sync_rom_user_statuses throws (connection error, DetachedInstanceError, UniqueConstraint violation on add_rom_user, etc.) the exception will bypass processed_users += 1 / update_stats.update(...) and unwind the entire for user in users: loop, silently aborting the sync for every subsequent user.
The existing try/except block already establishes the pattern of logging and continuing; _sync_rom_user_statuses should either be moved inside the try block or wrapped in its own guard:
else:
log.debug(
f"Updated RetroAchievements progress for user: {user.username}"
)
try:
_sync_rom_user_statuses(user, user_progression)
exceptExceptionase:
log.error(
f"Failed to sync ROM user statuses for user: {user.username}, error: {e}"
)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
During the scheduled RA progress sync, a user's
rom_user.statusis automatically derived from their RetroAchievements award and kept in sync on every run, so progress changes (e.g.INCOMPLETE→FINISHED→COMPLETED_100) are always reflected.Award → Status mapping
HighestAwardKindRomUserStatusmastered/completedCOMPLETED_100beaten-hardcore/beaten-softcoreFINISHEDINCOMPLETEChanges
ra_handler.py— Addedhighest_award_kind: NotRequired[str | None]toRAUserGameProgression. Populated fromHighestAwardKindin the completion-progress API response; refreshed even on cache-hit paths so the field stays current without extra API calls.sync_retroachievements_progress.py— Addedget_rom_user_status_from_ra_award_kind()mapping helper and_sync_rom_user_statuses()which, after each successful user progression update, looks up each RA game ID in the local ROM database and writes the derived status whenever it differs from the currently stored value. The DB write is skipped only when the status already matches.test_sync_retroachievements_progress.py— Full coverage of the mapping helper, the always-update behaviour across sync runs, and the no-op guard when the status is already up-to-date.Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.