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:
@@ -28,15 +28,20 @@ func (c ContextAffinity) Affinity(artistID pgtype.UUID) float64 {
|
||||
}
|
||||
|
||||
// LoadContextAffinity computes each artist's affinity for the user's CURRENT
|
||||
// daypart × weekday cell. For every artist with completed plays in the window
|
||||
// it compares the share of that artist's plays that fall in the current cell
|
||||
// context cell — daypart × weekday, narrowed by deviceClass when non-empty
|
||||
// (#1551; radio passes the current device, the daily mixes pass "" for a
|
||||
// device-agnostic cell). For every artist with completed plays in the window it
|
||||
// compares the share of that artist's plays that fall in the current cell
|
||||
// against the user's overall baseline share, shrinking sparse artists toward
|
||||
// the baseline. Returns an empty (all-neutral) affinity when the user has no
|
||||
// plays.
|
||||
func LoadContextAffinity(
|
||||
ctx context.Context, q *dbq.Queries, userID pgtype.UUID,
|
||||
ctx context.Context, q *dbq.Queries, userID pgtype.UUID, deviceClass string,
|
||||
) (ContextAffinity, error) {
|
||||
rows, err := q.ListArtistContextPlayCountsForUser(ctx, userID)
|
||||
rows, err := q.ListArtistContextPlayCountsForUser(ctx, dbq.ListArtistContextPlayCountsForUserParams{
|
||||
ID: userID,
|
||||
Column2: deviceClass,
|
||||
})
|
||||
if err != nil {
|
||||
return ContextAffinity{}, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user