Skip to content

Prevent duplicate play records from handoffs - #8

Merged
ingoau merged 1 commit into
mainfrom
claude/ecstatic-davinci-idi747
Sep 25, 2026
Merged

ingoau merged 1 commit into
mainfrom
claude/ecstatic-davinci-idi747

Conversation

@ingoau

@ingoau ingoau commented Sep 25, 2026

Copy link
Copy Markdown
Owner

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 the scrobbled flag 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 ConnectRouteProvider to 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

  • A play's identity is defined as the combination of (server_id, track_id, played_at) where played_at is the session's original start time, unchanged across handoffs.
  • The unique index prevents new duplicates while the migration handles existing ones.
  • The handoff return tracking uses a timeout (PENDING_MS) to eventually clear the returning state if the core doesn't confirm the return.

https://claude.ai/code/session_01K2WZ1wVZY63mQNMvbtWxpC

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
@ingoau
ingoau merged commit 263ff54 into main Sep 25, 2026
4 checks passed
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.

2 participants