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:
@@ -195,12 +195,14 @@ GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
|
||||
|
||||
-- name: ListArtistContextPlayCountsForUser :many
|
||||
-- Per-artist completed-play counts split by whether each play falls in the
|
||||
-- CURRENT daypart × weekday-type cell, in the user's local timezone (#1531).
|
||||
-- Feeds the context-affinity scoring term: an artist whose plays concentrate
|
||||
-- in the current cell (vs the user's overall baseline, computed Go-side from
|
||||
-- these rows) gets boosted right now. Skips excluded; a 365-day window bounds
|
||||
-- cost. Daypart buckets: night [22,5) morning [5,12) afternoon [12,17)
|
||||
-- evening [17,22). Weekend = ISO days 6–7 (Sat/Sun).
|
||||
-- CURRENT context cell, in the user's local timezone (#1531). The cell is
|
||||
-- daypart × weekday-type, optionally narrowed by device class (#1551): when
|
||||
-- $2 (the current device) is '', the device dimension is ignored (identical to
|
||||
-- the time-only #1531 behaviour, used by the daily mixes which have no device);
|
||||
-- when $2 is set (radio), a play only counts in the cell if its device_class
|
||||
-- matches. Feeds the context-affinity scoring term. Skips excluded; a 365-day
|
||||
-- window bounds cost. Daypart buckets: night [22,5) morning [5,12)
|
||||
-- afternoon [12,17) evening [17,22). Weekend = ISO days 6–7 (Sat/Sun).
|
||||
WITH tz AS (
|
||||
SELECT COALESCE(NULLIF(u.timezone, ''), 'UTC') AS zone
|
||||
FROM users u WHERE u.id = $1
|
||||
@@ -219,6 +221,7 @@ now_cell AS (
|
||||
),
|
||||
plays AS (
|
||||
SELECT t.artist_id,
|
||||
pe.device_class,
|
||||
CASE
|
||||
WHEN EXTRACT(hour FROM pe.started_at AT TIME ZONE tz.zone) < 5 THEN 3
|
||||
WHEN EXTRACT(hour FROM pe.started_at AT TIME ZONE tz.zone) < 12 THEN 0
|
||||
@@ -239,10 +242,21 @@ SELECT p.artist_id,
|
||||
count(*) FILTER (
|
||||
WHERE p.daypart = (SELECT daypart FROM now_cell)
|
||||
AND p.is_weekend = (SELECT is_weekend FROM now_cell)
|
||||
AND ($2::text = '' OR p.device_class = $2::text)
|
||||
) AS cell_plays
|
||||
FROM plays p
|
||||
GROUP BY p.artist_id;
|
||||
|
||||
-- name: GetLatestPlayDeviceClassForUser :one
|
||||
-- The device_class of the user's most recent play (#1551), used as the
|
||||
-- "current device" for radio context conditioning. NULL when the latest play
|
||||
-- predates device capture or came from a client that didn't send one.
|
||||
SELECT device_class
|
||||
FROM play_events
|
||||
WHERE user_id = $1
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: SuggestArtistsForUser :many
|
||||
-- M5c: per-user artist suggestions ranked by signal x similarity. The
|
||||
-- seeds CTE collects the user's likes (x5) plus recency-decayed plays
|
||||
|
||||
Reference in New Issue
Block a user