5749f48b4a
Milestone #160 Opt 3b. Adds device class as a third context axis on top of the #1531 time-of-day/weekday affinity: on the radio path, a candidate is boosted when its artist concentrates in the current (daypart × weekday × device) cell. Client-sent (client_id is opaque; no UA stored), so it's captured going forward and applies to radio only (daily mixes are cron-built with no device → stay device-agnostic). Server: - Migration 0048: play_events.device_class text NULL (no CHECK; normalized in Go — one whitelist entry per new client class, not a migration). - events.go: eventRequest.device_class + normalizeDeviceClass (whitelist → mobile/web/…, else "other", empty → NULL); threaded through both RecordPlayStartedWithSource and RecordOfflinePlay into InsertPlayEvent. - ListArtistContextPlayCountsForUser gains a current-device param; the cell FILTER adds AND ($2='' OR device_class=$2) — '' reproduces the #1531 time-only behaviour exactly (used by mixes). SessionVector.DeviceClass carries it; the radio handler derives the current device from the user's latest play (GetLatestPlayDeviceClassForUser) — request-free proxy. - No new tuning knob: device narrows the existing ContextAffinityScore (reuses context_time_weight). Clients: - web: play_started sends device_class 'web'. - android: play_started + offline replay send 'mobile' (EventsWire + PlayOfflinePayload + MutationReplayer + PlayEventsReporter). Test: LoadContextAffinity device-narrowing integration test (mobile vs web artist separation; device-agnostic parity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
3.6 KiB
SQL
104 lines
3.6 KiB
SQL
-- name: GetMostRecentPlaySessionForUser :one
|
|
SELECT * FROM play_sessions
|
|
WHERE user_id = $1
|
|
ORDER BY last_event_at DESC
|
|
LIMIT 1;
|
|
|
|
-- name: InsertPlaySession :one
|
|
INSERT INTO play_sessions (user_id, started_at, last_event_at, client_id)
|
|
VALUES ($1, $2, $2, $3)
|
|
RETURNING *;
|
|
|
|
-- name: TouchPlaySessionLastEvent :exec
|
|
UPDATE play_sessions
|
|
SET last_event_at = $2,
|
|
track_count = track_count + 1
|
|
WHERE id = $1;
|
|
|
|
-- name: GetOpenPlayEventForUser :one
|
|
-- Returns the most recent play_event for a user where ended_at IS NULL.
|
|
-- Used by the auto-close-prior step in playevents.RecordPlayStarted.
|
|
SELECT * FROM play_events
|
|
WHERE user_id = $1 AND ended_at IS NULL
|
|
ORDER BY started_at DESC
|
|
LIMIT 1;
|
|
|
|
-- name: InsertPlayEvent :one
|
|
-- pick_kind is non-NULL only for system-playlist plays whose track was
|
|
-- found (with a stamped kind) in the user's live snapshot for that
|
|
-- variant at ingestion time (#1249, generalized in #1270).
|
|
INSERT INTO play_events (
|
|
user_id, track_id, session_id, started_at, client_id, source, pick_kind,
|
|
device_class
|
|
) VALUES ($1, $2, $3, $4, $5, $6, sqlc.narg(pick_kind)::text,
|
|
sqlc.narg(device_class)::text)
|
|
RETURNING *;
|
|
|
|
-- name: GetSystemPickKindForTrack :one
|
|
-- Looks a track up in the user's CURRENT snapshot of the given system
|
|
-- variant and returns its pick_kind. Used at play-ingestion time to
|
|
-- freeze provenance onto the play_event — snapshots rebuild daily, so
|
|
-- attribution can't be reconstructed at read time (#1249/#1270). No
|
|
-- row = track not in today's snapshot (caller stores NULL); a row with
|
|
-- NULL pick_kind = the variant doesn't stamp (yet). songs_like_artist
|
|
-- is non-singleton (up to 3 mixes/user); a track in two of them takes
|
|
-- whichever LIMIT 1 hits — acceptable, tier stamps there describe the
|
|
-- same eligibility ladder.
|
|
SELECT pt.pick_kind
|
|
FROM playlist_tracks pt
|
|
JOIN playlists p ON p.id = pt.playlist_id
|
|
WHERE p.user_id = $1
|
|
AND p.system_variant = $2
|
|
AND pt.track_id = $3
|
|
LIMIT 1;
|
|
|
|
-- name: UpdatePlayEventEnded :one
|
|
-- Closes a play_event by id with the given ended_at, duration, and skip flag.
|
|
-- completion_ratio is computed from duration_played_ms and the track duration
|
|
-- (looked up by the caller — sqlc doesn't do joins on UPDATE).
|
|
UPDATE play_events
|
|
SET ended_at = $2,
|
|
duration_played_ms = $3,
|
|
completion_ratio = $4,
|
|
was_skipped = $5
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: GetPlayEventByID :one
|
|
SELECT * FROM play_events WHERE id = $1;
|
|
|
|
-- name: InsertSkipEvent :one
|
|
INSERT INTO skip_events (user_id, track_id, session_id, skipped_at, position_ms)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *;
|
|
|
|
-- name: ListRecentSessionTracks :many
|
|
-- Returns up to $3 tracks in session $1 whose play_event started before
|
|
-- $2, ordered newest-first. Used by playevents.RecordPlayStarted to
|
|
-- build the session_vector for the just-inserted play_event.
|
|
SELECT t.* FROM tracks t
|
|
JOIN play_events pe ON pe.track_id = t.id
|
|
WHERE pe.session_id = $1
|
|
AND pe.started_at < $2
|
|
ORDER BY pe.started_at DESC
|
|
LIMIT $3;
|
|
|
|
-- name: UpdatePlayEventVector :exec
|
|
-- Used right after InsertPlayEvent to populate session_vector_at_play
|
|
-- once the vector has been computed.
|
|
UPDATE play_events
|
|
SET session_vector_at_play = $2
|
|
WHERE id = $1;
|
|
|
|
-- name: GetCurrentSessionVectorForUser :one
|
|
-- Returns the session_vector_at_play of the user's most recent play_event
|
|
-- in a still-active (un-timed-out) session. NoRows means no current vector.
|
|
-- Joined with play_sessions so closed sessions don't leak stale vectors.
|
|
SELECT pe.session_vector_at_play
|
|
FROM play_events pe
|
|
JOIN play_sessions s ON s.id = pe.session_id
|
|
WHERE pe.user_id = $1
|
|
AND s.ended_at IS NULL
|
|
ORDER BY pe.started_at DESC
|
|
LIMIT 1;
|