Files
minstrel/internal/scrobble/queue.go
T
bvandeusen dba1deaad1 feat(scrobble): add MaybeEnqueue with idempotent insert + threshold gate
Bridges closed play_events to the scrobble_queue: loads the event,
applies the Qualifies() threshold, checks user LB config (enabled + token),
and inserts via ON CONFLICT DO NOTHING for idempotency. Best-effort —
callers (playevents.Writer hooks, Task 8) will log and swallow errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:13:11 -04:00

50 lines
1.4 KiB
Go

package scrobble
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// MaybeEnqueue inserts a scrobble_queue row for the given play_event if:
// 1. The user has listenbrainz_enabled = true with a non-empty token.
// 2. The play_event passes Qualifies().
//
// Idempotent via UNIQUE(play_event_id) — re-runs are no-ops.
//
// Best-effort: callers (the playevents.Writer hooks) should log returned
// errors but not propagate them, since scrobble delivery is enrichment
// and must not block the canonical play history.
func MaybeEnqueue(ctx context.Context, q *dbq.Queries, playEventID pgtype.UUID) error {
pe, err := q.GetPlayEventByID(ctx, playEventID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil
}
return err
}
// Only closed events qualify (DurationPlayedMs/CompletionRatio populated).
if pe.DurationPlayedMs == nil || pe.CompletionRatio == nil {
return nil
}
if !Qualifies(int(*pe.DurationPlayedMs), *pe.CompletionRatio) {
return nil
}
cfg, err := q.GetListenBrainzConfig(ctx, pe.UserID)
if err != nil {
return err
}
if !cfg.ListenbrainzEnabled || cfg.ListenbrainzToken == nil || *cfg.ListenbrainzToken == "" {
return nil
}
_, err = q.EnqueueScrobble(ctx, dbq.EnqueueScrobbleParams{
UserID: pe.UserID,
PlayEventID: pe.ID,
})
return err
}