feat(taste): device-class context conditioning — #1551
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>
This commit is contained in:
@@ -54,7 +54,7 @@ func (q *Queries) GetMostRecentPlaySessionForUser(ctx context.Context, userID pg
|
||||
}
|
||||
|
||||
const getOpenPlayEventForUser = `-- name: GetOpenPlayEventForUser :one
|
||||
SELECT id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind FROM play_events
|
||||
SELECT id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind, device_class FROM play_events
|
||||
WHERE user_id = $1 AND ended_at IS NULL
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
@@ -80,12 +80,13 @@ func (q *Queries) GetOpenPlayEventForUser(ctx context.Context, userID pgtype.UUI
|
||||
&i.ScrobbledAt,
|
||||
&i.Source,
|
||||
&i.PickKind,
|
||||
&i.DeviceClass,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getPlayEventByID = `-- name: GetPlayEventByID :one
|
||||
SELECT id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind FROM play_events WHERE id = $1
|
||||
SELECT id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind, device_class FROM play_events WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetPlayEventByID(ctx context.Context, id pgtype.UUID) (PlayEvent, error) {
|
||||
@@ -106,6 +107,7 @@ func (q *Queries) GetPlayEventByID(ctx context.Context, id pgtype.UUID) (PlayEve
|
||||
&i.ScrobbledAt,
|
||||
&i.Source,
|
||||
&i.PickKind,
|
||||
&i.DeviceClass,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -144,19 +146,22 @@ func (q *Queries) GetSystemPickKindForTrack(ctx context.Context, arg GetSystemPi
|
||||
|
||||
const insertPlayEvent = `-- name: InsertPlayEvent :one
|
||||
INSERT INTO play_events (
|
||||
user_id, track_id, session_id, started_at, client_id, source, pick_kind
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7::text)
|
||||
RETURNING id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind
|
||||
user_id, track_id, session_id, started_at, client_id, source, pick_kind,
|
||||
device_class
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7::text,
|
||||
$8::text)
|
||||
RETURNING id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind, device_class
|
||||
`
|
||||
|
||||
type InsertPlayEventParams struct {
|
||||
UserID pgtype.UUID
|
||||
TrackID pgtype.UUID
|
||||
SessionID pgtype.UUID
|
||||
StartedAt pgtype.Timestamptz
|
||||
ClientID *string
|
||||
Source *string
|
||||
PickKind *string
|
||||
UserID pgtype.UUID
|
||||
TrackID pgtype.UUID
|
||||
SessionID pgtype.UUID
|
||||
StartedAt pgtype.Timestamptz
|
||||
ClientID *string
|
||||
Source *string
|
||||
PickKind *string
|
||||
DeviceClass *string
|
||||
}
|
||||
|
||||
// pick_kind is non-NULL only for system-playlist plays whose track was
|
||||
@@ -171,6 +176,7 @@ func (q *Queries) InsertPlayEvent(ctx context.Context, arg InsertPlayEventParams
|
||||
arg.ClientID,
|
||||
arg.Source,
|
||||
arg.PickKind,
|
||||
arg.DeviceClass,
|
||||
)
|
||||
var i PlayEvent
|
||||
err := row.Scan(
|
||||
@@ -188,6 +194,7 @@ func (q *Queries) InsertPlayEvent(ctx context.Context, arg InsertPlayEventParams
|
||||
&i.ScrobbledAt,
|
||||
&i.Source,
|
||||
&i.PickKind,
|
||||
&i.DeviceClass,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -333,7 +340,7 @@ SET ended_at = $2,
|
||||
completion_ratio = $4,
|
||||
was_skipped = $5
|
||||
WHERE id = $1
|
||||
RETURNING id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind
|
||||
RETURNING id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind, device_class
|
||||
`
|
||||
|
||||
type UpdatePlayEventEndedParams struct {
|
||||
@@ -371,6 +378,7 @@ func (q *Queries) UpdatePlayEventEnded(ctx context.Context, arg UpdatePlayEventE
|
||||
&i.ScrobbledAt,
|
||||
&i.Source,
|
||||
&i.PickKind,
|
||||
&i.DeviceClass,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user