diff --git a/internal/db/migrations/0008_scrobble.down.sql b/internal/db/migrations/0008_scrobble.down.sql new file mode 100644 index 00000000..c5721872 --- /dev/null +++ b/internal/db/migrations/0008_scrobble.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS scrobble_queue; +ALTER TABLE users DROP COLUMN IF EXISTS listenbrainz_enabled; +ALTER TABLE users DROP COLUMN IF EXISTS listenbrainz_token; diff --git a/internal/db/migrations/0008_scrobble.up.sql b/internal/db/migrations/0008_scrobble.up.sql new file mode 100644 index 00000000..536d268f --- /dev/null +++ b/internal/db/migrations/0008_scrobble.up.sql @@ -0,0 +1,26 @@ +-- M4a: outbound ListenBrainz scrobble worker. +-- Per-user LB config (plaintext token; users rotate via /settings if leaked). +-- scrobble_queue is a work list, not a log: successfully-sent rows are +-- DELETEd by the worker (the canonical record stays in play_events.scrobbled_at +-- via M2's column). Only `pending` and `failed` states exist. + +ALTER TABLE users + ADD COLUMN listenbrainz_token TEXT NULL, + ADD COLUMN listenbrainz_enabled BOOLEAN NOT NULL DEFAULT FALSE; + +CREATE TABLE scrobble_queue ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + play_event_id uuid NOT NULL REFERENCES play_events(id) ON DELETE CASCADE, + status TEXT NOT NULL CHECK (status IN ('pending', 'failed')), + attempts INTEGER NOT NULL DEFAULT 0, + next_attempt_at timestamptz NOT NULL DEFAULT now(), + last_error TEXT NULL, + enqueued_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (play_event_id) +); + +-- Hot path for the worker's per-tick query. +CREATE INDEX scrobble_queue_pending_idx + ON scrobble_queue (next_attempt_at) + WHERE status = 'pending';