release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m36s
release / integration (push) Successful in 5m4s
release / android (push) Successful in 5m5s
release / Build signed APK (releases and dev) (push) Successful in 5m18s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 30s
release / Verify release artifacts (tag releases only) (push) Skipped
LB's similarity scores are counts (16 to 1445 for recordings, to 10,875 for artists), and they reached a scorer that reads similarity as 0 to 1. One edge added 64 to 5,800 points under Songs-like, so likes, skips and recency stopped mattering for any candidate with an edge. - ResolveListenBrainzTrackEdges divides each edge by the seed's best score in its whole cached answer. The cache keeps the raw answer. - The worker stores matched and unmatched artist rows over the seed's top artist. - Migration 0096 rescales the rows already stored. - The candidate arms hold sim_score bands so tier 1 stays above tier 2: lb_similar 0.5 + 0.5 x edge, tag_overlap halved, likes_overlap 0.3. Otherwise a seed's weaker LB edges fell below a shared genre tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
6.8 KiB
SQL
161 lines
6.8 KiB
SQL
-- name: ListPlayedTracksNeedingSimilarity :many
|
|
-- 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 (#5296); reading the edges is what let 25 such seeds hold
|
|
-- the head of the queue forever.
|
|
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;
|
|
|
|
-- name: ListPlayedArtistsNeedingSimilarity :many
|
|
-- The artist queue, with the same shape and the same reason.
|
|
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;
|
|
|
|
-- name: GetArtistsByMBIDs :many
|
|
SELECT id, mbid FROM artists WHERE mbid = ANY($1::text[]);
|
|
|
|
-- name: DeleteSimilarRecordingsForSeed :exec
|
|
-- A fetch replaces the seed's cached answer whole.
|
|
DELETE FROM listenbrainz_similar_recordings WHERE seed_track_id = $1;
|
|
|
|
-- name: InsertSimilarRecordings :exec
|
|
-- The seed's whole answer, in or out of the library. An MBID LB lists twice
|
|
-- keeps its best score.
|
|
INSERT INTO listenbrainz_similar_recordings (seed_track_id, recording_mbid, score)
|
|
SELECT sqlc.arg(seed_track_id)::uuid, r.mbid, max(r.score)
|
|
FROM (SELECT unnest(sqlc.arg(mbids)::text[]) AS mbid,
|
|
unnest(sqlc.arg(scores)::float8[]) AS score) r
|
|
WHERE r.mbid <> ''
|
|
GROUP BY r.mbid;
|
|
|
|
-- 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;
|
|
|
|
-- 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;
|
|
|
|
-- name: ResolveListenBrainzTrackEdges :exec
|
|
-- 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 (#6177). LB's raw scores are counts (16 to 1445 on the operator's
|
|
-- library) and reached the scorer, which reads similarity as 0 to 1, so one
|
|
-- edge outweighed 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.
|
|
WITH best AS (
|
|
SELECT seed_track_id, max(score) AS score
|
|
FROM listenbrainz_similar_recordings
|
|
WHERE sqlc.narg(seed_ids)::uuid[] IS NULL
|
|
OR seed_track_id = ANY(sqlc.narg(seed_ids)::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 sqlc.narg(seed_ids)::uuid[] IS NULL
|
|
OR c.seed_track_id = ANY(sqlc.narg(seed_ids)::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 (sqlc.narg(seed_ids)::uuid[] IS NULL OR ts.track_a_id = ANY(sqlc.narg(seed_ids)::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
|
|
);
|
|
|
|
-- 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;
|
|
|
|
-- name: UpsertArtistSimilarityUnmatched :exec
|
|
-- 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.
|
|
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();
|
|
|
|
-- name: ListSimilarArtistsForArtist :many
|
|
-- 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.
|
|
SELECT sqlc.embed(artists),
|
|
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 = sqlc.arg(seed_artist_id)
|
|
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 sqlc.arg(result_limit);
|