Prevent duplicate play records from handoffs - #8
Merged
Merged
Conversation
A play that moved back to a device that had already recorded it (quick handoffs back and forth, e.g. "Stop casting" tapped repeatedly while the track loaded) reached its scrobble threshold there again with the carried played time, and record_play_in wrote a second play_history row for the same track and start. The Android Home "Continue" list keys rows by start and track, so Compose threw on the pair on every launch. - record_play_in: a play (server, track, played_at) that already has a row returns that row (marking it scrobbled if this verdict says so) and bumps no counts; record_verdict enqueues no second submission for it. - Migration 0004 keeps the first row of each duplicated play (scrobbled if any copy was), takes the extra local play counts back, drops the copies and adds a unique index on (server_id, track_id, played_at). - Home: one row per play, so a repeated key can never reach the list. - ConnectRouteProvider: after "Stop casting" the routing session stays released until playback is back here (or 10 s pass), so the chip does not come back mid-handoff and invite another tap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K2WZ1wVZY63mQNMvbtWxpC
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Fixes a bug where plays that moved back to a device after a handoff could be recorded twice if they reached their scrobble threshold again on the original device. This caused crashes in the Android Home screen which uses play start time and track ID as a unique key.
Key Changes
Database Migration (0004): Added a unique index on
play_history(server_id, track_id, played_at)to enforce that a play (identified by its track and original start time) can only be recorded once. The migration cleans up any existing duplicates by keeping the first row (marked as scrobbled if any copy was), adjusting local play counts, and deleting duplicates.Play Recording Logic: Modified
record_play_in()to check if a play is already recorded before inserting a new row. If found, it returns the existing row's ID and only updates thescrobbledflag if needed, preventing duplicate counting.Verdict Recording: Updated
ScrobbleRecorder::record_verdict()to skip enqueueing a scrobble submission if the play was already recorded, preventing duplicate submissions while still allowing the verdict to be processed.Android Handoff Handling: Enhanced
ConnectRouteProviderto track when playback is returning to the local device after "Stop casting" is selected. The session remains released during the return window to prevent the casting chip from reappearing mid-handoff, which would trigger another handoff on the next tap.Android Home Screen: Added deduplication in the recent plays list using
distinctBy { playedAt to trackId }to handle older databases that might contain duplicate plays.Implementation Details
(server_id, track_id, played_at)whereplayed_atis the session's original start time, unchanged across handoffs.PENDING_MS) to eventually clear the returning state if the core doesn't confirm the return.https://claude.ai/code/session_01K2WZ1wVZY63mQNMvbtWxpC