Skip to content

feat: Auto-update game status from RetroAchievements award kind during progress sync#3118

Merged
gantoine merged 5 commits intomasterfrom
copilot/update-game-status-retroachievements
Mar 13, 2026
Merged

feat: Auto-update game status from RetroAchievements award kind during progress sync#3118
gantoine merged 5 commits intomasterfrom
copilot/update-game-status-retroachievements

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 11, 2026

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. INCOMPLETEFINISHEDCOMPLETED_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:

Image

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)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI 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
Copilot AI requested a review from gantoine March 12, 2026 23:00
@gantoine gantoine marked this pull request as ready for review March 12, 2026 23:02
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 12, 2026

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
14285 9666 68% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
backend/handler/metadata/ra_handler.py 46% 🟢
backend/tasks/scheduled/sync_retroachievements_progress.py 94% 🟢
TOTAL 70% 🟢

updated for commit: 7146081 by action🐍

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 12, 2026

Test Results (mariadb)

1 002 tests  +18   1 001 ✅ +18   2m 36s ⏱️ +4s
    1 suites ± 0       1 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 7146081. ± Comparison against base commit 6f02a4b.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 12, 2026

Test Results (postgresql)

1 002 tests  +18   1 001 ✅ +18   2m 35s ⏱️ -11s
    1 suites ± 0       1 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 7146081. ± Comparison against base commit 6f02a4b.

♻️ This comment has been updated with latest results.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR auto-derives and persists rom_user.status from a user's RetroAchievements award kind during the scheduled progress sync, mapping mastered/completedCOMPLETED_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.

Important Files Changed

Filename Overview
backend/tasks/scheduled/sync_retroachievements_progress.py 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.
backend/tests/tasks/test_sync_retroachievements_progress.py 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)

  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)
                    except Exception as e:
                        log.error(
                            f"Failed to sync ROM user statuses for user: {user.username}, error: {e}"
                        )

Last reviewed commit: 7146081

@gantoine gantoine merged commit a2be621 into master Mar 13, 2026
11 checks passed
@gantoine gantoine deleted the copilot/update-game-status-retroachievements branch March 13, 2026 00:31
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.

[Feature] Update game status via Retroachievements

2 participants