ce2b234 fixed the concurrent-call race, but a second, distinct bug
produces the same "plays twice" symptom: Liquidsoap's request.dynamic.list
refills its queue by calling /internal/:slug/next on a schedule
independent of when its separate on_metadata hook gets around to
confirming the previous pick via /playing. Those two calls are
sequential, not concurrent, so the row lock doesn't help -- if the queue
refill asks again long before the confirm arrives, next_track faithfully
replayed the same pending pick, and Liquidsoap's request.create(uri)
built a genuine second, full-length playback for it.
Added stations.rotation_pending_set_at (new migration) to timestamp when
a pending pull was first served. Past a 15s grace window (comfortably
above Liquidsoap's retry_delay=3.0), a re-ask for the same pending pick
now self-confirms it into play_history and draws a fresh one instead of
repeating it. Extracted confirm_in_conn as the shared, idempotent core of
"record a play, close the previous one, clear the pending marker" --
idempotent specifically so the real, now-redundant /playing POST for a
self-confirmed pick is a no-op rather than double-recording the play or
clobbering a newer pending pick drawn in the meantime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>