Rewrites the internal/db/queries comments to rule 32 and regenerates dbq, whose doc comments and embedded SQL carry them. The bugs behind the tiered mixes, the md5 order seed, the score bands and the round-robin arms are restated as the constraint each one enforces; counts from one library and stale cross-references are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
375 lines
12 KiB
Go
375 lines
12 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: similarity.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const deleteSimilarRecordingsForSeed = `-- name: DeleteSimilarRecordingsForSeed :exec
|
|
DELETE FROM listenbrainz_similar_recordings WHERE seed_track_id = $1
|
|
`
|
|
|
|
// A fetch replaces the seed's cached answer whole.
|
|
func (q *Queries) DeleteSimilarRecordingsForSeed(ctx context.Context, seedTrackID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteSimilarRecordingsForSeed, seedTrackID)
|
|
return err
|
|
}
|
|
|
|
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 insertSimilarRecordings = `-- name: InsertSimilarRecordings :exec
|
|
INSERT INTO listenbrainz_similar_recordings (seed_track_id, recording_mbid, score)
|
|
SELECT $1::uuid, r.mbid, max(r.score)
|
|
FROM (SELECT unnest($2::text[]) AS mbid,
|
|
unnest($3::float8[]) AS score) r
|
|
WHERE r.mbid <> ''
|
|
GROUP BY r.mbid
|
|
`
|
|
|
|
type InsertSimilarRecordingsParams struct {
|
|
SeedTrackID pgtype.UUID
|
|
Mbids []string
|
|
Scores []float64
|
|
}
|
|
|
|
// The seed's whole answer, in or out of the library. An MBID LB lists twice
|
|
// keeps its best score.
|
|
func (q *Queries) InsertSimilarRecordings(ctx context.Context, arg InsertSimilarRecordingsParams) error {
|
|
_, err := q.db.Exec(ctx, insertSimilarRecordings, arg.SeedTrackID, arg.Mbids, arg.Scores)
|
|
return err
|
|
}
|
|
|
|
const listPlayedArtistsNeedingSimilarity = `-- name: ListPlayedArtistsNeedingSimilarity :many
|
|
SELECT ar.id, ar.mbid
|
|
FROM artists ar
|
|
LEFT JOIN artist_similarity_fetches f ON f.artist_id = ar.id
|
|
WHERE ar.mbid IS NOT NULL
|
|
AND EXISTS (
|
|
SELECT 1 FROM tracks t
|
|
JOIN play_events pe ON pe.track_id = t.id
|
|
WHERE t.artist_id = ar.id
|
|
)
|
|
AND (f.artist_id IS NULL OR f.fetched_at < now() - interval '30 days')
|
|
ORDER BY f.fetched_at NULLS FIRST, ar.id
|
|
LIMIT $1
|
|
`
|
|
|
|
type ListPlayedArtistsNeedingSimilarityRow struct {
|
|
ID pgtype.UUID
|
|
Mbid *string
|
|
}
|
|
|
|
// The artist queue, with the same shape and the same reason.
|
|
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 t.id, t.mbid
|
|
FROM tracks t
|
|
LEFT JOIN track_similarity_fetches f ON f.track_id = t.id
|
|
WHERE t.mbid IS NOT NULL
|
|
AND t.missing_since IS NULL
|
|
AND EXISTS (SELECT 1 FROM play_events pe WHERE pe.track_id = t.id)
|
|
AND (f.track_id IS NULL OR f.fetched_at < now() - interval '30 days')
|
|
ORDER BY f.fetched_at NULLS FIRST, t.id
|
|
LIMIT $1
|
|
`
|
|
|
|
type ListPlayedTracksNeedingSimilarityRow struct {
|
|
ID pgtype.UUID
|
|
Mbid *string
|
|
}
|
|
|
|
// The worker's track queue: played, present tracks with a recording MBID that
|
|
// ListenBrainz has never answered for, or not in 30 days. Never-fetched first,
|
|
// then the oldest answer. Freshness is read from track_similarity_fetches, not
|
|
// from the edges, so a seed whose answer matched nothing in the library still
|
|
// counts as fetched; reading the edges would let such seeds hold the head of
|
|
// the queue forever.
|
|
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 listSimilarArtistsForArtist = `-- name: ListSimilarArtistsForArtist :many
|
|
SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, artists.artist_thumb_path, artists.artist_fanart_path, artists.artist_art_source, artists.artist_art_sources_version,
|
|
cov.id AS cover_album_id,
|
|
cnt.album_count::bigint AS album_count
|
|
FROM (
|
|
SELECT artist_b_id, max(score) AS sim_score
|
|
FROM artist_similarity
|
|
WHERE artist_a_id = $1
|
|
GROUP BY artist_b_id
|
|
) s
|
|
JOIN artists ON artists.id = s.artist_b_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT id FROM albums
|
|
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
|
|
ORDER BY created_at DESC LIMIT 1
|
|
) cov ON true
|
|
LEFT JOIN LATERAL (
|
|
SELECT count(*) AS album_count
|
|
FROM albums WHERE artist_id = artists.id
|
|
) cnt ON true
|
|
ORDER BY s.sim_score DESC, artists.sort_name
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListSimilarArtistsForArtistParams struct {
|
|
SeedArtistID pgtype.UUID
|
|
ResultLimit int32
|
|
}
|
|
|
|
type ListSimilarArtistsForArtistRow struct {
|
|
Artist Artist
|
|
CoverAlbumID pgtype.UUID
|
|
AlbumCount int64
|
|
}
|
|
|
|
// In-library artists similar to a seed artist, ranked by best similarity
|
|
// score across sources (deduped per candidate). cover_album_id + album_count
|
|
// mirror ListArtistsAlphaWithCovers so the strip renders identical cards.
|
|
func (q *Queries) ListSimilarArtistsForArtist(ctx context.Context, arg ListSimilarArtistsForArtistParams) ([]ListSimilarArtistsForArtistRow, error) {
|
|
rows, err := q.db.Query(ctx, listSimilarArtistsForArtist, arg.SeedArtistID, arg.ResultLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListSimilarArtistsForArtistRow
|
|
for rows.Next() {
|
|
var i ListSimilarArtistsForArtistRow
|
|
if err := rows.Scan(
|
|
&i.Artist.ID,
|
|
&i.Artist.Name,
|
|
&i.Artist.SortName,
|
|
&i.Artist.Mbid,
|
|
&i.Artist.CreatedAt,
|
|
&i.Artist.UpdatedAt,
|
|
&i.Artist.ArtistThumbPath,
|
|
&i.Artist.ArtistFanartPath,
|
|
&i.Artist.ArtistArtSource,
|
|
&i.Artist.ArtistArtSourcesVersion,
|
|
&i.CoverAlbumID,
|
|
&i.AlbumCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const recordArtistSimilarityFetch = `-- name: RecordArtistSimilarityFetch :exec
|
|
INSERT INTO artist_similarity_fetches (artist_id, fetched_at, returned)
|
|
VALUES ($1, now(), $2)
|
|
ON CONFLICT (artist_id) DO UPDATE SET fetched_at = now(), returned = EXCLUDED.returned
|
|
`
|
|
|
|
type RecordArtistSimilarityFetchParams struct {
|
|
ArtistID pgtype.UUID
|
|
Returned int32
|
|
}
|
|
|
|
func (q *Queries) RecordArtistSimilarityFetch(ctx context.Context, arg RecordArtistSimilarityFetchParams) error {
|
|
_, err := q.db.Exec(ctx, recordArtistSimilarityFetch, arg.ArtistID, arg.Returned)
|
|
return err
|
|
}
|
|
|
|
const recordTrackSimilarityFetch = `-- name: RecordTrackSimilarityFetch :exec
|
|
INSERT INTO track_similarity_fetches (track_id, fetched_at, returned)
|
|
VALUES ($1, now(), $2)
|
|
ON CONFLICT (track_id) DO UPDATE SET fetched_at = now(), returned = EXCLUDED.returned
|
|
`
|
|
|
|
type RecordTrackSimilarityFetchParams struct {
|
|
TrackID pgtype.UUID
|
|
Returned int32
|
|
}
|
|
|
|
func (q *Queries) RecordTrackSimilarityFetch(ctx context.Context, arg RecordTrackSimilarityFetchParams) error {
|
|
_, err := q.db.Exec(ctx, recordTrackSimilarityFetch, arg.TrackID, arg.Returned)
|
|
return err
|
|
}
|
|
|
|
const resolveListenBrainzTrackEdges = `-- name: ResolveListenBrainzTrackEdges :exec
|
|
WITH best AS (
|
|
SELECT seed_track_id, max(score) AS score
|
|
FROM listenbrainz_similar_recordings
|
|
WHERE $1::uuid[] IS NULL
|
|
OR seed_track_id = ANY($1::uuid[])
|
|
GROUP BY seed_track_id
|
|
),
|
|
want AS (
|
|
SELECT DISTINCT ON (c.seed_track_id, c.recording_mbid)
|
|
c.seed_track_id AS track_a_id, t.id AS track_b_id,
|
|
CASE WHEN b.score > 0 THEN c.score / b.score ELSE 0 END AS score
|
|
FROM listenbrainz_similar_recordings c
|
|
JOIN best b ON b.seed_track_id = c.seed_track_id
|
|
JOIN tracks t ON t.mbid = c.recording_mbid
|
|
AND t.missing_since IS NULL
|
|
AND t.id <> c.seed_track_id
|
|
WHERE $1::uuid[] IS NULL
|
|
OR c.seed_track_id = ANY($1::uuid[])
|
|
ORDER BY c.seed_track_id, c.recording_mbid, t.id
|
|
),
|
|
upserted AS (
|
|
INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at)
|
|
SELECT track_a_id, track_b_id, score, 'listenbrainz', now() FROM want
|
|
ON CONFLICT (track_a_id, track_b_id, source) DO UPDATE
|
|
SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at
|
|
WHERE track_similarity.score IS DISTINCT FROM EXCLUDED.score
|
|
RETURNING 1
|
|
)
|
|
DELETE FROM track_similarity ts
|
|
WHERE ts.source = 'listenbrainz'
|
|
AND ($1::uuid[] IS NULL OR ts.track_a_id = ANY($1::uuid[]))
|
|
AND EXISTS (SELECT 1 FROM track_similarity_fetches f WHERE f.track_id = ts.track_a_id)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM want w
|
|
WHERE w.track_a_id = ts.track_a_id AND w.track_b_id = ts.track_b_id
|
|
)
|
|
`
|
|
|
|
// Derives the listenbrainz edges in track_similarity from the cached answers:
|
|
// every cached recording that is a present track in the library becomes an
|
|
// edge, whenever it got there. Scoped to the given seeds, or every seed when
|
|
// seed_ids is NULL.
|
|
//
|
|
// One track per recording MBID: two copies of a song (an mp3 beside its flac,
|
|
// or one recording on two releases) would otherwise be two candidates, and the
|
|
// pool would offer the same song twice. No cap per seed: LB's answer is already
|
|
// at most 50.
|
|
//
|
|
// The score is RELATIVE: each edge's LB score over the seed's best score in its
|
|
// whole cached answer, so 1.0 is the seed's closest recording and the rest fall
|
|
// below it. LB's raw scores are counts, into the thousands, and the scorer
|
|
// reads similarity as 0 to 1, so a raw edge would outweigh likes, skips and
|
|
// recency together. The cache keeps the raw
|
|
// answer; only the edge is scaled.
|
|
//
|
|
// Only edges of seeds the cache owns are removed, those with a fetch row.
|
|
// Edges written before the cache existed, or copied onto a survivor by a fold
|
|
// (merge.sql), stay until their seed is fetched.
|
|
func (q *Queries) ResolveListenBrainzTrackEdges(ctx context.Context, seedIds []pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, resolveListenBrainzTrackEdges, seedIds)
|
|
return err
|
|
}
|
|
|
|
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 upsertArtistSimilarityUnmatched = `-- name: UpsertArtistSimilarityUnmatched :exec
|
|
INSERT INTO artist_similarity_unmatched (
|
|
seed_artist_id, candidate_mbid, candidate_name, score, source
|
|
) VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (seed_artist_id, candidate_mbid, source) DO UPDATE SET
|
|
candidate_name = EXCLUDED.candidate_name,
|
|
score = EXCLUDED.score,
|
|
fetched_at = now()
|
|
`
|
|
|
|
type UpsertArtistSimilarityUnmatchedParams struct {
|
|
SeedArtistID pgtype.UUID
|
|
CandidateMbid string
|
|
CandidateName string
|
|
Score float64
|
|
Source string
|
|
}
|
|
|
|
// Persists an out-of-library similar-artist MBID. Idempotent on
|
|
// (seed_artist_id, candidate_mbid, source) — re-fetches refresh the
|
|
// name/score and bump fetched_at.
|
|
func (q *Queries) UpsertArtistSimilarityUnmatched(ctx context.Context, arg UpsertArtistSimilarityUnmatchedParams) error {
|
|
_, err := q.db.Exec(ctx, upsertArtistSimilarityUnmatched,
|
|
arg.SeedArtistID,
|
|
arg.CandidateMbid,
|
|
arg.CandidateName,
|
|
arg.Score,
|
|
arg.Source,
|
|
)
|
|
return err
|
|
}
|