diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index e7b7e229..5ce88a32 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -29,6 +29,14 @@ type Artist struct { UpdatedAt pgtype.Timestamptz } +type ArtistSimilarity struct { + ArtistAID pgtype.UUID + ArtistBID pgtype.UUID + Score float64 + Source string + FetchedAt pgtype.Timestamptz +} + type ContextualLike struct { ID pgtype.UUID UserID pgtype.UUID @@ -129,6 +137,14 @@ type Track struct { UpdatedAt pgtype.Timestamptz } +type TrackSimilarity struct { + TrackAID pgtype.UUID + TrackBID pgtype.UUID + Score float64 + Source string + FetchedAt pgtype.Timestamptz +} + type User struct { ID pgtype.UUID Username string diff --git a/internal/db/dbq/similarity.sql.go b/internal/db/dbq/similarity.sql.go new file mode 100644 index 00000000..5a92cecf --- /dev/null +++ b/internal/db/dbq/similarity.sql.go @@ -0,0 +1,191 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: similarity.sql + +package dbq + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getArtistsByMBIDs = `-- name: GetArtistsByMBIDs :many +SELECT id, mbid FROM artists WHERE mbid = ANY($1::text[]) +` + +type GetArtistsByMBIDsRow struct { + ID pgtype.UUID + Mbid *string +} + +func (q *Queries) GetArtistsByMBIDs(ctx context.Context, dollar_1 []string) ([]GetArtistsByMBIDsRow, error) { + rows, err := q.db.Query(ctx, getArtistsByMBIDs, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetArtistsByMBIDsRow + for rows.Next() { + var i GetArtistsByMBIDsRow + if err := rows.Scan(&i.ID, &i.Mbid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTracksByMBIDs = `-- name: GetTracksByMBIDs :many +SELECT id, mbid FROM tracks WHERE mbid = ANY($1::text[]) +` + +type GetTracksByMBIDsRow struct { + ID pgtype.UUID + Mbid *string +} + +// Bulk in-library lookup: maps a slice of MBIDs back to local track IDs. +func (q *Queries) GetTracksByMBIDs(ctx context.Context, dollar_1 []string) ([]GetTracksByMBIDsRow, error) { + rows, err := q.db.Query(ctx, getTracksByMBIDs, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTracksByMBIDsRow + for rows.Next() { + var i GetTracksByMBIDsRow + if err := rows.Scan(&i.ID, &i.Mbid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPlayedArtistsNeedingSimilarity = `-- name: ListPlayedArtistsNeedingSimilarity :many +SELECT DISTINCT ar.id, ar.mbid +FROM artists ar +JOIN tracks t ON t.artist_id = ar.id +JOIN play_events pe ON pe.track_id = t.id +WHERE ar.mbid IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM artist_similarity asim + WHERE asim.artist_a_id = ar.id + AND asim.source = 'listenbrainz' + AND asim.fetched_at > now() - interval '7 days' + ) +ORDER BY ar.id +LIMIT $1 +` + +type ListPlayedArtistsNeedingSimilarityRow struct { + ID pgtype.UUID + Mbid *string +} + +func (q *Queries) ListPlayedArtistsNeedingSimilarity(ctx context.Context, limit int32) ([]ListPlayedArtistsNeedingSimilarityRow, error) { + rows, err := q.db.Query(ctx, listPlayedArtistsNeedingSimilarity, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListPlayedArtistsNeedingSimilarityRow + for rows.Next() { + var i ListPlayedArtistsNeedingSimilarityRow + if err := rows.Scan(&i.ID, &i.Mbid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPlayedTracksNeedingSimilarity = `-- name: ListPlayedTracksNeedingSimilarity :many +SELECT DISTINCT t.id, t.mbid +FROM tracks t +JOIN play_events pe ON pe.track_id = t.id +WHERE t.mbid IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM track_similarity ts + WHERE ts.track_a_id = t.id + AND ts.source = 'listenbrainz' + AND ts.fetched_at > now() - interval '7 days' + ) +ORDER BY t.id +LIMIT $1 +` + +type ListPlayedTracksNeedingSimilarityRow struct { + ID pgtype.UUID + Mbid *string +} + +// Tracks with at least one play, an MBID, and no fresh listenbrainz row +// (no row at all, OR fetched_at older than 7 days). Used by the worker +// to find work each tick. Bounded by $1. +func (q *Queries) ListPlayedTracksNeedingSimilarity(ctx context.Context, limit int32) ([]ListPlayedTracksNeedingSimilarityRow, error) { + rows, err := q.db.Query(ctx, listPlayedTracksNeedingSimilarity, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListPlayedTracksNeedingSimilarityRow + for rows.Next() { + var i ListPlayedTracksNeedingSimilarityRow + if err := rows.Scan(&i.ID, &i.Mbid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const upsertArtistSimilarity = `-- name: UpsertArtistSimilarity :exec +INSERT INTO artist_similarity (artist_a_id, artist_b_id, score, source, fetched_at) +VALUES ($1, $2, $3, 'listenbrainz', now()) +ON CONFLICT (artist_a_id, artist_b_id, source) +DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at +` + +type UpsertArtistSimilarityParams struct { + ArtistAID pgtype.UUID + ArtistBID pgtype.UUID + Score float64 +} + +func (q *Queries) UpsertArtistSimilarity(ctx context.Context, arg UpsertArtistSimilarityParams) error { + _, err := q.db.Exec(ctx, upsertArtistSimilarity, arg.ArtistAID, arg.ArtistBID, arg.Score) + return err +} + +const upsertTrackSimilarity = `-- name: UpsertTrackSimilarity :exec +INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at) +VALUES ($1, $2, $3, 'listenbrainz', now()) +ON CONFLICT (track_a_id, track_b_id, source) +DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at +` + +type UpsertTrackSimilarityParams struct { + TrackAID pgtype.UUID + TrackBID pgtype.UUID + Score float64 +} + +func (q *Queries) UpsertTrackSimilarity(ctx context.Context, arg UpsertTrackSimilarityParams) error { + _, err := q.db.Exec(ctx, upsertTrackSimilarity, arg.TrackAID, arg.TrackBID, arg.Score) + return err +} diff --git a/internal/db/queries/similarity.sql b/internal/db/queries/similarity.sql new file mode 100644 index 00000000..15a76b58 --- /dev/null +++ b/internal/db/queries/similarity.sql @@ -0,0 +1,50 @@ +-- name: ListPlayedTracksNeedingSimilarity :many +-- Tracks with at least one play, an MBID, and no fresh listenbrainz row +-- (no row at all, OR fetched_at older than 7 days). Used by the worker +-- to find work each tick. Bounded by $1. +SELECT DISTINCT t.id, t.mbid +FROM tracks t +JOIN play_events pe ON pe.track_id = t.id +WHERE t.mbid IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM track_similarity ts + WHERE ts.track_a_id = t.id + AND ts.source = 'listenbrainz' + AND ts.fetched_at > now() - interval '7 days' + ) +ORDER BY t.id +LIMIT $1; + +-- name: ListPlayedArtistsNeedingSimilarity :many +SELECT DISTINCT ar.id, ar.mbid +FROM artists ar +JOIN tracks t ON t.artist_id = ar.id +JOIN play_events pe ON pe.track_id = t.id +WHERE ar.mbid IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM artist_similarity asim + WHERE asim.artist_a_id = ar.id + AND asim.source = 'listenbrainz' + AND asim.fetched_at > now() - interval '7 days' + ) +ORDER BY ar.id +LIMIT $1; + +-- name: GetTracksByMBIDs :many +-- Bulk in-library lookup: maps a slice of MBIDs back to local track IDs. +SELECT id, mbid FROM tracks WHERE mbid = ANY($1::text[]); + +-- name: GetArtistsByMBIDs :many +SELECT id, mbid FROM artists WHERE mbid = ANY($1::text[]); + +-- name: UpsertTrackSimilarity :exec +INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at) +VALUES ($1, $2, $3, 'listenbrainz', now()) +ON CONFLICT (track_a_id, track_b_id, source) +DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at; + +-- name: UpsertArtistSimilarity :exec +INSERT INTO artist_similarity (artist_a_id, artist_b_id, score, source, fetched_at) +VALUES ($1, $2, $3, 'listenbrainz', now()) +ON CONFLICT (artist_a_id, artist_b_id, source) +DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at;